You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Critical fixes from 4-agent parallel review of feat/guest-bot-block:
- guest_bot.py: revert threshold check to < (not !=) and drop the
increment-on-failure. != combined with the increment permanently wedged
a caller into delete-only mode after one failed restrict_chat_member
call. < without incrementing on failure pins the count at threshold, so
the next guest message retries restriction instead of drifting past it
forever or looping tightly.
- captcha_recovery.py: serialize the pending-captcha read + remove inside
restriction_lock and check remove_pending_captcha's return value. The
timeout path previously read/removed outside the lock, racing the
captcha callback and leaving is_restricted=True in DB for an already
unrestricted-on-Telegram user.
- message.py, scheduler.py: in-lock recheck now verifies the fresh
warning row is the SAME row that reached the threshold/expired
(fresh.id != record.id), not just that some active row exists. Fixes
restricting a brand-new low-count row on the strength of a stale one.
- restriction_lock.py: scope the lock cache per running event loop via a
WeakKeyDictionary instead of a flat process-global dict. asyncio.Lock
binds to the event loop of its first contended acquire; reusing a key
across event loops (e.g. per-test loops) previously raised RuntimeError.
- verify.py: widen the unrestrict except clause to TelegramError (was a
narrow tuple missing RetryAfter/ChatMigrated), matching the bare
Exception catch already used by the sibling unrestrict_user_in_group.
- guest_bot.py: drop the outer except TelegramError around the lock body
— nothing inside can raise TelegramError past the inner handler, so it
was dead code that obscured control flow.
- tests/test_scheduler.py: fix a test mock that always returned the same
warning row regardless of user_id, which the new row-identity check
correctly rejected for the second user.
- tests/test_restriction_lock.py: update for the loop-scoped lock storage.
- AGENTS.md, README.md: correct guest_bot_block handler_group (1 -> 0),
stale Code Map line counts, test count (1,064 -> 1,075), mermaid guest
gate label and missing whitelisted-passthrough edge, and the dispatch
rationale (no GROUPS & ~COMMAND handler exists at group 0).
All 1075 tests passing, ruff/mypy clean.
@@ -179,12 +179,13 @@ group=6 # JobQueue only (not a handler group): auto_restrict_job, refresh_admi
179
179
180
180
### Guest Bot Moderation
181
181
-`handlers/guest_bot.py` blocks Telegram **Guest Mode** messages — messages posted by a bot on behalf of a user/channel via the `@` mention feature that Telegram routes through `message.guest_bot_caller_user` / `message.guest_bot_caller_chat` (PTB v22.8+)
182
-
- A custom `GuestBotFilter` (`filters.MessageFilter`) matches only messages where either guest-bot caller field is set, so the handler is dispatched before the broad `GROUPS & ~COMMAND` filters at the same group
183
-
- Registered as `guest_bot_block` at `handler_group=1` via `spam_mod.register_guest_bot_block` in `plugins/builtin/spam.py`, gated by `guard_plugin("guest_bot_block")`
182
+
- A custom `GuestBotFilter` (`filters.MessageFilter`) matches only messages where either guest-bot caller field is set, so the handler only fires on actual Guest Mode updates and raises `ApplicationHandlerStop` to stop the spam handlers at groups 1-5 from also processing the message
183
+
- Registered as `guest_bot_block` at `handler_group=0` (alongside commands/callbacks/captcha/dm) via `spam_mod.register_guest_bot_block` in `plugins/builtin/spam.py`, gated by `guard_plugin("guest_bot_block")`
184
184
- Non-whitelisted guest bot messages are always deleted; the invoking **human caller** then receives progressive enforcement using the group's existing `warning_threshold`:
185
185
- 1st violation → warning in the warning topic (`GUEST_BOT_WARNING`)
- If `restrict_chat_member` fails (e.g. bot lacks ban rights), the strike count is NOT incremented past the threshold — the next guest message from the same caller retries the restriction instead of drifting into a permanent delete-only state
188
189
- Admin/trusted callers have their guest message deleted but are **not** warned or restricted
189
190
- Chat/channel-only callers (no `guest_bot_caller_user`) are delete-only — there is no human to warn
190
191
- Already guest-bot-restricted callers do not start a fresh warning cycle (`is_user_restricted_by_bot` check)
@@ -331,7 +332,7 @@ if user.id not in admin_ids:
331
332
-**Trust feature**: `TrustedUser` table caches user_full_name + admin_full_name at trust time so `/trusted` lists admin info without Telegram API calls. Backfill script at `scripts/backfill_trusted_names.py` for pre-existing rows
332
333
-**Local review artifacts**: `reviews/` directory contains output from parallel reviewer subagents. Gitignored; not part of the source tree
333
334
-**Captcha DB ordering**: The captcha callback handler calls Telegram `unrestrict_user` BEFORE DB writes (remove_pending_captcha, start_new_user_probation). If unrestrict fails, the pending captcha stays in DB and the user can retry. DB finalization is idempotent — `remove_pending_captcha` returning False means a concurrent callback already finalized
334
-
-**Guest bot blocking**: Telegram Guest Mode lets any user `@mention` a bot and have the result posted in a chat. The `guest_bot_block` plugin (group=1) deletes non-whitelisted guest bot messages and progressively restricts the human caller (1st=warning, Nth=restrict). Admins/trusted users and channel-only callers are delete-only. Bot whitelist is case-insensitive with optional `@`. Guest strikes are tracked separately via `warning_kind="guest_bot"` and are not eligible for the DM self-service unrestriction flow
335
+
-**Guest bot blocking**: Telegram Guest Mode lets any user `@mention` a bot and have the result posted in a chat. The `guest_bot_block` plugin (group=0) deletes non-whitelisted guest bot messages and progressively restricts the human caller (1st=warning, Nth=restrict). Admins/trusted users and channel-only callers are delete-only. Bot whitelist is case-insensitive with optional `@`. A failed restriction does not increment the strike count past the threshold, so the caller's next guest message retries the restriction. Guest strikes are tracked separately via `warning_kind="guest_bot"` and are not eligible for the DM self-service unrestriction flow
-**Tests**: 1,064 total (includes 19 Hypothesis property tests)
215
+
-**Tests**: 1,075 total (includes 19 Hypothesis property tests)
216
216
-**Pass Rate**: 100%
217
217
-**Property tests**: `tests/test_properties.py` exercises pure functions (format helpers, URL whitelist, name formatters) with random inputs and shrinks failing cases to minimal examples
218
218
-**Mypy**: Pragmatic config in `pyproject.toml`. Disables error codes that are noisy from PTB / SQLModel / Pydantic v2; catches real type bugs in new code
0 commit comments