Skip to content

Commit 0084cf5

Browse files
rezhajulioqwencoder
andcommitted
feat: add inline keyboard spam detection
- Detect and remove messages with suspicious inline keyboard URLs - Add has_non_whitelisted_inline_keyboard_urls() to check url, login_url, and web_app buttons - Add handle_inline_keyboard_spam() handler to delete spam and restrict users - Add notification templates for inline keyboard spam (with/without restriction) - Integrate inline keyboard check into new user probation handler - Add comprehensive tests for detection and handler logic Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent 21cfeeb commit 0084cf5

4 files changed

Lines changed: 544 additions & 6 deletions

File tree

src/bot/constants.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ def format_hours_display(hours: int) -> str:
195195
"📖 [Baca aturan grup]({rules_link})"
196196
)
197197

198+
# Inline keyboard spam notification
199+
INLINE_KEYBOARD_SPAM_NOTIFICATION = (
200+
"🚫 *Spam Terdeteksi*\n\n"
201+
"Pesan dari {user_mention} telah dihapus karena mengandung "
202+
"tombol inline keyboard dengan tautan mencurigakan.\n\n"
203+
"Pengguna telah dibatasi.\n\n"
204+
"📌 [Peraturan Grup]({rules_link})"
205+
)
206+
207+
INLINE_KEYBOARD_SPAM_NOTIFICATION_NO_RESTRICT = (
208+
"🚫 *Spam Terdeteksi*\n\n"
209+
"Pesan dari {user_mention} telah dihapus karena mengandung "
210+
"tombol inline keyboard dengan tautan mencurigakan.\n\n"
211+
"📌 [Peraturan Grup]({rules_link})"
212+
)
213+
198214
# Whitelisted URL domains for new user probation
199215
# These domains are allowed even during probation period
200216
# Matches exact domain or subdomains (e.g., "github.com" matches "www.github.com")

src/bot/handlers/anti_spam.py

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
from urllib.parse import urlparse
1313

1414
from telegram import Message, MessageEntity, Update
15-
from telegram.ext import ContextTypes
15+
from telegram.ext import ApplicationHandlerStop, ContextTypes
1616

