Skip to content

Commit 9c95487

Browse files
committed
feat: add contact_spam_restrict per-group config option
- Add contact_spam_restrict field to GroupConfig (default: true) - Add .env fallback in config.py and build_group_registry - Skip restrict in handler when config is false - Add to groups.json.example and .env.example - Add test for disabled restrict config (534 total) - Update docs with new config option
1 parent 14208d2 commit 9c95487

8 files changed

Lines changed: 49 additions & 16 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ CAPTCHA_ENABLED=false
3636
# Default: 120 seconds (2 minutes)
3737
CAPTCHA_TIMEOUT_SECONDS=120
3838

39+
# Restrict users who share contact cards (true/false)
40+
# When true, users sharing contact cards will be restricted (muted)
41+
# When false, contact messages are deleted but user is not restricted
42+
CONTACT_SPAM_RESTRICT=true
43+
3944
# Enable duplicate message spam detection (true/false)
4045
# Detects and restricts users who paste the same message repeatedly
4146
DUPLICATE_SPAM_ENABLED=true

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Time threshold → Auto-restrict via scheduler (parallel path)
157157
- **Fixtures**: `mock_update`, `mock_context`, `mock_settings` — copy from existing tests
158158
- **Database tests**: Use `temp_db` fixture with `tempfile.TemporaryDirectory`
159159
- **Mocking**: `AsyncMock` for Telegram API; no real network calls
160-
- **Coverage**: 99.9% maintained (533 tests) — check before committing
160+
- **Coverage**: 99.9% maintained (534 tests) — check before committing
161161

162162
## Anti-Patterns (THIS PROJECT)
163163

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ uv run pytest -v
196196

197197
The project maintains comprehensive test coverage:
198198
- **Coverage**: 99.9% (1,570 statements, 1 unreachable line)
199-
- **Tests**: 533 total
200-
- **Pass Rate**: 100% (533/533 passed)
199+
- **Tests**: 534 total
200+
- **Pass Rate**: 100% (534/534 passed)
201201
- **All modules at 100%** except one unreachable line in `anti_spam.py`
202202
- Services: `bot_info.py`, `scheduler.py`, `user_checker.py`, `telegram_utils.py`, `captcha_recovery.py` — all 100%
203203
- Handlers: `anti_spam.py` (99%), `captcha.py`, `check.py`, `dm.py`, `message.py`, `topic_guard.py`, `verify.py`, `duplicate_spam.py` — all 100%
@@ -542,6 +542,7 @@ When a restricted user DMs the bot (or sends `/start`):
542542
| `CAPTCHA_TIMEOUT_SECONDS` | Seconds before kicking unverified members | `120` (2 minutes) |
543543
| `NEW_USER_PROBATION_HOURS` | Hours new users can't send links/forwards | `72` (3 days) |
544544
| `NEW_USER_VIOLATION_THRESHOLD` | Spam violations before restriction | `3` |
545+
| `CONTACT_SPAM_RESTRICT` | Restrict users who share contact cards | `true` |
545546
| `DATABASE_PATH` | SQLite database path | `data/bot.db` |
546547
| `RULES_LINK` | Link to group rules message | `https://t.me/pythonID/290029/321799` |
547548
| `LOGFIRE_ENABLED` | Enable Logfire logging integration | `true` |

groups.json.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"new_user_probation_hours": 72,
1111
"new_user_violation_threshold": 3,
1212
"rules_link": "https://t.me/pythonID/290029/321799",
13+
"contact_spam_restrict": true,
1314
"duplicate_spam_enabled": true,
1415
"duplicate_spam_window_seconds": 120,
1516
"duplicate_spam_threshold": 2,
@@ -27,6 +28,7 @@
2728
"new_user_probation_hours": 168,
2829
"new_user_violation_threshold": 2,
2930
"rules_link": "https://t.me/mygroup/rules",
31+
"contact_spam_restrict": true,
3032
"duplicate_spam_enabled": true,
3133
"duplicate_spam_window_seconds": 60,
3234
"duplicate_spam_threshold": 2,

