Merge branch 'fix/channel-participant-qts'
This commit is contained in:
commit
53872f8fc9
16 changed files with 482 additions and 42 deletions
|
|
@ -1360,7 +1360,8 @@ func run(logger *zap.Logger) error {
|
||||||
// 私聊端对端加密(Secret Chat)握手状态机 + qts 投递队列(盲中继)。
|
// 私聊端对端加密(Secret Chat)握手状态机 + qts 投递队列(盲中继)。
|
||||||
secretChatStore := postgres.NewSecretChatStore(pool)
|
secretChatStore := postgres.NewSecretChatStore(pool)
|
||||||
encryptedQueueStore := postgres.NewEncryptedQueueStore(pool)
|
encryptedQueueStore := postgres.NewEncryptedQueueStore(pool)
|
||||||
secretChatService := secretchatapp.NewService(secretChatStore, encryptedQueueStore)
|
channelParticipantQueueStore := postgres.NewChannelParticipantQueueStore(pool)
|
||||||
|
secretChatService := secretchatapp.NewService(secretChatStore, encryptedQueueStore, channelParticipantQueueStore)
|
||||||
// Passkey:凭据持久化走 postgres;一次性挑战走进程内内存(短 TTL,与 QR 登录 token
|
// Passkey:凭据持久化走 postgres;一次性挑战走进程内内存(短 TTL,与 QR 登录 token
|
||||||
// 同属进程内一次性凭据,不跨实例)。
|
// 同属进程内一次性凭据,不跨实例)。
|
||||||
passkeyStore := postgres.NewPasskeyStore(pool)
|
passkeyStore := postgres.NewPasskeyStore(pool)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS public.channel_participant_event_queue;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Durable per-device qts queue for channel-membership self-notifications
|
||||||
|
-- (kick/ban/promote/demote/ownership transfer). Shares the qts number space
|
||||||
|
-- in secret_qts_watermarks with encrypted_message_queue: a device has one
|
||||||
|
-- qts sequence, and updateChannelParticipant rides it per the MTProto spec,
|
||||||
|
-- so a disconnected device can recover a missed kick/ban via
|
||||||
|
-- updates.getDifference instead of losing it once its live push is missed.
|
||||||
|
CREATE TABLE public.channel_participant_event_queue (
|
||||||
|
receiver_auth_key_id bigint NOT NULL,
|
||||||
|
qts integer NOT NULL,
|
||||||
|
receiver_user_id bigint NOT NULL,
|
||||||
|
channel_id bigint NOT NULL,
|
||||||
|
actor_user_id bigint NOT NULL,
|
||||||
|
date integer NOT NULL,
|
||||||
|
previous_participant jsonb,
|
||||||
|
new_participant jsonb,
|
||||||
|
acked boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
PRIMARY KEY (receiver_auth_key_id, qts)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_cpeq_unacked ON public.channel_participant_event_queue USING btree (receiver_auth_key_id, qts) WHERE (acked = false);
|
||||||
|
|
@ -18,11 +18,19 @@ import (
|
||||||
type Service struct {
|
type Service struct {
|
||||||
store store.SecretChatStore
|
store store.SecretChatStore
|
||||||
queue store.EncryptedQueueStore
|
queue store.EncryptedQueueStore
|
||||||
|
// channelParticipants is the per-device qts queue for channel-membership
|
||||||
|
// self-notifications (kick/ban/promote/transfer). It shares its qts number
|
||||||
|
// space with queue (one qts sequence per device, per the MTProto spec for
|
||||||
|
// updateChannelParticipant) but is otherwise unrelated to secret chats;
|
||||||
|
// it lives here only because this Service already owns the device qts
|
||||||
|
// watermark plumbing.
|
||||||
|
channelParticipants store.ChannelParticipantQueueStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService 创建密聊服务。
|
// NewService 创建密聊服务。channelParticipants may be nil in tests that don't
|
||||||
func NewService(st store.SecretChatStore, queue store.EncryptedQueueStore) *Service {
|
// exercise channel-membership self-notifications.
|
||||||
return &Service{store: st, queue: queue}
|
func NewService(st store.SecretChatStore, queue store.EncryptedQueueStore, channelParticipants store.ChannelParticipantQueueStore) *Service {
|
||||||
|
return &Service{store: st, queue: queue, channelParticipants: channelParticipants}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestEncryption 受理 requestEncryption:校验 g_a → 校验 random_id/chat_id 全局唯一性 → 分配双
|
// RequestEncryption 受理 requestEncryption:校验 g_a → 校验 random_id/chat_id 全局唯一性 → 分配双
|
||||||
|
|
@ -242,6 +250,34 @@ func (s *Service) AckQueue(ctx context.Context, deviceAuthKeyID int64, maxQts in
|
||||||
return s.queue.AckEncryptedMessages(ctx, deviceAuthKeyID, maxQts)
|
return s.queue.AckEncryptedMessages(ctx, deviceAuthKeyID, maxQts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendChannelParticipantEvent durably enqueues a channel-membership
|
||||||
|
// self-notification for one device, reserving that device's next qts.
|
||||||
|
func (s *Service) AppendChannelParticipantEvent(ctx context.Context, ev domain.DeviceChannelParticipantEvent) (domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
if s.channelParticipants == nil || ev.ReceiverAuthKeyID == 0 {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, nil
|
||||||
|
}
|
||||||
|
return s.channelParticipants.AppendChannelParticipantEvent(ctx, ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListChannelParticipantEventsSince returns a device's channel-membership
|
||||||
|
// self-notifications with qts > sinceQts (getDifference catch-up).
|
||||||
|
func (s *Service) ListChannelParticipantEventsSince(ctx context.Context, deviceAuthKeyID int64, sinceQts, limit int) ([]domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
if s.channelParticipants == nil || deviceAuthKeyID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s.channelParticipants.ListChannelParticipantEventsSince(ctx, deviceAuthKeyID, sinceQts, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AckChannelParticipantEvents marks a device's channel-membership
|
||||||
|
// self-notifications up to maxQts as delivered (GC only; confirmed_qts itself
|
||||||
|
// advances via AckQueue against the shared watermark).
|
||||||
|
func (s *Service) AckChannelParticipantEvents(ctx context.Context, deviceAuthKeyID int64, maxQts int) error {
|
||||||
|
if s.channelParticipants == nil || deviceAuthKeyID == 0 || maxQts <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.channelParticipants.AckChannelParticipantEvents(ctx, deviceAuthKeyID, maxQts)
|
||||||
|
}
|
||||||
|
|
||||||
// RecordEncryptionEvent 写入 durable updateEncryption 状态事件(离线补偿)。
|
// RecordEncryptionEvent 写入 durable updateEncryption 状态事件(离线补偿)。
|
||||||
// targetAuthKeyID=0 表示账号级(建链前邀请/撤回对 target 所有设备可见),非 0 表示
|
// targetAuthKeyID=0 表示账号级(建链前邀请/撤回对 target 所有设备可见),非 0 表示
|
||||||
// 绑定设备定向。投递时按 secret_chats 权威态重建(不固化快照)。
|
// 绑定设备定向。投递时按 secret_chats 权威态重建(不固化快照)。
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ func validGA() []byte {
|
||||||
|
|
||||||
func newTestService() (*Service, *memory.SecretChatStore) {
|
func newTestService() (*Service, *memory.SecretChatStore) {
|
||||||
st := memory.NewSecretChatStore()
|
st := memory.NewSecretChatStore()
|
||||||
return NewService(st, memory.NewEncryptedQueueStore()), st
|
queue := memory.NewEncryptedQueueStore()
|
||||||
|
return NewService(st, queue, queue), st
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
19
internal/domain/channel_participant_queue.go
Normal file
19
internal/domain/channel_participant_queue.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
// DeviceChannelParticipantEvent is a durable, per-device queued entry recording
|
||||||
|
// that the receiving account's participant status in a channel changed
|
||||||
|
// (kick/ban/promote/demote/ownership transfer). It rides the same qts sequence
|
||||||
|
// as SecretChatMessage (one counter per device, shared with secret chats per
|
||||||
|
// the MTProto spec for updateChannelParticipant), so a device that misses the
|
||||||
|
// live push can recover the event via updates.getDifference instead of never
|
||||||
|
// learning it was removed.
|
||||||
|
type DeviceChannelParticipantEvent struct {
|
||||||
|
ReceiverAuthKeyID int64
|
||||||
|
ReceiverUserID int64
|
||||||
|
Qts int
|
||||||
|
ChannelID int64
|
||||||
|
ActorUserID int64
|
||||||
|
Date int
|
||||||
|
Previous ChannelMember
|
||||||
|
Participant ChannelMember
|
||||||
|
}
|
||||||
|
|
@ -383,6 +383,8 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages
|
||||||
}
|
}
|
||||||
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
|
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
|
||||||
r.addOnlineChannelMemberships(res.Channel.ID, res.OldOwner.UserID, res.NewOwner.UserID)
|
r.addOnlineChannelMemberships(res.Channel.ID, res.OldOwner.UserID, res.NewOwner.UserID)
|
||||||
|
r.deliverChannelParticipantSelfEvent(ctx, res.OldOwner.UserID, userID, res.Channel, res.PreviousOwner, res.OldOwner, res.Date)
|
||||||
|
r.deliverChannelParticipantSelfEvent(ctx, res.NewOwner.UserID, userID, res.Channel, res.PreviousNewOwner, res.NewOwner, res.Date)
|
||||||
cache := newViewerPeerCache(r)
|
cache := newViewerPeerCache(r)
|
||||||
updates := r.channelOwnershipTransferUpdatesWithPeerCache(ctx, userID, userID, res, cache)
|
updates := r.channelOwnershipTransferUpdatesWithPeerCache(ctx, userID, userID, res, cache)
|
||||||
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package rpc
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/iamxvbaba/td/proto"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
"github.com/iamxvbaba/td/tgerr"
|
"github.com/iamxvbaba/td/tgerr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
@ -451,6 +452,58 @@ func (r *Router) recordChannelStateForUser(ctx context.Context, userID, channelI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deliverChannelParticipantSelfEvent durably enqueues, and best-effort live
|
||||||
|
// pushes, a correctly qts-numbered updateChannelParticipant to every device of
|
||||||
|
// the affected (non-actor) user. updateChannelParticipant carries the account's
|
||||||
|
// qts per the MTProto spec; a client discards any update whose qts isn't a
|
||||||
|
// proper increment over its own, so the generic pts-based fanout in
|
||||||
|
// pushChannelUpdates -- which always sends qts=0 -- is silently ignored by
|
||||||
|
// real clients for this specific update type. This is the only path that
|
||||||
|
// actually reaches the affected user's own client, both live and (via
|
||||||
|
// AppendChannelParticipantEvent's durable queue, replayed by
|
||||||
|
// updates.getDifference) after being offline when it happened.
|
||||||
|
func (r *Router) deliverChannelParticipantSelfEvent(ctx context.Context, targetUserID, actorUserID int64, channel domain.Channel, previous, participant domain.ChannelMember, date int) {
|
||||||
|
if r.deps.SecretChats == nil || r.deps.Auth == nil || targetUserID == 0 || targetUserID == actorUserID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
auths, err := r.deps.Auth.ListAuthorizations(ctx, targetUserID)
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targeted, canTarget := r.deps.Sessions.(AuthKeyTargetedSessionBinder)
|
||||||
|
for _, a := range auths {
|
||||||
|
deviceAuthKeyID := businessAuthKeyInt64(a.AuthKeyID)
|
||||||
|
if deviceAuthKeyID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stored, err := r.deps.SecretChats.AppendChannelParticipantEvent(ctx, domain.DeviceChannelParticipantEvent{
|
||||||
|
ReceiverAuthKeyID: deviceAuthKeyID,
|
||||||
|
ReceiverUserID: targetUserID,
|
||||||
|
ChannelID: channel.ID,
|
||||||
|
ActorUserID: actorUserID,
|
||||||
|
Date: date,
|
||||||
|
Previous: previous,
|
||||||
|
Participant: participant,
|
||||||
|
})
|
||||||
|
if err != nil || stored.Qts == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !canTarget {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upd := r.channelParticipantUpdates(ctx, targetUserID, actorUserID, channel, previous, participant, date)
|
||||||
|
if upd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, u := range upd.Updates {
|
||||||
|
if uc, ok := u.(*tg.UpdateChannelParticipant); ok {
|
||||||
|
uc.Qts = stored.Qts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = targeted.PushToUserAuthKey(ctx, targetUserID, deviceAuthKeyBytes(deviceAuthKeyID), proto.MessageFromServer, upd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAdminRequest) (tg.UpdatesClass, error) {
|
func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAdminRequest) (tg.UpdatesClass, error) {
|
||||||
if r.deps.Channels == nil && r.deps.Communities == nil {
|
if r.deps.Channels == nil && r.deps.Communities == nil {
|
||||||
return nil, notImplementedErr()
|
return nil, notImplementedErr()
|
||||||
|
|
@ -511,6 +564,7 @@ func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAd
|
||||||
} else {
|
} else {
|
||||||
r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
||||||
}
|
}
|
||||||
|
r.deliverChannelParticipantSelfEvent(ctx, res.Participant.UserID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
||||||
cache := newViewerPeerCache(r)
|
cache := newViewerPeerCache(r)
|
||||||
updates := r.channelParticipantUpdatesWithPeerCache(ctx, userID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
|
updates := r.channelParticipantUpdatesWithPeerCache(ctx, userID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
|
||||||
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
||||||
|
|
@ -557,6 +611,7 @@ func (r *Router) onChannelsEditBanned(ctx context.Context, req *tg.ChannelsEditB
|
||||||
if res.Participant.Status == domain.ChannelMemberKicked && res.Previous.Status == domain.ChannelMemberActive {
|
if res.Participant.Status == domain.ChannelMemberKicked && res.Previous.Status == domain.ChannelMemberActive {
|
||||||
r.recordChannelStateForUser(ctx, res.Participant.UserID, res.Channel.ID, false)
|
r.recordChannelStateForUser(ctx, res.Participant.UserID, res.Channel.ID, false)
|
||||||
}
|
}
|
||||||
|
r.deliverChannelParticipantSelfEvent(ctx, res.Participant.UserID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
||||||
cache := newViewerPeerCache(r)
|
cache := newViewerPeerCache(r)
|
||||||
build := func(viewerUserID int64) *tg.Updates {
|
build := func(viewerUserID int64) *tg.Updates {
|
||||||
updates := r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
|
updates := r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
|
||||||
|
|
|
||||||
|
|
@ -1237,6 +1237,17 @@ type SecretChatService interface {
|
||||||
MarkStateEventsDelivered(ctx context.Context, deviceAuthKeyID int64, eventIDs []int64) error
|
MarkStateEventsDelivered(ctx context.Context, deviceAuthKeyID int64, eventIDs []int64) error
|
||||||
PutEncryptedFile(ctx context.Context, ownerUserID int64, ref domain.EncryptedFileRef) error
|
PutEncryptedFile(ctx context.Context, ownerUserID int64, ref domain.EncryptedFileRef) error
|
||||||
GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error)
|
GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error)
|
||||||
|
|
||||||
|
// AppendChannelParticipantEvent durably enqueues a channel-membership
|
||||||
|
// self-notification (kick/ban/promote/transfer) for one device, reserving
|
||||||
|
// that device's next qts from the same watermark as secret chat messages.
|
||||||
|
AppendChannelParticipantEvent(ctx context.Context, ev domain.DeviceChannelParticipantEvent) (domain.DeviceChannelParticipantEvent, error)
|
||||||
|
// ListChannelParticipantEventsSince returns a device's channel-membership
|
||||||
|
// self-notifications with qts > sinceQts (getDifference catch-up).
|
||||||
|
ListChannelParticipantEventsSince(ctx context.Context, deviceAuthKeyID int64, sinceQts, limit int) ([]domain.DeviceChannelParticipantEvent, error)
|
||||||
|
// AckChannelParticipantEvents marks a device's channel-membership
|
||||||
|
// self-notifications up to maxQts as delivered.
|
||||||
|
AckChannelParticipantEvents(ctx context.Context, deviceAuthKeyID int64, maxQts int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PhoneService 抽象私聊 1:1 通话信令状态机(app/phone)。所有返回值都是状态快照;
|
// PhoneService 抽象私聊 1:1 通话信令状态机(app/phone)。所有返回值都是状态快照;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ func newEncryptedFixture(t *testing.T) *encryptedFixture {
|
||||||
queueStore := memory.NewEncryptedQueueStore()
|
queueStore := memory.NewEncryptedQueueStore()
|
||||||
router := New(Config{}, Deps{
|
router := New(Config{}, Deps{
|
||||||
Users: appusers.NewService(userStore),
|
Users: appusers.NewService(userStore),
|
||||||
SecretChats: appsecret.NewService(secretStore, queueStore),
|
SecretChats: appsecret.NewService(secretStore, queueStore, queueStore),
|
||||||
Updates: appupdates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore()),
|
Updates: appupdates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore()),
|
||||||
Files: &fakeFiles{},
|
Files: &fakeFiles{},
|
||||||
Sessions: sessions,
|
Sessions: sessions,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package rpc
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/iamxvbaba/td/proto"
|
"github.com/iamxvbaba/td/proto"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"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 {
|
if err := r.deps.SecretChats.AckQueue(ctx, deviceKey, maxQts); err != nil {
|
||||||
return nil, internalErr()
|
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
|
return []int64{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,38 +148,84 @@ func (r *Router) deviceEncryptedQts(ctx context.Context) int {
|
||||||
return qts
|
return qts
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryptedDifference 返回当前设备 qts > sinceQts 的连续前缀、推进后的 qts 与是否还有
|
// encryptedDifference 返回当前设备 qts > sinceQts 的连续前缀(加密消息 +
|
||||||
// 下一页。存储错误或 qts gap 必须 fail-fast,禁止越过缺口推进客户端水位。
|
// channel 成员关系自通知,二者共用同一设备 qts 序列,按 qts 归并后统一做缺口
|
||||||
func (r *Router) encryptedDifference(ctx context.Context, sinceQts int) ([]tg.EncryptedMessageClass, int, bool, error) {
|
// 检查)、推进后的 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 {
|
if r.deps.SecretChats == nil {
|
||||||
return nil, sinceQts, false, nil
|
return nil, nil, nil, nil, sinceQts, false, nil
|
||||||
}
|
}
|
||||||
deviceKey, ok := businessAuthKeyIDFrom(ctx)
|
deviceKey, ok := businessAuthKeyIDFrom(ctx)
|
||||||
if !ok {
|
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)
|
msgs, err := r.deps.SecretChats.ListNewMessages(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, sinceQts, false, err
|
return nil, nil, nil, nil, sinceQts, false, err
|
||||||
}
|
}
|
||||||
if len(msgs) == 0 {
|
participantEvents, err := r.deps.SecretChats.ListChannelParticipantEventsSince(ctx, deviceKey, sinceQts, encryptedDifferencePageSize+1)
|
||||||
return nil, sinceQts, false, nil
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, sinceQts, false, err
|
||||||
}
|
}
|
||||||
partial := len(msgs) > encryptedDifferencePageSize
|
if len(msgs) == 0 && len(participantEvents) == 0 {
|
||||||
if partial {
|
return nil, nil, nil, nil, sinceQts, false, nil
|
||||||
msgs = msgs[:encryptedDifferencePageSize]
|
|
||||||
}
|
}
|
||||||
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
|
newQts := sinceQts
|
||||||
for i, m := range msgs {
|
for i, item := range items {
|
||||||
expected := newQts + 1
|
expected := newQts + 1
|
||||||
if m.Qts != expected {
|
if item.qts != expected {
|
||||||
return nil, sinceQts, false, fmt.Errorf("secret chat qts gap at index %d: got %d want %d", i, m.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))
|
if item.msg != nil {
|
||||||
newQts = m.Qts
|
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,
|
||||||
}
|
}
|
||||||
return out, newQts, partial, nil
|
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 /
|
// injectEncryptedMessages 把加密消息与推进后的 qts 注入差分响应(按类型分别写 State /
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ func TestInvokeWithoutUpdatesBaselineCommitsResultAndSecretEventsWithoutSubscrib
|
||||||
authKeyID := [8]byte{21}
|
authKeyID := [8]byte{21}
|
||||||
deviceKey := businessAuthKeyInt64(authKeyID)
|
deviceKey := businessAuthKeyInt64(authKeyID)
|
||||||
queue := memory.NewEncryptedQueueStore()
|
queue := memory.NewEncryptedQueueStore()
|
||||||
secret := appsecret.NewService(memory.NewSecretChatStore(), queue)
|
secret := appsecret.NewService(memory.NewSecretChatStore(), queue, queue)
|
||||||
eventID, err := queue.AppendStateEvent(context.Background(), domain.EncryptedStateEvent{
|
eventID, err := queue.AppendStateEvent(context.Background(), domain.EncryptedStateEvent{
|
||||||
TargetUserID: userID,
|
TargetUserID: userID,
|
||||||
ChatID: 77,
|
ChatID: 77,
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,9 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 密聊设备级 qts 消息(独立于账号级 pts 事件):按当前设备 req.Qts 补回。
|
// 密聊设备级 qts 消息 + channel 成员关系自通知(共用同一设备 qts 序列,独立于
|
||||||
encMsgs, newQts, encryptedPartial, err := r.encryptedDifference(ctx, req.Qts)
|
// 账号级 pts 事件):按当前设备 req.Qts 补回。
|
||||||
|
encMsgs, participantUpdates, participantPeerIDs, participantChannelIDs, newQts, encryptedPartial, err := r.encryptedDifference(ctx, req.Qts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Error("load secret chat qts difference", zap.Error(err))
|
r.log.Error("load secret chat qts difference", zap.Error(err))
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
|
|
@ -130,6 +131,8 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
|
||||||
r.log.Error("load secret chat state difference", zap.Error(err))
|
r.log.Error("load secret chat state difference", zap.Error(err))
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
|
stateUpdates = append(stateUpdates, participantUpdates...)
|
||||||
|
statePeerUserIDs = append(statePeerUserIDs, participantPeerIDs...)
|
||||||
// 账号 pts、设备 qts 和无序号状态事件任一被截断,都必须返回 differenceSlice。
|
// 账号 pts、设备 qts 和无序号状态事件任一被截断,都必须返回 differenceSlice。
|
||||||
st.Partial = st.Partial || encryptedPartial || encryptedStatePartial
|
st.Partial = st.Partial || encryptedPartial || encryptedStatePartial
|
||||||
if !st.Partial && len(st.Events) == 0 && len(st.ChannelNudges) == 0 && len(encMsgs) == 0 && len(stateUpdates) == 0 {
|
if !st.Partial && len(st.Events) == 0 && len(st.ChannelNudges) == 0 && len(encMsgs) == 0 && len(stateUpdates) == 0 {
|
||||||
|
|
@ -158,12 +161,40 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
|
diff = r.injectChannelParticipantEventChats(ctx, userID, diff, participantChannelIDs)
|
||||||
returnedCursor := st.State
|
returnedCursor := st.State
|
||||||
returnedCursor.Qts = newQts
|
returnedCursor.Qts = newQts
|
||||||
r.stageUpdatesBaselineAfterDelivery(ctx, userID, &returnedCursor, domain.UpdateStateCommitDeliveredOnly, stateEventIDs, true)
|
r.stageUpdatesBaselineAfterDelivery(ctx, userID, &returnedCursor, domain.UpdateStateCommitDeliveredOnly, stateEventIDs, true)
|
||||||
return diff, nil
|
return diff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// injectChannelParticipantEventChats resolves each channel referenced by a
|
||||||
|
// recovered channel-participant self-notification through GetChannels (which
|
||||||
|
// already projects Forbidden for a kicked/banned viewer, see
|
||||||
|
// tgChannelChatForView) and appends the resulting chat objects to diff --
|
||||||
|
// the bare updateChannel nudge alone carries no chat payload, and the
|
||||||
|
// receiving client needs the full projection to apply the removal.
|
||||||
|
func (r *Router) injectChannelParticipantEventChats(ctx context.Context, userID int64, diff tg.UpdatesDifferenceClass, channelIDs []int64) tg.UpdatesDifferenceClass {
|
||||||
|
if r.deps.Channels == nil || len(channelIDs) == 0 {
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
views, err := r.deps.Channels.GetChannels(ctx, userID, channelIDs)
|
||||||
|
if err != nil || len(views) == 0 {
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
chats := make([]tg.ChatClass, 0, len(views))
|
||||||
|
for _, view := range views {
|
||||||
|
chats = append(chats, tgChannelChatForView(userID, view))
|
||||||
|
}
|
||||||
|
switch v := diff.(type) {
|
||||||
|
case *tg.UpdatesDifference:
|
||||||
|
v.Chats = appendUniqueTGChats(v.Chats, chats...)
|
||||||
|
case *tg.UpdatesDifferenceSlice:
|
||||||
|
v.Chats = appendUniqueTGChats(v.Chats, chats...)
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int64, sinceDate int) []domain.ChannelDifferenceNudge {
|
func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int64, sinceDate int) []domain.ChannelDifferenceNudge {
|
||||||
if r.deps.Channels == nil || userID == 0 || sinceDate <= 0 {
|
if r.deps.Channels == nil || userID == 0 || sinceDate <= 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -114,10 +114,13 @@ func (s *SecretChatStore) ListActiveSecretChatsByAuthKey(_ context.Context, auth
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptedQueueStore 是 store.EncryptedQueueStore 的进程内实现。
|
// EncryptedQueueStore 是 store.EncryptedQueueStore 的进程内实现。也实现
|
||||||
|
// store.ChannelParticipantQueueStore:两者共用同一个 reserved 水位 map(一台
|
||||||
|
// 设备一条 qts 序列),与 postgres 两张表共用一张 secret_qts_watermarks 对齐。
|
||||||
type EncryptedQueueStore struct {
|
type EncryptedQueueStore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
byDevice map[int64][]domain.SecretChatMessage // receiverAuthKeyID → qts 升序消息
|
byDevice map[int64][]domain.SecretChatMessage // receiverAuthKeyID → qts 升序消息
|
||||||
|
byDeviceParticipant map[int64][]domain.DeviceChannelParticipantEvent // receiverAuthKeyID → qts 升序事件
|
||||||
reserved map[int64]int
|
reserved map[int64]int
|
||||||
confirmed map[int64]int
|
confirmed map[int64]int
|
||||||
dedup map[emqDedupKey]int // → qts
|
dedup map[emqDedupKey]int // → qts
|
||||||
|
|
@ -137,6 +140,7 @@ type emqDedupKey struct {
|
||||||
func NewEncryptedQueueStore() *EncryptedQueueStore {
|
func NewEncryptedQueueStore() *EncryptedQueueStore {
|
||||||
return &EncryptedQueueStore{
|
return &EncryptedQueueStore{
|
||||||
byDevice: make(map[int64][]domain.SecretChatMessage),
|
byDevice: make(map[int64][]domain.SecretChatMessage),
|
||||||
|
byDeviceParticipant: make(map[int64][]domain.DeviceChannelParticipantEvent),
|
||||||
reserved: make(map[int64]int),
|
reserved: make(map[int64]int),
|
||||||
confirmed: make(map[int64]int),
|
confirmed: make(map[int64]int),
|
||||||
dedup: make(map[emqDedupKey]int),
|
dedup: make(map[emqDedupKey]int),
|
||||||
|
|
@ -144,6 +148,47 @@ func NewEncryptedQueueStore() *EncryptedQueueStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneChannelParticipantEvent(ev domain.DeviceChannelParticipantEvent) domain.DeviceChannelParticipantEvent {
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendChannelParticipantEvent implements store.ChannelParticipantQueueStore,
|
||||||
|
// reserving from the same per-device qts counter as AppendEncryptedMessage.
|
||||||
|
func (s *EncryptedQueueStore) AppendChannelParticipantEvent(_ context.Context, ev domain.DeviceChannelParticipantEvent) (domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.reserved[ev.ReceiverAuthKeyID]++
|
||||||
|
ev.Qts = s.reserved[ev.ReceiverAuthKeyID]
|
||||||
|
stored := cloneChannelParticipantEvent(ev)
|
||||||
|
s.byDeviceParticipant[ev.ReceiverAuthKeyID] = append(s.byDeviceParticipant[ev.ReceiverAuthKeyID], stored)
|
||||||
|
return cloneChannelParticipantEvent(stored), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EncryptedQueueStore) ListChannelParticipantEventsSince(_ context.Context, receiverAuthKeyID int64, sinceQts, limit int) ([]domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
var out []domain.DeviceChannelParticipantEvent
|
||||||
|
for _, ev := range s.byDeviceParticipant[receiverAuthKeyID] {
|
||||||
|
if ev.Qts > sinceQts {
|
||||||
|
out = append(out, cloneChannelParticipantEvent(ev))
|
||||||
|
if len(out) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AckChannelParticipantEvents is a no-op in the memory store: unlike postgres
|
||||||
|
// it does no row-level GC, and confirmed_qts is already advanced by
|
||||||
|
// AckEncryptedMessages against the shared reserved/confirmed watermark.
|
||||||
|
func (s *EncryptedQueueStore) AckChannelParticipantEvents(_ context.Context, _ int64, _ int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func cloneSecretMessage(m domain.SecretChatMessage) domain.SecretChatMessage {
|
func cloneSecretMessage(m domain.SecretChatMessage) domain.SecretChatMessage {
|
||||||
m.Bytes = append([]byte(nil), m.Bytes...)
|
m.Bytes = append([]byte(nil), m.Bytes...)
|
||||||
if m.File != nil {
|
if m.File != nil {
|
||||||
|
|
|
||||||
149
internal/store/postgres/channel_participant_queue.go
Normal file
149
internal/store/postgres/channel_participant_queue.go
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
"telesrv/internal/store/postgres/sqlcgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChannelParticipantQueueStore is the PostgreSQL implementation of
|
||||||
|
// store.ChannelParticipantQueueStore. It reserves qts from the same
|
||||||
|
// secret_qts_watermarks table as EncryptedQueueStore (one qts sequence per
|
||||||
|
// device, shared across event kinds), keeping this table's own schema and
|
||||||
|
// queries entirely separate from the encrypted-message hot path.
|
||||||
|
type ChannelParticipantQueueStore struct {
|
||||||
|
db sqlcgen.DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChannelParticipantQueueStore builds the store on a pgx pool or transaction.
|
||||||
|
func NewChannelParticipantQueueStore(db sqlcgen.DBTX) *ChannelParticipantQueueStore {
|
||||||
|
return &ChannelParticipantQueueStore{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelParticipantEventColumns = `receiver_auth_key_id, qts, receiver_user_id, channel_id, actor_user_id,
|
||||||
|
date, previous_participant, new_participant`
|
||||||
|
|
||||||
|
func scanChannelParticipantEvent(row rowScanner) (domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
var ev domain.DeviceChannelParticipantEvent
|
||||||
|
var previousRaw, newRaw []byte
|
||||||
|
if err := row.Scan(
|
||||||
|
&ev.ReceiverAuthKeyID, &ev.Qts, &ev.ReceiverUserID, &ev.ChannelID, &ev.ActorUserID,
|
||||||
|
&ev.Date, &previousRaw, &newRaw,
|
||||||
|
); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, err
|
||||||
|
}
|
||||||
|
if len(previousRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(previousRaw, &ev.Previous); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("unmarshal previous participant: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(newRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(newRaw, &ev.Participant); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("unmarshal new participant: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChannelParticipantQueueStore) begin(ctx context.Context, op string) (pgx.Tx, error) {
|
||||||
|
beginner, ok := s.db.(txBeginner)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: db does not support transactions", op)
|
||||||
|
}
|
||||||
|
tx, err := beginner.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("begin %s: %w", op, err)
|
||||||
|
}
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendChannelParticipantEvent reserves the receiving device's next qts
|
||||||
|
// (shared secret_qts_watermarks sequence) and writes the event, in one
|
||||||
|
// transaction so the device's qts never has a hole.
|
||||||
|
func (s *ChannelParticipantQueueStore) AppendChannelParticipantEvent(ctx context.Context, ev domain.DeviceChannelParticipantEvent) (domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
tx, err := s.begin(ctx, "append channel participant event")
|
||||||
|
if err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, err
|
||||||
|
}
|
||||||
|
committed := false
|
||||||
|
defer func() {
|
||||||
|
if !committed {
|
||||||
|
_ = tx.Rollback(ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var qts int
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO secret_qts_watermarks (auth_key_id, reserved_qts)
|
||||||
|
VALUES ($1, 1)
|
||||||
|
ON CONFLICT (auth_key_id) DO UPDATE SET reserved_qts = secret_qts_watermarks.reserved_qts + 1, updated_at = now()
|
||||||
|
RETURNING reserved_qts`, ev.ReceiverAuthKeyID).Scan(&qts); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("reserve device qts: %w", err)
|
||||||
|
}
|
||||||
|
ev.Qts = qts
|
||||||
|
|
||||||
|
previousRaw, err := marshalJSON(ev.Previous, "null")
|
||||||
|
if err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("marshal previous participant: %w", err)
|
||||||
|
}
|
||||||
|
newRaw, err := marshalJSON(ev.Participant, "null")
|
||||||
|
if err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("marshal new participant: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO channel_participant_event_queue (receiver_auth_key_id, qts, receiver_user_id, channel_id, actor_user_id,
|
||||||
|
date, previous_participant, new_participant)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
|
ev.ReceiverAuthKeyID, ev.Qts, ev.ReceiverUserID, ev.ChannelID, ev.ActorUserID,
|
||||||
|
ev.Date, previousRaw, newRaw); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("insert channel participant event: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return domain.DeviceChannelParticipantEvent{}, fmt.Errorf("commit append channel participant event: %w", err)
|
||||||
|
}
|
||||||
|
committed = true
|
||||||
|
return ev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChannelParticipantQueueStore) ListChannelParticipantEventsSince(ctx context.Context, receiverAuthKeyID int64, sinceQts, limit int) ([]domain.DeviceChannelParticipantEvent, error) {
|
||||||
|
if limit <= 0 || limit > 1001 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
rows, err := s.db.Query(ctx,
|
||||||
|
`SELECT `+channelParticipantEventColumns+` FROM channel_participant_event_queue
|
||||||
|
WHERE receiver_auth_key_id = $1 AND qts > $2 ORDER BY qts ASC LIMIT $3`,
|
||||||
|
receiverAuthKeyID, sinceQts, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list channel participant events: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []domain.DeviceChannelParticipantEvent
|
||||||
|
for rows.Next() {
|
||||||
|
ev, err := scanChannelParticipantEvent(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, ev)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChannelParticipantQueueStore) AckChannelParticipantEvents(ctx context.Context, receiverAuthKeyID int64, maxQts int) error {
|
||||||
|
if maxQts <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := s.db.Exec(ctx, `
|
||||||
|
UPDATE channel_participant_event_queue SET acked = true
|
||||||
|
WHERE receiver_auth_key_id = $1 AND qts <= $2 AND NOT acked`, receiverAuthKeyID, maxQts); err != nil {
|
||||||
|
return fmt.Errorf("ack channel participant events: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -59,3 +59,20 @@ type EncryptedQueueStore interface {
|
||||||
// GetEncryptedFile 按 id + access_hash 回查文件快照(inputEncryptedFile 复用路径)。
|
// GetEncryptedFile 按 id + access_hash 回查文件快照(inputEncryptedFile 复用路径)。
|
||||||
GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error)
|
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 行为 acked(confirmed_qts 由
|
||||||
|
// AckEncryptedMessages 在同一张水位表上推进,这里只做本表的 GC 标记)。
|
||||||
|
AckChannelParticipantEvents(ctx context.Context, receiverAuthKeyID int64, maxQts int) error
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue