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.
339 lines
13 KiB
Go
339 lines
13 KiB
Go
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 handler(P1)。服务端是盲中继: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;幂等重发返回首次落库 date(store 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 返回当前设备已分配的最高 qts(getState 注入用,无则 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
|
||
}
|