chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,404 @@
package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
)
func (r *Router) onUpdatesGetChannelDifference(ctx context.Context, req *tg.UpdatesGetChannelDifferenceRequest) (tg.UpdatesChannelDifferenceClass, error) {
if r.deps.Channels == nil {
return &tg.UpdatesChannelDifferenceEmpty{Final: true, Pts: req.Pts, Timeout: 30}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
// difference 类 catch-up FLOOD_WAIT(设计 Phase 2 / §10.3):nudge 被消费后客户端会触发
// getChannelDifference,大群 nudge 全速前需限速防风暴。未配置阈值时不限速。
if err := r.checkCatchupRateLimit(ctx, userID, channelDifferenceRateLimitKeyPrefix); err != nil {
return nil, err
}
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
if err != nil {
return nil, err
}
r.trackChannelInterest(ctx, userID, channelID)
diff, err := r.deps.Channels.GetDifference(ctx, userID, domain.ChannelDifferenceRequest{
UserID: userID,
ChannelID: channelID,
Pts: req.Pts,
Limit: req.Limit,
Force: req.Force,
})
if err != nil {
if errors.Is(err, domain.ErrPersistentTimestamp) {
return nil, persistentTimestampInvalidErr()
}
return nil, channelInvalidErr(err)
}
diff = r.enrichChannelDifference(ctx, userID, diff)
return tgChannelDifference(userID, diff), nil
}
func (r *Router) channelOperationUpdates(ctx context.Context, viewerUserID int64, res domain.CreateChannelResult) *tg.Updates {
return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, res, newViewerPeerCache(r))
}
func (r *Router) channelOperationUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, res domain.CreateChannelResult, cache *viewerPeerCache) *tg.Updates {
if cache == nil {
cache = newViewerPeerCache(r)
}
userIDs := make(map[int64]struct{}, len(res.Members)+4)
channelIDs := make(map[int64]struct{})
if res.Channel.CreatorUserID != 0 {
userIDs[res.Channel.CreatorUserID] = struct{}{}
}
collectChannelUpdatePeerRefs(res.Event, res.Channel.ID, userIDs, channelIDs)
collectChannelMessagePeerRefs(res.Message, res.Channel.ID, userIDs, channelIDs)
for _, member := range res.Members {
if member.UserID != 0 {
userIDs[member.UserID] = struct{}{}
}
if member.InviterUserID != 0 {
userIDs[member.InviterUserID] = struct{}{}
}
}
updates := make([]tg.UpdateClass, 0, 2)
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
if res.Channel.ID != 0 {
updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID})
}
chats := []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, channelMemberForUser(res.Members, viewerUserID))}
chats = append(chats, tgChannels(viewerUserID, cache.channelsForIDs(ctx, viewerUserID, peerIDsExcept(peerIDMapKeys(channelIDs), res.Channel.ID)))...)
return &tg.Updates{
Updates: updates,
Users: tgUsersForViewer(viewerUserID, cache.usersForIDs(ctx, viewerUserID, peerIDMapKeys(userIDs))),
Chats: chats,
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) tdesktopCreateChatUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, res domain.CreateChannelResult, cache *viewerPeerCache) *tg.Updates {
updates := r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, res, cache)
if updates == nil {
return updates
}
self := channelMemberForUser(res.Members, viewerUserID)
legacy := tgMigratedLegacyChat(viewerUserID, res.Channel, self)
if legacy == nil {
return updates
}
chats := make([]tg.ChatClass, 0, len(updates.Chats)+1)
chats = append(chats, legacy)
chats = append(chats, updates.Chats...)
updates.Chats = chats
return updates
}
func (r *Router) channelStateUpdates(viewerUserID int64, channel domain.Channel) *tg.Updates {
return &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateChannel{ChannelID: channel.ID}},
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
// appendChannelStateUpdates 把 extra 的 update/chat 合并进 dst(chats 按 id 去重)。用于在一次响应里
// 叠加关联频道的状态更新,例如随母频道删除一并下发的 monoforum ChannelForbidden 墓碑。
func appendChannelStateUpdates(dst *tg.Updates, extra *tg.Updates) {
if dst == nil || extra == nil {
return
}
dst.Updates = append(dst.Updates, extra.Updates...)
for _, ch := range extra.Chats {
dst.Chats = appendUniqueTGChats(dst.Chats, ch)
}
}
func (r *Router) channelStateUpdatesWithLinkedMonoforum(viewerUserID int64, channel domain.Channel, mono domain.Channel, includeMono bool) *tg.Updates {
updates := r.channelStateUpdates(viewerUserID, channel)
// 母广播频道有/曾有关联 monoforum 时,按完整(非 min)形态下发。关闭 Direct Messages 时
// linked_monoforum_id 在投影里被隐藏,只有完整频道对象才能覆盖客户端缓存里旧的 linked_monoforum_id,
// 触发 applyMonoforumLinkedId(parent,0) → monoforum 的 MonoforumDisabled → 「已停用私信」停用页脚;
// min 母频道不覆盖缓存字段,页脚出不来。与官方 disable(parent 完整下发 + 隐藏 linked id)一致。
// 普通频道(无 linked monoforum)仍走 min,不受影响。
if channel.LinkedMonoforumID != 0 {
updates.Chats = []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)}
}
if includeMono {
updates.Chats = appendUniqueTGChats(updates.Chats, tgChannelChat(viewerUserID, mono, nil))
}
return updates
}
func (r *Router) linkedMonoforumForChannelState(ctx context.Context, userID int64, channel domain.Channel) (domain.Channel, bool) {
if r.deps.Channels == nil || userID == 0 || !channel.BroadcastMessagesAllowed || channel.LinkedMonoforumID == 0 {
return domain.Channel{}, false
}
mono, err := r.deps.Channels.GetJoinableChannel(ctx, userID, channel.LinkedMonoforumID)
if err != nil || !mono.Monoforum || mono.LinkedMonoforumID != channel.ID {
return domain.Channel{}, false
}
return mono, true
}
func (r *Router) channelMessageUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, res domain.SendChannelMessageResult, randomID int64, cache *viewerPeerCache) *tg.Updates {
randomIDs := []int64(nil)
includeMessageIDs := randomID != 0
if includeMessageIDs {
randomIDs = []int64{randomID}
}
return r.channelMessagesUpdatesWithPeerCache(ctx, viewerUserID, []domain.SendChannelMessageResult{res}, randomIDs, includeMessageIDs, nil, cache)
}
func (r *Router) pushChannelDiscussionUpdate(ctx context.Context, originUserID int64, discussion *domain.SendChannelDiscussionResult) {
if discussion == nil || discussion.Channel.ID == 0 || discussion.Event.Pts == 0 {
return
}
res := domain.SendChannelMessageResult{
Channel: discussion.Channel,
Message: discussion.Message,
Event: discussion.Event,
Recipients: discussion.Recipients,
MentionUserIDs: discussion.MentionUserIDs,
}
// 讨论组联动(broadcast↔linked megagroup)的第二轮 fan-out 也异步化 + 跨 viewer 投影预热
// (设计 Phase 0/Phase 1)。cache 仅被本 fan-out 闭包/预热使用、由单分片 worker 串行执行,
// 无跨 goroutine 竞态。
r.enqueueChannelMessageFanout(ctx, originUserID, res, nil)
}
func (r *Router) channelMessagesUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, results []domain.SendChannelMessageResult, randomIDs []int64, includeMessageIDs bool, extraUserIDs []int64, cache *viewerPeerCache) *tg.Updates {
if cache == nil {
cache = newViewerPeerCache(r)
}
updates := make([]tg.UpdateClass, 0, len(results)*2)
userIDs := make(map[int64]struct{}, len(results)+len(extraUserIDs))
for _, id := range extraUserIDs {
if id != 0 {
userIDs[id] = struct{}{}
}
}
channelIDs := make(map[int64]struct{}, len(results))
var channel domain.Channel
date := 0
for i, res := range results {
if res.Channel.ID != 0 {
channel = res.Channel
}
if includeMessageIDs && res.Message.ID != 0 && i < len(randomIDs) && randomIDs[i] != 0 {
updates = append(updates, &tg.UpdateMessageID{ID: res.Message.ID, RandomID: randomIDs[i]})
}
if res.Event.Pts != 0 {
event := projectChannelMentionForViewer(res.Event, res.MentionUserIDs, viewerUserID)
if update := tgChannelUpdate(viewerUserID, event); update != nil {
updates = append(updates, update)
}
}
collectChannelUpdatePeerRefs(res.Event, res.Channel.ID, userIDs, channelIDs)
collectChannelMessagePeerRefs(res.Message, res.Channel.ID, userIDs, channelIDs)
if date == 0 {
date = res.Event.Date
}
if date == 0 {
date = res.Message.Date
}
}
chats := []tg.ChatClass(nil)
if channel.ID != 0 {
chats = []tg.ChatClass{tgChannelChatMin(viewerUserID, channel)}
}
chats = append(chats, tgChannels(viewerUserID, cache.channelsForIDs(ctx, viewerUserID, peerIDsExcept(peerIDMapKeys(channelIDs), channel.ID)))...)
if date == 0 {
date = int(r.clock.Now().Unix())
}
return &tg.Updates{
Updates: updates,
Users: tgUsersForViewer(viewerUserID, cache.usersForIDs(ctx, viewerUserID, peerIDMapKeys(userIDs))),
Chats: chats,
Date: date,
Seq: 0,
}
}
func (r *Router) channelEditMessageUpdates(ctx context.Context, viewerUserID int64, res domain.EditChannelMessageResult) *tg.Updates {
return r.channelEditMessageUpdatesWithPeerCache(ctx, viewerUserID, res, newViewerPeerCache(r))
}
func (r *Router) channelEditMessageUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, res domain.EditChannelMessageResult, cache *viewerPeerCache) *tg.Updates {
if cache == nil {
cache = newViewerPeerCache(r)
}
updates := make([]tg.UpdateClass, 0, 2)
userIDs := make(map[int64]struct{}, 2)
channelIDs := make(map[int64]struct{})
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
collectChannelUpdatePeerRefs(res.Event, res.Channel.ID, userIDs, channelIDs)
collectChannelMessagePeerRefs(res.Message, res.Channel.ID, userIDs, channelIDs)
}
if res.ServiceEvent.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.ServiceEvent); update != nil {
updates = append(updates, update)
}
collectChannelUpdatePeerRefs(res.ServiceEvent, res.Channel.ID, userIDs, channelIDs)
collectChannelMessagePeerRefs(res.ServiceMessage, res.Channel.ID, userIDs, channelIDs)
}
chats := []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)}
chats = append(chats, tgChannels(viewerUserID, cache.channelsForIDs(ctx, viewerUserID, peerIDsExcept(peerIDMapKeys(channelIDs), res.Channel.ID)))...)
return &tg.Updates{
Updates: updates,
Users: tgUsersForViewer(viewerUserID, cache.usersForIDs(ctx, viewerUserID, peerIDMapKeys(userIDs))),
Chats: chats,
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) channelDeleteMessagesUpdates(viewerUserID int64, channel domain.Channel, event domain.ChannelUpdateEvent) *tg.Updates {
updates := make([]tg.UpdateClass, 0, 1)
if update := tgChannelUpdate(viewerUserID, event); update != nil {
updates = append(updates, update)
}
return &tg.Updates{
Updates: updates,
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) channelAvailableMessagesUpdates(viewerUserID int64, channel domain.Channel, availableMinID int) *tg.Updates {
updates := make([]tg.UpdateClass, 0, 1)
if channel.ID != 0 && availableMinID > 0 {
updates = append(updates, &tg.UpdateChannelAvailableMessages{
ChannelID: channel.ID,
AvailableMinID: availableMinID,
})
}
return &tg.Updates{
Updates: updates,
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
// projectChannelMentionForViewer 把全局消息快照按接收者投影 viewer-specific
// 的 mentioned/media_unread:被 @ 的在线成员必须在实时推送里就看到 @ 角标,
// 不能等到离线差量或重开会话才出现。
func projectChannelMentionForViewer(event domain.ChannelUpdateEvent, mentionUserIDs []int64, viewerUserID int64) domain.ChannelUpdateEvent {
if viewerUserID == 0 || event.Message.ID == 0 || viewerUserID == event.Message.SenderUserID {
return event
}
for _, id := range mentionUserIDs {
if id != viewerUserID {
continue
}
// 客户端的未读提及判定要求 mentioned 与 media_unread 同时置位,
// 与消息是否含媒体无关。
event.Message.Mentioned = true
event.Message.MediaUnread = true
return event
}
return event
}
type channelUpdatesBuilder func(viewerUserID int64) *tg.Updates
func (r *Router) pushChannelReadOutboxUpdates(ctx context.Context, channelID int64, updates []domain.ChannelReadOutboxUpdate) {
if r.deps.Sessions == nil || channelID == 0 || len(updates) == 0 {
return
}
seen := make(map[int64]int, len(updates))
for _, update := range updates {
if update.UserID == 0 || update.MaxID <= 0 {
continue
}
if seen[update.UserID] < update.MaxID {
seen[update.UserID] = update.MaxID
}
}
date := int(r.clock.Now().Unix())
for userID, maxID := range seen {
r.pushUserUpdates(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateReadChannelOutbox{ChannelID: channelID, MaxID: maxID}},
Date: date,
Seq: 0,
})
}
}
func (r *Router) pushChannelUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
r.pushChannelUpdatesWithScope(ctx, channelFanoutMembers, originUserID, channelID, recipients, build)
}
func (r *Router) pushChannelViewerUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
r.pushChannelUpdatesWithScope(ctx, channelFanoutViewers, originUserID, channelID, recipients, build)
}
func (r *Router) pushChannelExplicitUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
r.pushChannelUpdatesWithScope(ctx, channelFanoutExplicit, originUserID, channelID, recipients, build)
}
func (r *Router) pushChannelUpdatesWithScope(ctx context.Context, scope channelFanoutScope, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
if r.deps.Sessions == nil || build == nil {
return
}
explicit := recipients
recipients = r.channelFanoutRecipients(ctx, scope, channelID, recipients)
r.log.Debug("push channel updates fanout",
zap.Int64("channel_id", channelID),
zap.Int("scope", int(scope)),
zap.Int64("origin_user_id", originUserID),
zap.Int64s("explicit", explicit),
zap.Int64s("recipients", recipients),
)
seen := make(map[int64]struct{}, len(recipients))
pushed := false
for _, userID := range recipients {
if userID == 0 {
continue
}
if _, ok := seen[userID]; ok {
continue
}
seen[userID] = struct{}{}
updates := build(userID)
if updates == nil {
continue
}
r.pushUserUpdates(ctx, userID, updates)
pushed = true
}
if !pushed && originUserID != 0 {
updates := build(originUserID)
if updates == nil {
return
}
r.pushUserUpdates(ctx, originUserID, updates)
}
}
func tgEmptyUpdates(date int) *tg.Updates {
return &tg.Updates{
Updates: []tg.UpdateClass{},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: date,
Seq: 0,
}
}