Skip to content

Commit 4654f0d

Browse files
author
0xMett
committed
feat(ban): support multi-ID ban with bracket syntax [id1, id2, id3]
1 parent 89d6967 commit 4654f0d

2 files changed

Lines changed: 152 additions & 1 deletion

File tree

src/python_italy_bot/handlers/moderation.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,65 @@ async def _handle_force_group_registration(
6969
await message.reply_text(strings.GROUP_REGISTERED.format(chat_id=chat.id))
7070

7171

72+
async def _handle_multi_ban(
73+
update: Update,
74+
context: ContextTypes.DEFAULT_TYPE,
75+
moderation_service: ModerationService,
76+
message,
77+
chat,
78+
multi_match: re.Match[str],
79+
raw_text: str,
80+
) -> None:
81+
"""Ban multiple users by ID. Usage: /ban [id1, id2, id3] [reason]."""
82+
inner = multi_match.group(1)
83+
parts = [p.strip() for p in inner.split(",") if p.strip()]
84+
85+
# Validate all parts are numeric IDs
86+
if not parts or not all(re.match(r"^-?\d+$", p) for p in parts):
87+
await message.reply_text(strings.BAN_USAGE)
88+
return
89+
90+
user_ids = list(dict.fromkeys(int(p) for p in parts)) # deduplicate, preserve order
91+
92+
# Extract reason from text after the closing bracket
93+
after_bracket = raw_text[multi_match.end() :].strip()
94+
reason = after_bracket if after_bracket else None
95+
96+
# Fetch chat list once (same for all users)
97+
chat_ids: list[int] | None = None
98+
total_success = 0
99+
total_fail = 0
100+
101+
for user_id in user_ids:
102+
ban_chat_ids = await moderation_service.add_global_ban(
103+
user_id, message.from_user.id, reason
104+
)
105+
if chat_ids is None:
106+
chat_ids = ban_chat_ids
107+
108+
for cid in ban_chat_ids:
109+
try:
110+
await context.bot.ban_chat_member(cid, user_id)
111+
total_success += 1
112+
except Exception as e:
113+
logger.debug("Ban user %s in chat %s failed: %s", user_id, cid, e)
114+
total_fail += 1
115+
116+
await message.reply_text(
117+
strings.ban_multi_success(len(user_ids), total_success, total_fail, reason)
118+
)
119+
120+
# Notify admins of the multi-ban
121+
await _notify_admins_of_multi_ban(
122+
context=context,
123+
chat=chat,
124+
admin=message.from_user,
125+
banned_ids=user_ids,
126+
success_count=total_success,
127+
reason=reason,
128+
)
129+
130+
72131
async def _handle_ban(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
73132
"""Ban a user globally. Usage: /ban user_id|@username [reason] or reply with /ban [reason]."""
74133
moderation_service: ModerationService = context.bot_data["moderation_service"]
@@ -84,7 +143,17 @@ async def _handle_ban(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
84143
await message.reply_text(strings.ONLY_ADMINS)
85144
return
86145

87-
args = message.text.split(maxsplit=2)[1:] if message.text else []
146+
raw_text = message.text or ""
147+
148+
# Multi-ban: /ban [id1, id2, id3] [reason]
149+
multi_match = re.search(r"\[([^\]]+)\]", raw_text)
150+
if multi_match:
151+
await _handle_multi_ban(
152+
update, context, moderation_service, message, chat, multi_match, raw_text
153+
)
154+
return
155+
156+
args = raw_text.split(maxsplit=2)[1:]
88157

89158
user_id: int | None = None
90159
reason: str | None = None
@@ -565,6 +634,52 @@ async def _notify_admins_of_ban(
565634
)
566635

567636

637+
async def _notify_admins_of_multi_ban(
638+
context: ContextTypes.DEFAULT_TYPE,
639+
chat,
640+
admin,
641+
banned_ids: list[int],
642+
success_count: int,
643+
reason: str | None,
644+
) -> None:
645+
"""Send multi-ban notification to all chat admins via private message."""
646+
try:
647+
chat_admins = await context.bot.get_chat_administrators(chat.id)
648+
except Exception as e:
649+
logger.warning("Failed to get admins for multi-ban notification: %s", e)
650+
return
651+
652+
chat_title = chat.title or "Chat"
653+
admin_name = _get_user_display_name(admin)
654+
655+
notification = strings.ban_multi_notification(
656+
chat_title=chat_title,
657+
banned_ids=banned_ids,
658+
admin_name=admin_name,
659+
admin_id=admin.id,
660+
success_count=success_count,
661+
reason=reason,
662+
)
663+
664+
for member in chat_admins:
665+
if member.user.is_bot:
666+
continue
667+
if member.user.id == admin.id:
668+
continue
669+
try:
670+
await context.bot.send_message(
671+
chat_id=member.user.id,
672+
text=notification,
673+
parse_mode="HTML",
674+
)
675+
except Exception as e:
676+
logger.debug(
677+
"Could not send multi-ban notification to admin %s: %s",
678+
member.user.id,
679+
e,
680+
)
681+
682+
568683
async def _resolve_user_id(
569684
context: ContextTypes.DEFAULT_TYPE,
570685
chat_id: int,

src/python_italy_bot/strings.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def get_default_welcome_template(bot_username: str) -> str:
6969
# Ban
7070
BAN_USAGE = (
7171
"Uso: /ban user_id|@username [motivo], "
72+
"/ban [id1, id2, id3] [motivo], "
7273
"o rispondi a un messaggio (anche di benvenuto) con /ban [motivo]."
7374
)
7475

@@ -104,6 +105,41 @@ def ban_notification(
104105
return text
105106

106107

108+
def ban_multi_success(
109+
user_count: int,
110+
success_count: int,
111+
fail_count: int,
112+
reason: str | None,
113+
) -> str:
114+
"""Format multi-ban success message."""
115+
msg = f"{user_count} utenti bannati globalmente in {success_count} gruppi."
116+
if fail_count > 0:
117+
msg += f" ({fail_count} falliti)"
118+
msg += f"\nMotivo: {reason or 'Nessuno'}"
119+
return msg
120+
121+
122+
def ban_multi_notification(
123+
chat_title: str,
124+
banned_ids: list[int],
125+
admin_name: str,
126+
admin_id: int,
127+
success_count: int,
128+
reason: str | None,
129+
) -> str:
130+
"""Format multi-ban notification message for admins."""
131+
safe_chat_title = html.escape(chat_title, quote=True)
132+
safe_admin_name = html.escape(admin_name, quote=True)
133+
safe_reason = html.escape(reason, quote=True) if reason else "Nessuno"
134+
ids_text = ", ".join(str(uid) for uid in banned_ids)
135+
text = f"<b>{safe_chat_title}:</b>\n"
136+
text += f"Utenti bannati: {ids_text}\n"
137+
text += f'Bannato da: <a href="tg://user?id={admin_id}">{safe_admin_name}</a> ({admin_id})\n'
138+
text += f"Gruppi: {success_count}\n"
139+
text += f"Motivo: {safe_reason}"
140+
return text
141+
142+
107143
# Unban
108144
UNBAN_USAGE = "Uso: /unban user_id|@username, o rispondi al messaggio con /unban"
109145

0 commit comments

Comments
 (0)