owpengram-server/internal/store/secretchat.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

78 lines
5.7 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 store
import (
"context"
"telesrv/internal/domain"
)
// SecretChatStore 持久化私聊密聊握手状态机真值memory/postgres 双实现,行为契约由
// storetest 钉死。服务端是盲中继g_a/g_b/key_fingerprint 原样不透明存储。
// 设计见 docs/secret-chat-module.md。
type SecretChatStore interface {
// CreateSecretChat 插入 requested 态密聊。chat_id 必须等于 requestEncryption.random_id
// 主键/幂等键撞键返回 domain.ErrSecretChatRandomIDDuplicate禁止另分配 ID。
CreateSecretChat(ctx context.Context, chat domain.SecretChat) error
// GetSecretChat 按 chat_id 取密聊。
GetSecretChat(ctx context.Context, chatID int) (domain.SecretChat, bool, error)
// AcceptSecretChat 原子 CAS 绑定:仅当 requested 且 participant_auth_key_id 未绑定时
// 迁移到 normal、落 g_b/key_fingerprint、绑定接受设备。并发第二个 accept 或已成型/
// 已销毁分别返回 domain.ErrSecretChatAlreadyAccepted / ErrSecretChatAlreadyDeclined
// chat_id 不存在返回 domain.ErrSecretChatNotFound。
AcceptSecretChat(ctx context.Context, chatID int, participantAuthKeyID int64, gb []byte, keyFingerprint int64) (domain.SecretChat, error)
// DiscardSecretChat 迁移到 discarded幂等已是终态时 already=true 返回当前快照;
// chat_id 不存在返回 domain.ErrSecretChatNotFound。
DiscardSecretChat(ctx context.Context, chatID int, historyDeleted bool) (chat domain.SecretChat, already bool, err error)
// ListActiveSecretChatsByAuthKey 返回绑定该设备 perm auth_key作为发起方 admin 或
// 接受方 participant且未终态的密聊按 chat_id 升序。设备登出 / 授权撤销时用于级联
// discard 并通知对端(避免对端继续往死 auth_key 投递的静默死链。authKeyID==0 返回 nil。
ListActiveSecretChatsByAuthKey(ctx context.Context, authKeyID int64) ([]domain.SecretChat, error)
}
// EncryptedQueueStore 持久化密聊 qts 投递队列设备级memory/postgres 双实现)。
// updateNewEncryptedMessage 按接收设备 qts 序列投递qts 分配 + 写队列 + 推进
// reserved 水位在单事务内完成无空洞。盲中继bytes 原样存储。
// 设计见 docs/secret-chat-module.md §7。
type EncryptedQueueStore interface {
// AppendEncryptedMessage 为接收设备分配下一个 qts首值 1并写入队列与 reserved_qts
// 推进同事务。幂等:同 (receiver_auth_key_id, chat_id, random_id) 已存在时返回既有行 +
// existing=true不重分配 qts发送方丢响应重发拿同一 qts/date
AppendEncryptedMessage(ctx context.Context, msg domain.SecretChatMessage) (stored domain.SecretChatMessage, existing bool, err error)
// ListEncryptedMessagesSince 返回接收设备 qts > sinceQts 的连续消息qts 升序,最多 limit
ListEncryptedMessagesSince(ctx context.Context, receiverAuthKeyID int64, sinceQts, limit int) ([]domain.SecretChatMessage, error)
// ReservedQts 返回接收设备当前已分配的最高 qtsgetState 用,无行返 0
ReservedQts(ctx context.Context, receiverAuthKeyID int64) (int, error)
// AckEncryptedMessages 推进设备 confirmed_qts 到 maxQtsGREATEST 幂等,回退忽略),
// 标记 qts<=maxQts 行为 ackedreceivedQueue
AckEncryptedMessages(ctx context.Context, receiverAuthKeyID int64, maxQts int) error
// AppendStateEvent 写入无 qts 的 durable 密聊状态事件(握手/已读,离线补偿)。
AppendStateEvent(ctx context.Context, ev domain.EncryptedStateEvent) (int64, error)
// ListUndeliveredStateEvents 返回某设备未投递的状态事件(账号级 target_auth_key_id=0
// 或绑定本设备),按 id 升序,最多 limit。
ListUndeliveredStateEvents(ctx context.Context, targetUserID, deviceAuthKeyID int64, limit int) ([]domain.EncryptedStateEvent, error)
// MarkStateEventsDelivered 登记某设备已投递这些事件(幂等)。
MarkStateEventsDelivered(ctx context.Context, deviceAuthKeyID int64, eventIDs []int64) error
// PutEncryptedFile 持久化密聊文件元数据快照(铸造后写一次,幂等覆盖)。
PutEncryptedFile(ctx context.Context, ownerUserID int64, ref domain.EncryptedFileRef) error
// GetEncryptedFile 按 id + access_hash 回查文件快照inputEncryptedFile 复用路径)。
GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error)
}
// ChannelParticipantQueueStore 持久化 channel 成员关系自通知kick/ban/promote/
// transfer的设备级 qts 投递队列。updateChannelParticipant 走 qts 序列MTProto
// 规范),与 EncryptedQueueStore 共用同一张 secret_qts_watermarks 水位表——每设备
// 只有一条 qts 序列,两类事件按 qts 交错,通过各自独立的表分别落盘,读侧
// updates.getDifference按 qts 归并成连续序列。
type ChannelParticipantQueueStore interface {
// AppendChannelParticipantEvent 为接收设备分配下一个 qts 并写入队列,与
// reserved_qts 推进同事务(与 AppendEncryptedMessage 共用同一水位表)。
AppendChannelParticipantEvent(ctx context.Context, ev domain.DeviceChannelParticipantEvent) (domain.DeviceChannelParticipantEvent, error)
// ListChannelParticipantEventsSince 返回接收设备 qts > sinceQts 的连续事件
// qts 升序,最多 limit
ListChannelParticipantEventsSince(ctx context.Context, receiverAuthKeyID int64, sinceQts, limit int) ([]domain.DeviceChannelParticipantEvent, error)
// AckChannelParticipantEvents 标记 qts<=maxQts 行为 ackedconfirmed_qts 由
// AckEncryptedMessages 在同一张水位表上推进,这里只做本表的 GC 标记)。
AckChannelParticipantEvents(ctx context.Context, receiverAuthKeyID int64, maxQts int) error
}