channels: give kicked/banned/promoted/transferred users a real qts so their client applies it

updateChannelParticipant carries the account's qts per the MTProto spec, but
the server always sent Qts: 0, so real clients silently discarded it as a
stale duplicate -- the banned/kicked user's channel never vanished locally
and no correct "removed by admin" message showed, even though the update was
delivered successfully at the transport layer.

Add a durable per-device qts queue (channel_participant_event_queue) sharing
its qts number space with the existing secret-chat queue (one qts sequence
per device, per spec), and use it to stamp a correct, monotonically
increasing qts on the update for every device of the affected user -- for
channel bans/kicks, admin promotion/demotion, and ownership transfer. A
device offline when it happened can now recover the event via
updates.getDifference instead of missing it permanently.
This commit is contained in:
Astra 2026-09-15 15:43:52 +01:00
parent 97711c9d2e
commit 206bde18e0
16 changed files with 482 additions and 42 deletions

View file

@ -3,6 +3,7 @@ package rpc
import (
"context"
"fmt"
"sort"
"github.com/iamxvbaba/td/proto"
"github.com/iamxvbaba/td/tg"
@ -103,6 +104,10 @@ func (r *Router) onMessagesReceivedQueue(ctx context.Context, maxQts int) ([]int
if err := r.deps.SecretChats.AckQueue(ctx, deviceKey, maxQts); err != nil {
return nil, internalErr()
}
// Best-effort GC mark for the sibling channel-participant-event queue,
// which shares this device's qts sequence; a failure here doesn't affect
// correctness (only retention), so it isn't fatal to the RPC.
_ = r.deps.SecretChats.AckChannelParticipantEvents(ctx, deviceKey, maxQts)
return []int64{}, nil
}
@ -143,38 +148,84 @@ func (r *Router) deviceEncryptedQts(ctx context.Context) int {
return qts
}
// encryptedDifference 返回当前设备 qts > sinceQts 的连续前缀、推进后的 qts 与是否还有
// 下一页。存储错误或 qts gap 必须 fail-fast禁止越过缺口推进客户端水位。
func (r *Router) encryptedDifference(ctx context.Context, sinceQts int) ([]tg.EncryptedMessageClass, int, bool, error) {
// encryptedDifference 返回当前设备 qts > sinceQts 的连续前缀(加密消息 +
// channel 成员关系自通知,二者共用同一设备 qts 序列,按 qts 归并后统一做缺口
// 检查)、推进后的 qts 与是否还有下一页。存储错误或 qts gap 必须 fail-fast
// 禁止越过缺口推进客户端水位。
func (r *Router) encryptedDifference(ctx context.Context, sinceQts int) ([]tg.EncryptedMessageClass, []tg.UpdateClass, []int64, []int64, int, bool, error) {
if r.deps.SecretChats == nil {
return nil, sinceQts, false, nil
return nil, nil, nil, nil, sinceQts, false, nil
}
deviceKey, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, sinceQts, false, nil
return nil, nil, nil, nil, sinceQts, false, nil
}
msgs, err := r.deps.SecretChats.ListNewMessages(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
if err != nil {
return nil, sinceQts, false, err
return nil, nil, nil, nil, sinceQts, false, err
}
if len(msgs) == 0 {
return nil, sinceQts, false, nil
participantEvents, err := r.deps.SecretChats.ListChannelParticipantEventsSince(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
if err != nil {
return nil, nil, nil, nil, sinceQts, false, err
}
partial := len(msgs) > encryptedDifferencePageSize
if partial {
msgs = msgs[:encryptedDifferencePageSize]
if len(msgs) == 0 && len(participantEvents) == 0 {
return nil, nil, nil, nil, sinceQts, false, nil
}
out := make([]tg.EncryptedMessageClass, 0, len(msgs))
items := make([]deviceQtsItem, 0, len(msgs)+len(participantEvents))
for i := range msgs {
items = append(items, deviceQtsItem{qts: msgs[i].Qts, msg: &msgs[i]})
}
for i := range participantEvents {
items = append(items, deviceQtsItem{qts: participantEvents[i].Qts, participant: &participantEvents[i]})
}
sort.Slice(items, func(i, j int) bool { return items[i].qts < items[j].qts })
partial := len(msgs) > encryptedDifferencePageSize || len(participantEvents) > encryptedDifferencePageSize
if len(items) > encryptedDifferencePageSize {
items = items[:encryptedDifferencePageSize]
partial = true
}
encMsgs := make([]tg.EncryptedMessageClass, 0, len(items))
participantUpdates := make([]tg.UpdateClass, 0, len(items))
participantPeerIDs := make([]int64, 0, len(items))
participantChannelIDs := make([]int64, 0, len(items))
newQts := sinceQts
for i, m := range msgs {
for i, item := range items {
expected := newQts + 1
if m.Qts != expected {
return nil, sinceQts, false, fmt.Errorf("secret chat qts gap at index %d: got %d want %d", i, m.Qts, expected)
if item.qts != expected {
return nil, nil, nil, nil, sinceQts, false, fmt.Errorf("device qts gap at index %d: got %d want %d", i, item.qts, expected)
}
out = append(out, tgEncryptedMessage(m))
newQts = m.Qts
if item.msg != nil {
encMsgs = append(encMsgs, tgEncryptedMessage(*item.msg))
} else {
ev := item.participant
update := &tg.UpdateChannelParticipant{
ChannelID: ev.ChannelID,
Date: ev.Date,
ActorID: ev.ActorUserID,
UserID: ev.Participant.UserID,
Qts: ev.Qts,
}
if ev.Previous.UserID != 0 {
update.SetPrevParticipant(tgChannelParticipantForUpdate(ev.ReceiverUserID, ev.Previous))
}
if ev.Participant.UserID != 0 {
update.SetNewParticipant(tgChannelParticipantForUpdate(ev.ReceiverUserID, ev.Participant))
}
participantUpdates = append(participantUpdates, update, &tg.UpdateChannel{ChannelID: ev.ChannelID})
participantPeerIDs = append(participantPeerIDs, ev.ActorUserID, ev.Participant.UserID, ev.Participant.InviterUserID, ev.Previous.UserID, ev.Previous.InviterUserID)
participantChannelIDs = append(participantChannelIDs, ev.ChannelID)
}
newQts = item.qts
}
return out, newQts, partial, nil
return encMsgs, participantUpdates, participantPeerIDs, participantChannelIDs, newQts, partial, nil
}
// deviceQtsItem is one entry in a device's merged qts stream: either an
// encrypted message or a channel-participant self-notification, never both.
type deviceQtsItem struct {
qts int
msg *domain.SecretChatMessage
participant *domain.DeviceChannelParticipantEvent
}
// injectEncryptedMessages 把加密消息与推进后的 qts 注入差分响应(按类型分别写 State /