owpengram-server/internal/rpc/encrypted_messages.go
Astra 206bde18e0 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.
2026-09-15 15:43:52 +01:00

339 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"fmt"
"sort"
"github.com/iamxvbaba/td/proto"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
)
const encryptedDifferencePageSize = 1000
// 私聊密聊 qts 消息收发 RPC handlerP1。服务端是盲中继sendEncrypted* 的 bytes 是
// 客户端加密的 DecryptedMessage服务端盲存进【接收方设备】的 qts 队列、原样转发,
// 永不解密。在线推 updateNewEncryptedMessage设备定向离线靠 getDifference 补回)。
// 设计见 docs/secret-chat-module.md §7/§8。
// pushEncryptedNewMessage 把 updateNewEncryptedMessage 定向投递给接收设备。
func (r *Router) pushEncryptedNewMessage(ctx context.Context, msg domain.SecretChatMessage) {
if msg.ReceiverUserID == 0 || msg.ReceiverAuthKeyID == 0 {
return
}
now := int(r.clock.Now().Unix())
upd := &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateNewEncryptedMessage{
Message: tgEncryptedMessage(msg),
Qts: msg.Qts,
}},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: now,
Seq: 0,
}
if targeted, ok := r.deps.Sessions.(AuthKeyTargetedSessionBinder); ok {
_, _ = targeted.PushToUserAuthKey(ctx, msg.ReceiverUserID, deviceAuthKeyBytes(msg.ReceiverAuthKeyID), proto.MessageFromServer, upd)
return
}
// 设备隔离是安全边界;缺少定向能力时只保留 durable qts离线 difference 补回。
r.log.Error("secret chat targeted session binder unavailable",
zap.Int64("target_user_id", msg.ReceiverUserID),
zap.Int64("target_auth_key_id", msg.ReceiverAuthKeyID))
}
func (r *Router) sendEncryptedCommon(ctx context.Context, peer tg.InputEncryptedChat, randomID int64, data []byte, isService bool) (tg.MessagesSentEncryptedMessageClass, error) {
if len(data) > domain.MaxSecretMessageDataBytes {
return nil, dataTooLongErr()
}
if r.deps.SecretChats == nil {
return nil, notImplementedErr()
}
userID, err := r.secretChatRequireUser(ctx)
if err != nil {
return nil, err
}
deviceAuthKeyID, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, internalErr()
}
_, stored, err := r.deps.SecretChats.SendEncrypted(ctx, peer.ChatID, userID, deviceAuthKeyID, peer.AccessHash, domain.SecretMessageDelivery{
RandomID: randomID,
Bytes: data,
IsService: isService,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, secretChatErr(err)
}
r.pushEncryptedNewMessage(ctx, stored)
// 无 server message id幂等重发返回首次落库 datestore dedup 保证)。
return &tg.MessagesSentEncryptedMessage{Date: stored.Date}, nil
}
func (r *Router) onMessagesSendEncrypted(ctx context.Context, req *tg.MessagesSendEncryptedRequest) (tg.MessagesSentEncryptedMessageClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
return r.sendEncryptedCommon(ctx, req.Peer, req.RandomID, req.Data, false)
}
func (r *Router) onMessagesSendEncryptedService(ctx context.Context, req *tg.MessagesSendEncryptedServiceRequest) (tg.MessagesSentEncryptedMessageClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
return r.sendEncryptedCommon(ctx, req.Peer, req.RandomID, req.Data, true)
}
// onMessagesReceivedQueue 确认接收设备已处理到 max_qts推进 confirmed + 标 acked 可 GC
// 返回空 Vector<long>DrKLO 调用处忽略响应sendRequest(req, null)),空集安全。
func (r *Router) onMessagesReceivedQueue(ctx context.Context, maxQts int) ([]int64, error) {
if r.deps.SecretChats == nil {
return nil, notImplementedErr()
}
if _, err := r.secretChatRequireUser(ctx); err != nil {
return nil, err
}
deviceKey, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, internalErr()
}
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
}
// onMessagesReportEncryptedSpam persists an immutable chat-metadata snapshot;
// the server remains unable to inspect encrypted message plaintext. Reporting
// does not discard or block the chat and emits no update.
func (r *Router) onMessagesReportEncryptedSpam(ctx context.Context, peer tg.InputEncryptedChat) (bool, error) {
if r.deps.SecretChats == nil || r.deps.Moderation == nil {
return false, notImplementedErr()
}
userID, err := r.secretChatRequireUser(ctx)
if err != nil {
return false, err
}
chat, _, _, err := r.resolveSecretChatPeer(ctx, userID, peer)
if err != nil {
return false, err
}
if _, _, err := r.deps.Moderation.ReportEncryptedSpam(ctx, userID, chat, r.clock.Now()); err != nil {
return false, moderationReportError(err)
}
return true, nil
}
// deviceEncryptedQts 返回当前设备已分配的最高 qtsgetState 注入用,无则 0
func (r *Router) deviceEncryptedQts(ctx context.Context) int {
if r.deps.SecretChats == nil {
return 0
}
deviceKey, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return 0
}
qts, err := r.deps.SecretChats.DeviceReservedQts(ctx, deviceKey)
if err != nil {
return 0
}
return qts
}
// 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, nil, nil, nil, sinceQts, false, nil
}
deviceKey, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, nil, nil, nil, sinceQts, false, nil
}
msgs, err := r.deps.SecretChats.ListNewMessages(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
if err != nil {
return nil, nil, nil, nil, sinceQts, false, err
}
participantEvents, err := r.deps.SecretChats.ListChannelParticipantEventsSince(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
if err != nil {
return nil, nil, nil, nil, sinceQts, false, err
}
if len(msgs) == 0 && len(participantEvents) == 0 {
return nil, nil, nil, nil, sinceQts, false, nil
}
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, item := range items {
expected := newQts + 1
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)
}
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 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 /
// IntermediateState 的 Qts
func injectEncryptedMessages(diff tg.UpdatesDifferenceClass, encMsgs []tg.EncryptedMessageClass, newQts int) tg.UpdatesDifferenceClass {
switch v := diff.(type) {
case *tg.UpdatesDifference:
v.NewEncryptedMessages = append(v.NewEncryptedMessages, encMsgs...)
v.State.Qts = newQts
case *tg.UpdatesDifferenceSlice:
v.NewEncryptedMessages = append(v.NewEncryptedMessages, encMsgs...)
v.IntermediateState.Qts = newQts
}
return diff
}
// encryptedStateUpdates 返回当前设备未投递的握手/已读状态事件重建出的 update
// OtherUpdates、涉及的 peer user id补 Users、以及要登记已投递的事件 id。
// encryption 事件按 secret_chats 权威态重建(不固化密钥材料快照)。账号级邀请在
// accept 后只对未绑定设备投影为 discarded获胜设备消费并跳过绝不能收到 normal 泄漏。
func (r *Router) encryptedStateUpdates(ctx context.Context, userID int64) (updates []tg.UpdateClass, peerUserIDs []int64, eventIDs []int64, partial bool, err error) {
if r.deps.SecretChats == nil {
return nil, nil, nil, false, nil
}
deviceKey, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, nil, nil, false, nil
}
events, err := r.deps.SecretChats.ListStateEvents(ctx, userID, deviceKey, encryptedDifferencePageSize+1)
if err != nil {
return nil, nil, nil, false, err
}
if len(events) == 0 {
return nil, nil, nil, false, nil
}
partial = len(events) > encryptedDifferencePageSize
if partial {
events = events[:encryptedDifferencePageSize]
}
seenEncryption := make(map[int]struct{})
for _, ev := range events {
switch ev.Type {
case domain.EncryptedStateEventEncryption:
chat, found, gerr := r.deps.SecretChats.GetSecretChat(ctx, ev.ChatID)
if gerr != nil || !found {
continue
}
eventIDs = append(eventIDs, ev.ID)
if _, duplicate := seenEncryption[chat.ID]; duplicate {
continue
}
seenEncryption[chat.ID] = struct{}{}
chatView := tgEncryptedChatForViewer(chat, userID)
if ev.TargetAuthKeyID == 0 && chat.State == domain.SecretChatStateNormal {
// 账号级事件只承载 accept 前邀请。accept 后获胜设备已有同步响应;其它设备
// 必须收敛为 discarded不能用当前 normal 权威态泄漏 access_hash/g_a。
if chat.AuthKeyOf(userID) == deviceKey {
continue
}
chatView = &tg.EncryptedChatDiscarded{ID: chat.ID, HistoryDeleted: true}
}
updates = append(updates, &tg.UpdateEncryption{Chat: chatView, Date: ev.Date})
peerUserIDs = append(peerUserIDs, chat.AdminUserID, chat.ParticipantUserID)
case domain.EncryptedStateEventRead:
updates = append(updates, &tg.UpdateEncryptedMessagesRead{
ChatID: ev.ChatID,
MaxDate: ev.MaxDate,
Date: ev.Date,
})
eventIDs = append(eventIDs, ev.ID)
}
}
return updates, peerUserIDs, eventIDs, partial, nil
}
// injectEncryptedOtherUpdates 把握手/已读 update 追加进差分的 OtherUpdates、把 peer
// user 对象追加进 Users。
func (r *Router) injectEncryptedOtherUpdates(ctx context.Context, viewerUserID int64, diff tg.UpdatesDifferenceClass, updates []tg.UpdateClass, peerUserIDs []int64) tg.UpdatesDifferenceClass {
if len(updates) == 0 {
return diff
}
users := r.tgUsersForIDs(ctx, viewerUserID, peerUserIDs)
return appendEncryptedOtherUpdates(diff, updates, users)
}
func (r *Router) injectEncryptedOtherUpdatesStrict(ctx context.Context, viewerUserID int64, diff tg.UpdatesDifferenceClass, updates []tg.UpdateClass, peerUserIDs []int64, cache *viewerPeerCache) (tg.UpdatesDifferenceClass, error) {
if len(updates) == 0 {
return diff, nil
}
if cache == nil {
cache = newViewerPeerCache(r)
}
users, err := cache.usersForIDsStrict(ctx, viewerUserID, peerUserIDs)
if err != nil {
return nil, err
}
return appendEncryptedOtherUpdates(diff, updates, r.tgUsersForViewer(viewerUserID, users)), nil
}
func appendEncryptedOtherUpdates(diff tg.UpdatesDifferenceClass, updates []tg.UpdateClass, users []tg.UserClass) tg.UpdatesDifferenceClass {
switch v := diff.(type) {
case *tg.UpdatesDifference:
v.OtherUpdates = append(v.OtherUpdates, updates...)
v.Users = appendUniqueTGUsers(v.Users, users...)
case *tg.UpdatesDifferenceSlice:
v.OtherUpdates = append(v.OtherUpdates, updates...)
v.Users = appendUniqueTGUsers(v.Users, users...)
}
return diff
}