fix(telegram): invert the chat gate — mute the ineligible (default-allow)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Telegram intersects the chat default with each user's permissions, so a per-user grant can never exceed a deny-by-default group: the original default-deny + grant design could not let any user write (can_send=true was AND-ed with the denying default). Invert it — the chat allows sending by default and the bot MUTES an ineligible member (unregistered, admin-suspended, or chat_muted) and restores an eligible one it had muted, acting only when the current state differs (idempotent, no self-loop). The block/unblock/chat_muted/registration path already sets can_send to the eligibility, so it is unchanged.
This commit is contained in:
@@ -254,10 +254,12 @@ func (t *Bot) SetEligibilityResolver(resolve EligibilityResolver) {
|
||||
t.eligibility = resolve
|
||||
}
|
||||
|
||||
// handleChatMember grants write access to a user who joins the moderated chat when
|
||||
// they are registered and not blocked. A non-eligible joiner is left muted (the chat
|
||||
// defaults to no-send), and a resolve failure fails closed (also left muted). Other
|
||||
// status changes (leaves, restrictions, admin edits) are ignored.
|
||||
// handleChatMember keeps a chat member's write access in sync with their eligibility.
|
||||
// The chat allows sending by default, so the bot mutes an ineligible member (not
|
||||
// registered, or admin-suspended, or chat_muted) and restores an eligible one it had
|
||||
// muted; an eligible member that can already send is left untouched. It acts only when
|
||||
// the current state differs from the desired one, so it is idempotent and does not
|
||||
// re-act on its own change; a resolve failure makes no change.
|
||||
func (t *Bot) handleChatMember(ctx context.Context, cm *models.ChatMemberUpdated) {
|
||||
user := chatMemberUser(cm.NewChatMember)
|
||||
var uid int64
|
||||
@@ -289,13 +291,22 @@ func (t *Bot) handleChatMember(ctx context.Context, cm *models.ChatMemberUpdated
|
||||
if t.botID != 0 && cm.From.ID == t.botID {
|
||||
return
|
||||
}
|
||||
// Act only on a user who is in the chat (member or restricted) — a join into the
|
||||
// default-deny chat appears as `restricted`, not `member`. Left/kicked and admins
|
||||
// have nothing to grant here. An eligible user is (re)granted write; revoking a
|
||||
// now-ineligible user is the chat-gate path's job (an admin block or chat_muted).
|
||||
// The chat allows sending by default and the bot only restricts: Telegram intersects
|
||||
// the chat default with the per-user permission, so a per-user grant cannot exceed a
|
||||
// deny-by-default — the gate must mute the ineligible, not grant the eligible.
|
||||
// Determine whether the user is in the chat and can currently send: a plain member
|
||||
// follows the permissive default; a restricted member can send only with
|
||||
// CanSendMessages, and only while a member.
|
||||
var inChat, currentlyCanSend bool
|
||||
switch cm.NewChatMember.Type {
|
||||
case models.ChatMemberTypeMember, models.ChatMemberTypeRestricted:
|
||||
case models.ChatMemberTypeMember:
|
||||
inChat, currentlyCanSend = true, true
|
||||
case models.ChatMemberTypeRestricted:
|
||||
inChat, currentlyCanSend = isMember, canSend
|
||||
default:
|
||||
return // left / kicked / administrator / owner — not a member to gate
|
||||
}
|
||||
if !inChat {
|
||||
return
|
||||
}
|
||||
if t.eligibility == nil {
|
||||
@@ -307,15 +318,20 @@ func (t *Bot) handleChatMember(ctx context.Context, cm *models.ChatMemberUpdated
|
||||
t.log.Warn("chat access eligibility failed", zap.Int64("user_id", user.ID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
t.log.Info("chat access evaluated", zap.Int64("user_id", user.ID), zap.Bool("eligible", eligible))
|
||||
if !eligible {
|
||||
return // not registered or blocked: leave muted
|
||||
}
|
||||
if err := t.setChatWrite(ctx, user.ID, true); err != nil {
|
||||
t.log.Warn("grant chat write failed", zap.Int64("user_id", user.ID), zap.Error(err))
|
||||
t.log.Info("chat access evaluated",
|
||||
zap.Int64("user_id", user.ID), zap.Bool("eligible", eligible), zap.Bool("can_send", currentlyCanSend))
|
||||
// Desired: an eligible user may send, an ineligible one may not. Act only when the
|
||||
// current state differs — idempotent, a no-op for the common eligible member, and it
|
||||
// keeps the bot from re-acting on its own change.
|
||||
if eligible == currentlyCanSend {
|
||||
return
|
||||
}
|
||||
t.log.Info("chat write granted", zap.Int64("user_id", user.ID))
|
||||
if err := t.setChatWrite(ctx, user.ID, eligible); err != nil {
|
||||
t.log.Warn("set chat write failed",
|
||||
zap.Int64("user_id", user.ID), zap.Bool("can_send", eligible), zap.Error(err))
|
||||
return
|
||||
}
|
||||
t.log.Info("chat access applied", zap.Int64("user_id", user.ID), zap.Bool("can_send", eligible))
|
||||
}
|
||||
|
||||
// ApplyChatGate applies a chat-gate command (an admin block/unblock or chat_muted
|
||||
|
||||
Reference in New Issue
Block a user