1717
from bot.constants import (
18+
INLINE_KEYBOARD_SPAM_NOTIFICATION,
19+
INLINE_KEYBOARD_SPAM_NOTIFICATION_NO_RESTRICT,
1820
NEW_USER_SPAM_RESTRICTION,
1921
NEW_USER_SPAM_WARNING,
2022
RESTRICTED_PERMISSIONS,
@@ -194,6 +196,139 @@ def has_non_whitelisted_link(message: Message) -> bool:
194196
return False
195197

196198

199+
def has_non_whitelisted_inline_keyboard_urls(message: Message) -> bool:
200+
"""
201+
Check if a message contains inline keyboard buttons with non-whitelisted URLs.
202+
203+
Regular Telegram users cannot create inline keyboards from the client.
204+
Messages with inline keyboard URL buttons pointing to non-whitelisted
205+
domains are considered spam. Checks url, login_url, and web_app button types.
206+
207+
Args:
208+
message: Telegram message to check.
209+
210+
Returns:
211+
bool: True if any inline keyboard button has a non-whitelisted URL.
212+
"""
213+
rm = getattr(message, "reply_markup", None)
214+
keyboard = getattr(rm, "inline_keyboard", None)
215+
if not keyboard:
216+
return False
217+
218+
for row in keyboard:
219+
if not row:
220+
continue
221+
for button in row:
222+
if not button:
223+
continue
224+
225+
candidates: list[str] = []
226+
if getattr(button, "url", None):
227+
candidates.append(button.url)
228+
229+
login_url = getattr(button, "login_url", None)
230+
if getattr(login_url, "url", None):
231+
candidates.append(login_url.url)
232+
233+
web_app = getattr(button, "web_app", None)
234+
if getattr(web_app, "url", None):
235+
candidates.append(web_app.url)
236+
237+
for u in candidates:
238+
if not is_url_whitelisted(u):
239+
return True
240+
241+
return False
242+
243+
244+
async def handle_inline_keyboard_spam(
245+
update: Update, context: ContextTypes.DEFAULT_TYPE
246+
) -> None:
247+
"""
248+
Handle spam messages containing inline keyboard buttons with non-whitelisted URLs.
249+
250+
Regular users cannot create inline keyboards from the Telegram client.
251+
Any group message with inline keyboard URL buttons pointing to
252+
non-whitelisted domains is treated as spam.
253+
254+
Args:
255+
update: Telegram update containing the message.
256+
context: Bot context with helper methods.
257+
"""
258+
if not update.message or not update.message.from_user:
259+
return
260+
261+
group_config = get_group_config_for_update(update)
262+
if group_config is None:
263+
return
264+
265+
user = update.message.from_user
266+
if user.is_bot:
267+
return
268+
269+
admin_ids = context.bot_data.get("group_admin_ids", {}).get(group_config.group_id, [])
270+
if user.id in admin_ids:
271+
return
272+
273+
msg = update.message
274+
if not has_non_whitelisted_inline_keyboard_urls(msg):
275+
return
276+
277+
user_mention = get_user_mention(user)
278+
logger.info(
279+
f"Inline keyboard spam detected: user_id={user.id}, "
280+
f"group_id={group_config.group_id}"
281+
)
282+
283+
try:
284+
await msg.delete()
285+
logger.info(f"Deleted inline keyboard spam from user_id={user.id}")
286+
except Exception:
287+
logger.error(
288+
f"Failed to delete inline keyboard spam: user_id={user.id}",
289+
exc_info=True,
290+
)
291+
292+
restricted = False
293+
try:
294+
await context.bot.restrict_chat_member(
295+
chat_id=group_config.group_id,
296+
user_id=user.id,
297+
permissions=RESTRICTED_PERMISSIONS,
298+
)
299+
restricted = True
300+
logger.info(f"Restricted user_id={user.id} for inline keyboard spam")
301+
except Exception:
302+
logger.error(
303+
f"Failed to restrict user for inline keyboard spam: user_id={user.id}",
304+
exc_info=True,
305+
)
306+
307+
try:
308+
template = (
309+
INLINE_KEYBOARD_SPAM_NOTIFICATION if restricted
310+
else INLINE_KEYBOARD_SPAM_NOTIFICATION_NO_RESTRICT
311+
)
312+
notification_text = template.format(
313+
user_mention=user_mention,
314+
rules_link=group_config.rules_link,
315+
)
316+
await context.bot.send_message(
317+
chat_id=group_config.group_id,
318+
message_thread_id=group_config.warning_topic_id,
319+
text=notification_text,
320+
parse_mode="Markdown",
321+
)
322+
logger.info(f"Sent inline keyboard spam notification for user_id={user.id}")
323+
except Exception:
324+
logger.error(
325+
f"Failed to send inline keyboard spam notification: user_id={user.id}",
326+
exc_info=True,
327+
)
328+
329+
raise ApplicationHandlerStop
330+
331+
197332
async def handle_new_user_spam(
198333
update: Update, context: ContextTypes.DEFAULT_TYPE
199334
) -> None:
@@ -249,13 +384,20 @@ async def handle_new_user_spam(
249384
user_mention = get_user_mention(user)
250385

251386
# Check for violations (forwarded message or non-whitelisted link or external reply)
252-
if not (is_forwarded(msg) or has_non_whitelisted_link(msg) or has_external_reply(msg) or has_story(msg)):
387+
if not (
388+
is_forwarded(msg)
389+
or has_non_whitelisted_link(msg)
390+
or has_external_reply(msg)
391+
or has_story(msg)
392+
or has_non_whitelisted_inline_keyboard_urls(msg)
393+
):
253394
return # Not a violation
254395

255396
logger.info(
256397
f"Probation violation detected: user_id={user.id}, "
257398
f"forwarded={is_forwarded(msg)}, has_non_whitelisted_link={has_non_whitelisted_link(msg)}, "
258-
f"external_reply={has_external_reply(msg)}, has_story={has_story(msg)}"
399+
f"external_reply={has_external_reply(msg)}, has_story={has_story(msg)}, "
400+
f"inline_keyboard_spam={has_non_whitelisted_inline_keyboard_urls(msg)}"
259401
)
260402

261403
# 1. Delete the violating message

src/bot/main.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from bot.database.service import init_database
1919
from bot.group_config import get_group_registry, init_group_registry
2020
from bot.handlers import captcha
21-
from bot.handlers.anti_spam import handle_new_user_spam
21+
from bot.handlers.anti_spam import handle_inline_keyboard_spam, handle_new_user_spam
2222
from bot.handlers.dm import handle_dm
2323
from bot.handlers.message import handle_message
2424
from bot.handlers.topic_guard import guard_warning_topic
@@ -275,7 +275,17 @@ def main() -> None:
275275
)
276276
logger.info("Registered handler: dm_handler (group=0)")
277277

278-
# Handler 8: New-user anti-spam handler - checks for forwards/links from users on probation
278+
# Handler 8: Inline keyboard spam handler - catches messages with
279+
# non-whitelisted URL buttons in inline keyboards (spam from bots/forwards)
280+
application.add_handler(
281+
MessageHandler(
282+
filters.ChatType.GROUPS,
283+
handle_inline_keyboard_spam,
284+
)
285+
)
286+
logger.info("Registered handler: inline_keyboard_spam_handler (group=0)")
287+
288+
# Handler 9: New-user anti-spam handler - checks for forwards/links from users on probation
279289
application.add_handler(
280290
MessageHandler(
281291
filters.ChatType.GROUPS,
@@ -284,7 +294,7 @@ def main() -> None:
284294
)
285295
logger.info("Registered handler: anti_spam_handler (group=0)")
286296

287-
# Handler 9: Group message handler - monitors messages in monitored
297+
# Handler 10: Group message handler - monitors messages in monitored
288298
# groups and warns/restricts users with incomplete profiles
289299
application.add_handler(
290300
MessageHandler(

0 commit comments

Comments
 (0)