src/bot/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Settings(BaseSettings):
7979
captcha_timeout_seconds: int = 120
8080
new_user_probation_hours: int = 72 # 3 days default
8181
new_user_violation_threshold: int = 3 # restrict after this many violations
82+
contact_spam_restrict: bool = True
8283
duplicate_spam_enabled: bool = True
8384
duplicate_spam_window_seconds: int = 120
8485
duplicate_spam_threshold: int = 2

src/bot/group_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class GroupConfig(BaseModel):
3535
new_user_probation_hours: int = 72
3636
new_user_violation_threshold: int = 3
3737
rules_link: str = "https://t.me/pythonID/290029/321799"
38+
contact_spam_restrict: bool = True
3839
duplicate_spam_enabled: bool = True
3940
duplicate_spam_window_seconds: int = 120
4041
duplicate_spam_threshold: int = 2
@@ -186,6 +187,7 @@ def build_group_registry(settings: object) -> GroupRegistry:
186187
new_user_probation_hours=settings.new_user_probation_hours,
187188
new_user_violation_threshold=settings.new_user_violation_threshold,
188189
rules_link=settings.rules_link,
190+
contact_spam_restrict=settings.contact_spam_restrict,
189191
duplicate_spam_enabled=settings.duplicate_spam_enabled,
190192
duplicate_spam_window_seconds=settings.duplicate_spam_window_seconds,
191193
duplicate_spam_threshold=settings.duplicate_spam_threshold,

src/bot/handlers/anti_spam.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,20 @@ async def handle_contact_spam(
304304
)
305305

306306
restricted = False
307-
try:
308-
await context.bot.restrict_chat_member(
309-
chat_id=group_config.group_id,
310-
user_id=user.id,
311-
permissions=RESTRICTED_PERMISSIONS,
312-
)
313-
restricted = True
314-
logger.info(f"Restricted user_id={user.id} for contact spam")
315-
except Exception:
316-
logger.error(
317-
f"Failed to restrict user for contact spam: user_id={user.id}",
318-
exc_info=True,
319-
)
307+
if group_config.contact_spam_restrict:
308+
try:
309+
await context.bot.restrict_chat_member(
310+
chat_id=group_config.group_id,
311+
user_id=user.id,
312+
permissions=RESTRICTED_PERMISSIONS,
313+
)
314+
restricted = True
315+
logger.info(f"Restricted user_id={user.id} for contact spam")
316+
except Exception:
317+
logger.error(
318+
f"Failed to restrict user for contact spam: user_id={user.id}",
319+
exc_info=True,
320+
)
320321

321322
try:
322323
template = (

tests/test_anti_spam.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,3 +1356,24 @@ async def test_raises_application_handler_stop(self, mock_update, mock_context,
13561356
with patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config):
13571357
with pytest.raises(ApplicationHandlerStop):
13581358
await handle_contact_spam(mock_update, mock_context)
1359+
1360+
async def test_contact_spam_no_restrict_when_config_disabled(
1361+
self, mock_update, mock_context
1362+
):
1363+
"""Test that user is NOT restricted when contact_spam_restrict is False."""
1364+
group_config = GroupConfig(
1365+
group_id=-100123456,
1366+
warning_topic_id=123,
1367+
rules_link="https://example.com/rules",
1368+
contact_spam_restrict=False,
1369+
)
1370+
1371+
with patch("bot.handlers.anti_spam.get_group_config_for_update", return_value=group_config):
1372+
with pytest.raises(ApplicationHandlerStop):
1373+
await handle_contact_spam(mock_update, mock_context)
1374+
1375+
mock_update.message.delete.assert_called_once()
1376+
mock_context.bot.restrict_chat_member.assert_not_called()
1377+
mock_context.bot.send_message.assert_called_once()
1378+
call_args = mock_context.bot.send_message.call_args
1379+
assert "dibatasi" not in call_args.kwargs.get("text", call_args[1].get("text", ""))

0 commit comments

Comments
 (0)