Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/bot/handlers/anti_spam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -466,3 +466,5 @@ async def handle_new_user_spam(
f"Failed to restrict user: user_id={user.id}",
exc_info=True,
)

raise ApplicationHandlerStop
24 changes: 15 additions & 9 deletions src/bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
40 changes: 27 additions & 13 deletions tests/test_anti_spam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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()
Expand All @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down