Skip to content

Commit c06342f

Browse files
committed
fix: move spam handlers to separate groups to fix anti-spam regression
In PTB v20+, only the first matching handler per group runs. handle_inline_keyboard_spam (group=0) consumed all group messages, preventing handle_new_user_spam and handle_duplicate_spam from ever executing — introduced in 02d0967. Changes: - Move each spam handler to its own group (1, 2, 3) so they all independently process every group message - Move handle_message (profile check) to group=4 - Add ApplicationHandlerStop to handle_new_user_spam on violations to prevent later groups from processing deleted spam - Use >= instead of == for violation threshold to handle edge cases with concurrent updates or DB race conditions
1 parent 7b920c9 commit c06342f

3 files changed

Lines changed: 45 additions & 23 deletions

File tree

src/bot/handlers/anti_spam.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ async def handle_new_user_spam(
436436
)
437437

438438
# 4. Threshold reached: restrict user and notify
439-
if record.violation_count == group_config.new_user_violation_threshold:
439+
if record.violation_count >= group_config.new_user_violation_threshold:
440440
try:
441441
await context.bot.restrict_chat_member(
442442
chat_id=group_config.group_id,
@@ -466,3 +466,5 @@ async def handle_new_user_spam(
466466
f"Failed to restrict user: user_id={user.id}",
467467
exc_info=True,
468468
)
469+
470+
raise ApplicationHandlerStop

src/bot/main.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,32 +277,38 @@ def main() -> None:
277277
logger.info("Registered handler: dm_handler (group=0)")
278278

279279
# Handler 8: Inline keyboard spam handler - catches messages with
280-
# non-whitelisted URL buttons in inline keyboards (spam from bots/forwards)
280+
# non-whitelisted URL buttons in inline keyboards (spam from bots/forwards).
281+
# Each spam handler runs in its own group so they all independently process
282+
# every group message. They raise ApplicationHandlerStop to prevent later
283+
# groups from running when spam IS detected.
281284
application.add_handler(
282285
MessageHandler(
283286
filters.ChatType.GROUPS,
284287
handle_inline_keyboard_spam,
285-
)
288+
),
289+
group=1,
286290
)
287-
logger.info("Registered handler: inline_keyboard_spam_handler (group=0)")
291+
logger.info("Registered handler: inline_keyboard_spam_handler (group=1)")
288292

289293
# Handler 9: New-user anti-spam handler - checks for forwards/links from users on probation
290294
application.add_handler(
291295
MessageHandler(
292296
filters.ChatType.GROUPS,
293297
handle_new_user_spam,
294-
)
298+
),
299+
group=2,
295300
)
296-
logger.info("Registered handler: anti_spam_handler (group=0)")
301+
logger.info("Registered handler: anti_spam_handler (group=2)")
297302

298303
# Handler 10: Duplicate message spam handler - detects repeated identical messages
299304
application.add_handler(
300305
MessageHandler(
301306
filters.ChatType.GROUPS & ~filters.COMMAND,
302307
handle_duplicate_spam,
303-
)
308+
),
309+
group=3,
304310
)
305-
logger.info("Registered handler: duplicate_spam_handler (group=0)")
311+
logger.info("Registered handler: duplicate_spam_handler (group=3)")
306312

307313
# Handler 11: Group message handler - monitors messages in monitored
308314
# groups and warns/restricts users with incomplete profiles
@@ -311,9 +317,9 @@ def main() -> None:
311317
filters.ChatType.GROUPS & ~filters.COMMAND,
312318
handle_message,
313319
),
314-
group=1,
320+
group=4,
315321
)
316-
logger.info("Registered handler: message_handler (group=1)")
322+
logger.info("Registered handler: message_handler (group=4)")
317323

318324
# Register auto-restriction job to run every 5 minutes
319325
if application.job_queue:

tests/test_anti_spam.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from telegram import Chat, Message, MessageEntity, User
88

