Skip to content

fix: separate /unlock from /unmute and re-trigger captcha on rejoin#53

Open
MattiaFailla wants to merge 1 commit into
masterfrom
fix/stale-users-unlock
Open

fix: separate /unlock from /unmute and re-trigger captcha on rejoin#53
MattiaFailla wants to merge 1 commit into
masterfrom
fix/stale-users-unlock

Conversation

@MattiaFailla

@MattiaFailla MattiaFailla commented Jun 17, 2026

Copy link
Copy Markdown
Member

Why

Investigating #52 (keobox:fix_stale_users). The reported symptom: users stuck muted forever in group is real, but the proposed fix conflates concerns and includes inert/risky permission edits. This PR fixes the actual root causes and keeps moderation (/mute) and captcha (/unlock) cleanly separated.

Root cause (two distinct bugs)

Bug A — silent re-mute on rejoin. In welcome.py:_handle_new_member, a non-verified rejoiner is re-restricted read-only and re-added to pending_verifications, then the function returns early at the has_been_welcomed() guard before sending the captcha. There was no LEFT/BANNED handler to clear the per-chat welcome state, so the user is muted with no way to verify.

Bug B — /unmute clears the wrong table. Stale users were restricted by the captcha flow (pending_verifications), not by /mute. /unmute only DELETEs from mutes, so it never cleared their captcha "exception".

Changes

  • _handle_new_member: on LEFT/BANNED, clear welcomed + pending for (user, chat) so a genuine rejoin re-shows the captcha. Global verification is intentionally preserved (verified users stay verified).
  • /unlock @handle (new admin command): globally verifies a stuck user, clears pending state, and restores send permissions in their pending chats. Distinct from /unmute, which stays a pure moderation-mute reversal.
  • Repository: remove_welcomed (in-memory + postgres) + CaptchaService wrappers (remove_welcomed, remove_pending).

Why not flip can_change_info / can_pin_messages to True (as in #52)

Per the official Telegram Bot API and python-telegram-bot docs, both fields are "Ignored in public supergroups" — so in @PythonMilano (public) the flip is a no-op, and in any private supergroup it would be a latent privilege escalation (every verified/unmuted user gains pin + edit-group-info rights above the False group default). It is also causally unrelated to the mute, which is can_send_messages=False; those two fields aren't send permissions and aren't in any permission cascade. This PR keeps them False.

Tests

  • tests/test_unlock.py: admin / non-admin rejection / private-chat ignore / permission restoration (asserts can_send_messages=True, can_change_info=False, can_pin_messages=False).
  • tests/test_welcome_rejoin.py: remove_welcomed repo behavior + departure (LEFT, BANNED) clears welcome/pending + global verification preserved.

All 27 tests pass; ruff check, ruff format, and mypy clean on changed files.

Relation to #52

Supersedes #52's approach. Keeps the one load-bearing idea (clearing the captcha exception when an admin frees a user) but moves it to a dedicated /unlock command, drops the inert permission flip, and additionally fixes the root rejoin bug so new stale users stop accumulating.

… on rejoin

Stale users sat muted forever in public supergroups for two reasons:

1. On rejoin, _handle_new_member re-restricted the user and re-added the
   pending row, then returned early at the has_been_welcomed() guard before
   sending the captcha. The user was silently muted with no way to verify,
   because no LEFT/KICKED handler ever cleared the per-chat welcome state.

2. /unmute only deletes from the mutes table, but stale users were restricted
   by the captcha flow (pending_verifications), so /unmute never cleared their
   "exception".

Changes:
- Add a departure branch to _handle_new_member: on LEFT/BANNED, clear the
  welcomed + pending state for that (user, chat) so a genuine rejoin re-shows
  the captcha. Global verification is intentionally preserved.
- Add /unlock @handle: an admin command that globally verifies a stuck user,
  clears pending state, and restores send permissions in their pending chats.
  Kept distinct from /unmute, which stays a pure moderation-mute reversal.
- Add Repository.remove_welcomed (in-memory + postgres) and CaptchaService
  wrappers (remove_welcomed, remove_pending).

Deliberately did NOT flip can_change_info / can_pin_messages to True (as
proposed in #52). Per the Telegram Bot API docs those fields are "Ignored in
public supergroups", so the flip is a no-op there and a latent privilege
escalation in any private supergroup, and it is unrelated to the mute itself
(which is can_send_messages=False).

Tests: /unlock (admin / non-admin / private chat / restored permissions) and
rejoin state-clearing (LEFT, BANNED, global-verification preserved).
@MattiaFailla MattiaFailla force-pushed the fix/stale-users-unlock branch from ae1506f to 497ac18 Compare June 17, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant