diff --git a/src/bot/handlers/anti_spam.py b/src/bot/handlers/anti_spam.py index f675949..5b06eac 100644 --- a/src/bot/handlers/anti_spam.py +++ b/src/bot/handlers/anti_spam.py @@ -436,7 +436,7 @@ async def handle_new_user_spam( ) # 4. Threshold reached: restrict user and notify - if record.violation_count == group_config.new_user_violation_threshold: + if record.violation_count >= group_config.new_user_violation_threshold: try: await context.bot.restrict_chat_member( chat_id=group_config.group_id, @@ -466,3 +466,5 @@ async def handle_new_user_spam( f"Failed to restrict user: user_id={user.id}", exc_info=True, ) + + raise ApplicationHandlerStop diff --git a/src/bot/main.py b/src/bot/main.py index 54ba309..8e0cd64 100644 --- a/src/bot/main.py +++ b/src/bot/main.py @@ -277,32 +277,38 @@ def main() -> None: logger.info("Registered handler: dm_handler (group=0)") # Handler 8: Inline keyboard spam handler - catches messages with - # non-whitelisted URL buttons in inline keyboards (spam from bots/forwards) + # non-whitelisted URL buttons in inline keyboards (spam from bots/forwards). + # Each spam handler runs in its own group so they all independently process + # every group message. They raise ApplicationHandlerStop to prevent later + # groups from running when spam IS detected. application.add_handler( MessageHandler( filters.ChatType.GROUPS, handle_inline_keyboard_spam, - ) + ), + group=1, ) - logger.info("Registered handler: inline_keyboard_spam_handler (group=0)") + logger.info("Registered handler: inline_keyboard_spam_handler (group=1)") # Handler 9: New-user anti-spam handler - checks for forwards/links from users on probation application.add_handler( MessageHandler( filters.ChatType.GROUPS, handle_new_user_spam, - ) + ), + group=2, ) - logger.info("Registered handler: anti_spam_handler (group=0)") + logger.info("Registered handler: anti_spam_handler (group=2)") # Handler 10: Duplicate message spam handler - detects repeated identical messages application.add_handler( MessageHandler( filters.ChatType.GROUPS & ~filters.COMMAND, handle_duplicate_spam, - ) + ), + group=3, ) - logger.info("Registered handler: duplicate_spam_handler (group=0)") + logger.info("Registered handler: duplicate_spam_handler (group=3)") # Handler 11: Group message handler - monitors messages in monitored # groups and warns/restricts users with incomplete profiles @@ -311,9 +317,9 @@ def main() -> None: filters.ChatType.GROUPS & ~filters.COMMAND, handle_message, ), - group=1, + group=4, ) - logger.info("Registered handler: message_handler (group=1)") + logger.info("Registered handler: message_handler (group=4)") # Register auto-restriction job to run every 5 minutes if application.job_queue: diff --git a/tests/test_anti_spam.py b/tests/test_anti_spam.py index c8e72fe..836aaa8 100644 --- a/tests/test_anti_spam.py +++ b/tests/test_anti_spam.py @@ -7,6 +7,8 @@ from telegram import Chat, Message, MessageEntity, User from bot.group_config import GroupConfig +from telegram.ext import ApplicationHandlerStop + from bot.handlers.anti_spam import ( extract_urls, handle_inline_keyboard_spam, @@ -396,8 +398,9 @@ async def test_handles_naive_datetime_from_database( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - # Should not raise TypeError - await handle_new_user_spam(mock_update, mock_context) + # Should not raise TypeError, but raises ApplicationHandlerStop on violation + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -462,7 +465,8 @@ async def test_deletes_forwarded_message( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -492,7 +496,8 @@ async def test_deletes_message_with_non_whitelisted_link( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -543,7 +548,8 @@ async def test_sends_warning_on_first_violation( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_context.bot.send_message.assert_called_once() @@ -568,7 +574,8 @@ async def test_no_warning_on_second_violation( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_context.bot.send_message.assert_not_called() @@ -593,7 +600,8 @@ async def test_restricts_user_at_threshold( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_context.bot.restrict_chat_member.assert_called_once() @@ -618,7 +626,8 @@ async def test_sends_restriction_notification_at_threshold( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) # Should call send_message for restriction notification (not first warning) mock_context.bot.send_message.assert_called_once() @@ -645,7 +654,8 @@ async def test_deletes_message_with_external_reply( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -670,7 +680,8 @@ async def test_deletes_message_with_story( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -713,7 +724,8 @@ async def test_continues_when_delete_fails( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_context.bot.send_message.assert_called_once() @@ -741,7 +753,8 @@ async def test_continues_when_send_warning_fails( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once() @@ -769,7 +782,8 @@ async def test_continues_when_restrict_fails( patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config), patch("bot.handlers.anti_spam.get_database", return_value=mock_db), ): - await handle_new_user_spam(mock_update, mock_context) + with pytest.raises(ApplicationHandlerStop): + await handle_new_user_spam(mock_update, mock_context) mock_update.message.delete.assert_called_once()