99
from bot.group_config import GroupConfig
10+
from telegram.ext import ApplicationHandlerStop
11+
1012
from bot.handlers.anti_spam import (
1113
extract_urls,
1214
handle_inline_keyboard_spam,
@@ -396,8 +398,9 @@ async def test_handles_naive_datetime_from_database(
396398
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
397399
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
398400
):
399-
# Should not raise TypeError
400-
await handle_new_user_spam(mock_update, mock_context)
401+
# Should not raise TypeError, but raises ApplicationHandlerStop on violation
402+
with pytest.raises(ApplicationHandlerStop):
403+
await handle_new_user_spam(mock_update, mock_context)
401404

402405
mock_update.message.delete.assert_called_once()
403406

@@ -462,7 +465,8 @@ async def test_deletes_forwarded_message(
462465
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
463466
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
464467
):
465-
await handle_new_user_spam(mock_update, mock_context)
468+
with pytest.raises(ApplicationHandlerStop):
469+
await handle_new_user_spam(mock_update, mock_context)
466470

467471
mock_update.message.delete.assert_called_once()
468472

@@ -492,7 +496,8 @@ async def test_deletes_message_with_non_whitelisted_link(
492496
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
493497
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
494498
):
495-
await handle_new_user_spam(mock_update, mock_context)
499+
with pytest.raises(ApplicationHandlerStop):
500+
await handle_new_user_spam(mock_update, mock_context)
496501

497502
mock_update.message.delete.assert_called_once()
498503

@@ -543,7 +548,8 @@ async def test_sends_warning_on_first_violation(
543548
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
544549
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
545550
):
546-
await handle_new_user_spam(mock_update, mock_context)
551+
with pytest.raises(ApplicationHandlerStop):
552+
await handle_new_user_spam(mock_update, mock_context)
547553

548554
mock_context.bot.send_message.assert_called_once()
549555

@@ -568,7 +574,8 @@ async def test_no_warning_on_second_violation(
568574
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
569575
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
570576
):
571-
await handle_new_user_spam(mock_update, mock_context)
577+
with pytest.raises(ApplicationHandlerStop):
578+
await handle_new_user_spam(mock_update, mock_context)
572579

573580
mock_context.bot.send_message.assert_not_called()
574581

@@ -593,7 +600,8 @@ async def test_restricts_user_at_threshold(
593600
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
594601
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
595602
):
596-
await handle_new_user_spam(mock_update, mock_context)
603+
with pytest.raises(ApplicationHandlerStop):
604+
await handle_new_user_spam(mock_update, mock_context)
597605

598606
mock_context.bot.restrict_chat_member.assert_called_once()
599607

@@ -618,7 +626,8 @@ async def test_sends_restriction_notification_at_threshold(
618626
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
619627
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
620628
):
621-
await handle_new_user_spam(mock_update, mock_context)
629+
with pytest.raises(ApplicationHandlerStop):
630+
await handle_new_user_spam(mock_update, mock_context)
622631

623632
# Should call send_message for restriction notification (not first warning)
624633
mock_context.bot.send_message.assert_called_once()
@@ -645,7 +654,8 @@ async def test_deletes_message_with_external_reply(
645654
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
646655
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
647656
):
648-
await handle_new_user_spam(mock_update, mock_context)
657+
with pytest.raises(ApplicationHandlerStop):
658+
await handle_new_user_spam(mock_update, mock_context)
649659

650660
mock_update.message.delete.assert_called_once()
651661

@@ -670,7 +680,8 @@ async def test_deletes_message_with_story(
670680
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
671681
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
672682
):
673-
await handle_new_user_spam(mock_update, mock_context)
683+
with pytest.raises(ApplicationHandlerStop):
684+
await handle_new_user_spam(mock_update, mock_context)
674685

675686
mock_update.message.delete.assert_called_once()
676687

@@ -713,7 +724,8 @@ async def test_continues_when_delete_fails(
713724
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
714725
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
715726
):
716-
await handle_new_user_spam(mock_update, mock_context)
727+
with pytest.raises(ApplicationHandlerStop):
728+
await handle_new_user_spam(mock_update, mock_context)
717729

718730
mock_context.bot.send_message.assert_called_once()
719731

@@ -741,7 +753,8 @@ async def test_continues_when_send_warning_fails(
741753
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
742754
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
743755
):
744-
await handle_new_user_spam(mock_update, mock_context)
756+
with pytest.raises(ApplicationHandlerStop):
757+
await handle_new_user_spam(mock_update, mock_context)
745758

746759
mock_update.message.delete.assert_called_once()
747760

@@ -769,7 +782,8 @@ async def test_continues_when_restrict_fails(
769782
patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config),
770783
patch("bot.handlers.anti_spam.get_database", return_value=mock_db),
771784
):
772-
await handle_new_user_spam(mock_update, mock_context)
785+
with pytest.raises(ApplicationHandlerStop):
786+
await handle_new_user_spam(mock_update, mock_context)
773787

774788
mock_update.message.delete.assert_called_once()
775789

0 commit comments

Comments
 (0)