493 lines
18 KiB
Go
493 lines
18 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"sort"
|
||
|
||
"github.com/iamxvbaba/td/tg"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// resolveMonoforumForAdmin 解析 TDesktop messages.getSavedDialogs/getSavedHistory 的 parent_peer,
|
||
// 并校验当前用户可管理母广播频道的 Direct Messages。TDesktop 的 SavedSublist 实际会把
|
||
// parentChat()->input() 作为 parent_peer;根据客户端 materialize 路径,它既可能是 monoforum
|
||
// 虚拟频道,也可能是与之关联的母广播频道。因此这里把两种 wire peer 归一到同一个 monoforum,
|
||
// 授权仍只认母频道的 creator / ManageDirectMessages,绝不能把普通 admin 放进管理者视图。
|
||
//
|
||
// 返回 (monoforum频道, isMonoforum, err):parent 是有效但未关联 Direct Messages 的普通频道时
|
||
// 返回 (零, false, nil),由调用方保留良性空响应;关联频道的非管理者返回 CHAT_ADMIN_REQUIRED。
|
||
func (r *Router) resolveMonoforumForAdmin(ctx context.Context, userID int64, parent domain.Peer) (domain.Channel, bool, error) {
|
||
if r.deps.Channels == nil {
|
||
return domain.Channel{}, false, notImplementedErr()
|
||
}
|
||
if parent.Type != domain.PeerTypeChannel || parent.ID == 0 {
|
||
return domain.Channel{}, false, parentPeerInvalidErr()
|
||
}
|
||
mono, isAdmin, err := r.deps.Channels.ResolveMonoforumSend(ctx, userID, parent.ID)
|
||
if errors.Is(err, domain.ErrChannelInvalid) {
|
||
// TDesktop 当前的 Direct Messages subsection 会传母广播频道。只接受显式的
|
||
// linked_monoforum 关系,不能把任意普通频道猜成 monoforum。
|
||
views, viewErr := r.deps.Channels.GetChannels(ctx, userID, []int64{parent.ID})
|
||
if viewErr != nil {
|
||
return domain.Channel{}, false, internalErr()
|
||
}
|
||
if len(views) != 1 {
|
||
return domain.Channel{}, false, nil
|
||
}
|
||
parentChannel := views[0].Channel
|
||
if parentChannel.ID != parent.ID || parentChannel.Deleted || parentChannel.Monoforum || parentChannel.LinkedMonoforumID == 0 {
|
||
return domain.Channel{}, false, nil
|
||
}
|
||
mono, isAdmin, err = r.deps.Channels.ResolveMonoforumSend(ctx, userID, parentChannel.LinkedMonoforumID)
|
||
if err != nil {
|
||
// A visible parent that advertises linked_monoforum_id but cannot resolve
|
||
// that target violates the durable channel-link invariant. Do not disguise
|
||
// it as an ordinary channel probe.
|
||
return domain.Channel{}, false, internalErr()
|
||
}
|
||
}
|
||
if err != nil {
|
||
if errors.Is(err, domain.ErrChannelInvalid) {
|
||
return domain.Channel{}, false, nil
|
||
}
|
||
return domain.Channel{}, false, internalErr()
|
||
}
|
||
if !isAdmin {
|
||
// 是 monoforum,但调用者不是母频道管理员:读私信列表/历史仅限管理员。
|
||
return domain.Channel{}, false, tgerr400("CHAT_ADMIN_REQUIRED")
|
||
}
|
||
return mono, true, nil
|
||
}
|
||
|
||
// monoforumSavedDialogs 返回 monoforum 的订阅者子会话列表(管理员视角的私信列表)。mono 已解析+鉴权。
|
||
func (r *Router) monoforumSavedDialogs(ctx context.Context, userID int64, mono domain.Channel, limit, offsetID int) (tg.MessagesSavedDialogsClass, error) {
|
||
list, err := r.deps.Channels.ListMonoforumDialogs(ctx, domain.MonoforumDialogsFilter{
|
||
MonoforumID: mono.ID,
|
||
Limit: limit,
|
||
OffsetID: offsetID,
|
||
})
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
dialogs := make([]tg.SavedDialogClass, 0, len(list.Dialogs))
|
||
for _, d := range list.Dialogs {
|
||
peer := tgPeer(d.SavedPeer)
|
||
if peer == nil {
|
||
continue
|
||
}
|
||
md := &tg.MonoForumDialog{
|
||
Peer: peer,
|
||
TopMessage: d.TopMessageID,
|
||
ReadInboxMaxID: d.ReadInboxMaxID,
|
||
ReadOutboxMaxID: d.ReadOutboxMaxID,
|
||
UnreadCount: d.UnreadCount,
|
||
}
|
||
dialogs = append(dialogs, md)
|
||
}
|
||
messages := make([]tg.MessageClass, 0, len(list.Messages))
|
||
for _, m := range list.Messages {
|
||
if item := tgChannelMessage(userID, m); item != nil {
|
||
messages = append(messages, item)
|
||
}
|
||
}
|
||
users, err := r.monoforumSubscriberUsersWithPeerCacheAndOverlaysStrict(ctx, userID, list.Dialogs, list.Messages, nil, nil)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.MessagesSavedDialogs{
|
||
Dialogs: dialogs,
|
||
Messages: messages,
|
||
Chats: r.monoforumChats(ctx, userID, mono),
|
||
Users: users,
|
||
}, nil
|
||
}
|
||
|
||
// monoforumSavedHistory 返回某订阅者在 monoforum 内的私信历史。mono 已解析+鉴权。
|
||
func (r *Router) monoforumSavedHistory(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, limit, offsetID int) (tg.MessagesMessagesClass, error) {
|
||
if savedPeer.ID == 0 {
|
||
return nil, peerIDInvalidErr()
|
||
}
|
||
hist, err := r.deps.Channels.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{
|
||
MonoforumID: mono.ID,
|
||
SavedPeer: savedPeer,
|
||
Limit: limit,
|
||
OffsetID: offsetID,
|
||
})
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
messages := make([]tg.MessageClass, 0, len(hist.Messages))
|
||
for _, m := range hist.Messages {
|
||
if item := tgChannelMessage(userID, m); item != nil {
|
||
messages = append(messages, item)
|
||
}
|
||
}
|
||
users, err := r.monoforumSubscriberUsersWithPeerCacheAndOverlaysStrict(ctx, userID, nil, hist.Messages, nil, nil)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
result := &tg.MessagesMessagesSlice{
|
||
Count: hist.Count,
|
||
Messages: messages,
|
||
Chats: r.monoforumChats(ctx, userID, mono),
|
||
Users: users,
|
||
}
|
||
r.applyPeerReadModelsToMessages(ctx, userID, result)
|
||
return result, nil
|
||
}
|
||
|
||
// monoforumChats 投影客户端 materialize monoforum 私信所需的频道:monoforum 自身直接投影,
|
||
// 再按 viewer 补母广播频道。订阅者没有 monoforum member row,管理员身份也只来自母频道。
|
||
func (r *Router) monoforumChats(ctx context.Context, userID int64, mono domain.Channel) []tg.ChatClass {
|
||
chats := []tg.ChatClass{tgChannelChatForView(userID, domain.ChannelView{Channel: mono})}
|
||
if mono.LinkedMonoforumID != 0 && r.deps.Channels != nil {
|
||
if views, err := r.deps.Channels.GetChannels(ctx, userID, []int64{mono.LinkedMonoforumID}); err == nil {
|
||
for _, view := range views {
|
||
if view.Channel.ID != 0 {
|
||
chats = append(chats, tgChannelChatForView(userID, view))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return chats
|
||
}
|
||
|
||
type monoforumPeerOverlays struct {
|
||
usernames map[domain.Peer][]domain.Username
|
||
botProfiles map[int64]domain.BotProfile
|
||
botVerifications map[domain.Peer]domain.CustomVerification
|
||
}
|
||
|
||
func monoforumSubscriberUserIDs(dialogs []domain.MonoforumDialog, messages []domain.ChannelMessage) []int64 {
|
||
ids := make([]int64, 0, len(dialogs)+len(messages))
|
||
seen := map[int64]struct{}{}
|
||
add := func(id int64) {
|
||
if id == 0 {
|
||
return
|
||
}
|
||
if _, ok := seen[id]; ok {
|
||
return
|
||
}
|
||
seen[id] = struct{}{}
|
||
ids = append(ids, id)
|
||
}
|
||
for _, d := range dialogs {
|
||
if d.SavedPeer.Type == domain.PeerTypeUser {
|
||
add(d.SavedPeer.ID)
|
||
}
|
||
}
|
||
for _, m := range messages {
|
||
add(m.SenderUserID)
|
||
}
|
||
// tgChannelMessage can reference users beyond its sender (from/send_as,
|
||
// forward, via_bot, reply/quote mention, contact/poll/todo/action/reaction).
|
||
// Reuse the common channel-message closure so saved history, send echo and
|
||
// online fan-out all materialize the same complete Users envelope.
|
||
userIDs := make(map[int64]struct{})
|
||
channelIDs := make(map[int64]struct{})
|
||
for _, message := range messages {
|
||
collectChannelMessagePeerRefs(message, message.ChannelID, userIDs, channelIDs)
|
||
}
|
||
extra := peerIDMapKeys(userIDs)
|
||
sort.Slice(extra, func(i, j int) bool { return extra[i] < extra[j] })
|
||
for _, id := range extra {
|
||
add(id)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func monoforumProjectionPeers(monoforumID, parentID int64, userIDs []int64) []domain.Peer {
|
||
peers := make([]domain.Peer, 0, len(userIDs)+2)
|
||
seen := make(map[domain.Peer]struct{}, len(userIDs)+2)
|
||
add := func(peer domain.Peer) {
|
||
if peer.ID == 0 {
|
||
return
|
||
}
|
||
if _, ok := seen[peer]; ok {
|
||
return
|
||
}
|
||
seen[peer] = struct{}{}
|
||
peers = append(peers, peer)
|
||
}
|
||
for _, userID := range userIDs {
|
||
add(domain.Peer{Type: domain.PeerTypeUser, ID: userID})
|
||
}
|
||
add(domain.Peer{Type: domain.PeerTypeChannel, ID: monoforumID})
|
||
add(domain.Peer{Type: domain.PeerTypeChannel, ID: parentID})
|
||
return peers
|
||
}
|
||
|
||
// loadMonoforumPeerOverlays resolves peer-wide facts once before a fan-out. The
|
||
// returned snapshot is immutable for the lifetime of that job and can therefore
|
||
// be reused by every viewer builder without turning overlays into N+1 reads.
|
||
func (r *Router) loadMonoforumPeerOverlays(ctx context.Context, peers []domain.Peer) *monoforumPeerOverlays {
|
||
overlays := &monoforumPeerOverlays{
|
||
usernames: r.usernameRegistryMap(ctx, peers),
|
||
botVerifications: r.botVerificationMap(ctx, peers),
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return overlays
|
||
}
|
||
userIDs := make([]int64, 0, len(peers))
|
||
for _, peer := range peers {
|
||
if peer.Type == domain.PeerTypeUser && peer.ID != 0 {
|
||
userIDs = append(userIDs, peer.ID)
|
||
}
|
||
}
|
||
if len(userIDs) == 0 {
|
||
return overlays
|
||
}
|
||
if batch, ok := r.deps.Bots.(botProfileBatchResolver); ok {
|
||
if profiles, err := batch.BotInfos(ctx, userIDs); err == nil {
|
||
overlays.botProfiles = profiles
|
||
return overlays
|
||
}
|
||
}
|
||
overlays.botProfiles = make(map[int64]domain.BotProfile)
|
||
for _, userID := range userIDs {
|
||
if profile, found, err := r.deps.Bots.BotInfo(ctx, userID); err == nil && found {
|
||
overlays.botProfiles[userID] = profile
|
||
}
|
||
}
|
||
return overlays
|
||
}
|
||
|
||
func applyMonoforumPeerOverlays(users []tg.UserClass, chats []tg.ChatClass, overlays *monoforumPeerOverlays) {
|
||
if overlays == nil {
|
||
return
|
||
}
|
||
applyUsernamesFromRegistry(users, chats, overlays.usernames)
|
||
for _, item := range users {
|
||
u, ok := item.(*tg.User)
|
||
if !ok || u == nil || u.Deleted {
|
||
continue
|
||
}
|
||
if u.Bot {
|
||
if profile, found := overlays.botProfiles[u.ID]; found {
|
||
applyBotProfileFlags(u, profile)
|
||
}
|
||
}
|
||
if mark, found := overlays.botVerifications[domain.Peer{Type: domain.PeerTypeUser, ID: u.ID}]; found && mark.IconDocumentID > 0 {
|
||
u.SetBotVerificationIcon(mark.IconDocumentID)
|
||
}
|
||
}
|
||
for _, item := range chats {
|
||
ch, ok := item.(*tg.Channel)
|
||
if !ok || ch == nil {
|
||
continue
|
||
}
|
||
if mark, found := overlays.botVerifications[domain.Peer{Type: domain.PeerTypeChannel, ID: ch.ID}]; found && mark.IconDocumentID > 0 {
|
||
ch.SetBotVerificationIcon(mark.IconDocumentID)
|
||
}
|
||
}
|
||
}
|
||
|
||
// monoforumSubscriberUsers projects subscriber users (saved_peer + message
|
||
// senders) for one viewer. Fan-out callers pass their preheated peer cache and
|
||
// overlay snapshot; single-viewer callers retain the same output shape through a
|
||
// one-shot local cache. The pure TL conversion is viewer-aware so self is never
|
||
// lost for a subscriber viewing their own envelope.
|
||
func (r *Router) monoforumSubscriberUsersWithPeerCacheAndOverlaysStrict(ctx context.Context, userID int64, dialogs []domain.MonoforumDialog, messages []domain.ChannelMessage, cache *viewerPeerCache, overlays *monoforumPeerOverlays) ([]tg.UserClass, error) {
|
||
ids := monoforumSubscriberUserIDs(dialogs, messages)
|
||
if len(ids) == 0 {
|
||
return []tg.UserClass{}, nil
|
||
}
|
||
if cache == nil {
|
||
cache = newViewerPeerCache(r)
|
||
}
|
||
projected, err := cache.usersForIDsStrict(ctx, userID, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if overlays == nil {
|
||
overlays = r.loadMonoforumPeerOverlays(ctx, monoforumProjectionPeers(0, 0, ids))
|
||
}
|
||
out := tgUsersForViewer(userID, projected)
|
||
applyMonoforumPeerOverlays(out, nil, overlays)
|
||
return out, nil
|
||
}
|
||
|
||
// monoforumReplyPresent 判断 sendMessage 的 reply_to 是否显式携带 monoforum_peer_id。
|
||
// 管理员回复必须带目标订阅者;普通订阅者按官方 TDesktop 行为不携带 reply_to,目标由调用者推导。
|
||
func monoforumReplyPresent(input tg.InputReplyToClass) bool {
|
||
switch v := input.(type) {
|
||
case *tg.InputReplyToMonoForum:
|
||
return v != nil
|
||
case *tg.InputReplyToMessage:
|
||
if v == nil {
|
||
return false
|
||
}
|
||
_, ok := v.GetMonoforumPeerID()
|
||
return ok
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// monoforumReplyTargetPeer 从 reply_to 的 monoforum_peer_id 解析订阅者子会话 peer。
|
||
func (r *Router) monoforumReplyTargetPeer(userID int64, input tg.InputReplyToClass) (domain.Peer, bool) {
|
||
var inputPeer tg.InputPeerClass
|
||
switch v := input.(type) {
|
||
case *tg.InputReplyToMonoForum:
|
||
if v != nil {
|
||
inputPeer = v.MonoforumPeerID
|
||
}
|
||
case *tg.InputReplyToMessage:
|
||
if v != nil {
|
||
if p, ok := v.GetMonoforumPeerID(); ok {
|
||
inputPeer = p
|
||
}
|
||
}
|
||
}
|
||
if inputPeer == nil {
|
||
return domain.Peer{}, false
|
||
}
|
||
return r.domainPeerFromInputPeer(userID, inputPeer)
|
||
}
|
||
|
||
func (r *Router) monoforumSavedPeerForSender(userID int64, isAdmin bool, replyTo tg.InputReplyToClass) (domain.Peer, error) {
|
||
savedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
||
if monoforumReplyPresent(replyTo) {
|
||
var valid bool
|
||
savedPeer, valid = r.monoforumReplyTargetPeer(userID, replyTo)
|
||
if !valid || savedPeer.Type != domain.PeerTypeUser || savedPeer.ID == 0 {
|
||
return domain.Peer{}, replyToMonoforumPeerInvalidErr()
|
||
}
|
||
} else if isAdmin {
|
||
return domain.Peer{}, replyToMonoforumPeerInvalidErr()
|
||
}
|
||
if !isAdmin && savedPeer.ID != userID {
|
||
return domain.Peer{}, replyToMonoforumPeerInvalidErr()
|
||
}
|
||
return savedPeer, nil
|
||
}
|
||
|
||
// monoforumMessageReplyFromInput separates the sub-dialog selector from the actual message reply.
|
||
// InputReplyToMonoForum only selects a subscriber; InputReplyToMessage may carry both the selector
|
||
// and a real reply_to_msg_id, so clear flags.5 before reusing the common structural validator.
|
||
func (r *Router) monoforumMessageReplyFromInput(ctx context.Context, userID int64, peer domain.Peer, input tg.InputReplyToClass) (*domain.MessageReply, error) {
|
||
switch value := input.(type) {
|
||
case nil, *tg.InputReplyToMonoForum:
|
||
return nil, nil
|
||
case *tg.InputReplyToMessage:
|
||
if value == nil {
|
||
return nil, nil
|
||
}
|
||
clean := *value
|
||
clean.Flags.Unset(5)
|
||
clean.MonoforumPeerID = nil
|
||
return r.messageReplyFromInput(ctx, userID, peer, &clean)
|
||
default:
|
||
return r.messageReplyFromInput(ctx, userID, peer, input)
|
||
}
|
||
}
|
||
|
||
// sendMonoforumMessage 处理向频道私信(monoforum)发送:订阅者发到自己的子会话,管理员回复到目标订阅者。
|
||
// saved_peer 对订阅者由调用者推导、对管理员来自 reply_to;管理员可写任意订阅者子会话,订阅者只能写自己的。
|
||
func (r *Router) sendMonoforumMessage(ctx context.Context, userID int64, peer domain.Peer, mono domain.Channel, isAdmin bool, req domain.SendMonoforumMessageRequest) (tg.UpdatesClass, error) {
|
||
if r.deps.Channels == nil {
|
||
return nil, notImplementedErr()
|
||
}
|
||
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
||
return nil, peerIDInvalidErr()
|
||
}
|
||
if mono.ID != peer.ID || !mono.Monoforum || req.SavedPeer.Type != domain.PeerTypeUser || req.SavedPeer.ID == 0 {
|
||
return nil, replyToMonoforumPeerInvalidErr()
|
||
}
|
||
if !isAdmin && req.SavedPeer.ID != userID {
|
||
// 普通订阅者只能写自己的子会话,不能写他人的。
|
||
return nil, replyToMonoforumPeerInvalidErr()
|
||
}
|
||
req.MonoforumID = mono.ID
|
||
req.SenderUserID = userID
|
||
if req.Date == 0 {
|
||
req.Date = int(r.clock.Now().Unix())
|
||
}
|
||
res, err := r.deps.Channels.SendMonoforumMessage(ctx, req)
|
||
if err != nil {
|
||
return nil, messageSendErr(err)
|
||
}
|
||
if req.ClearDraft {
|
||
r.clearDraftAfterSend(ctx, userID, peer, req.ReplyTo)
|
||
}
|
||
if !res.Duplicate {
|
||
r.enqueueMonoforumMessageFanout(ctx, userID, mono, req.SavedPeer, res)
|
||
}
|
||
return r.monoforumSendUpdatesStrict(ctx, userID, mono, req.SavedPeer, res)
|
||
}
|
||
|
||
// monoforumSendUpdates 给发送者构造回声 Updates:updateMessageID(关联 random_id)+ updateNewChannelMessage
|
||
// (monoforum 走 channel pts)。另一方经 monoforum 频道的 getChannelDifference 收取该 durable 事件。
|
||
func (r *Router) monoforumSendUpdatesStrict(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, res domain.SendChannelMessageResult) (tg.UpdatesClass, error) {
|
||
return r.monoforumSendUpdatesWithPeerCacheAndOverlaysStrict(ctx, userID, mono, savedPeer, res, nil, nil)
|
||
}
|
||
|
||
func (r *Router) monoforumSendUpdatesWithPeerCacheAndOverlays(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, res domain.SendChannelMessageResult, cache *viewerPeerCache, overlays *monoforumPeerOverlays) tg.UpdatesClass {
|
||
updates, _ := r.monoforumSendUpdatesWithPeerCacheAndOverlaysStrict(ctx, userID, mono, savedPeer, res, cache, overlays)
|
||
return updates
|
||
}
|
||
|
||
func (r *Router) monoforumSendUpdatesWithPeerCacheAndOverlaysStrict(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, res domain.SendChannelMessageResult, cache *viewerPeerCache, overlays *monoforumPeerOverlays) (tg.UpdatesClass, error) {
|
||
updates := make([]tg.UpdateClass, 0, 3)
|
||
if res.Message.RandomID != 0 {
|
||
updates = append(updates, &tg.UpdateMessageID{ID: res.Message.ID, RandomID: res.Message.RandomID})
|
||
}
|
||
newMsg := &tg.UpdateNewChannelMessage{Pts: res.Event.Pts, PtsCount: res.Event.PtsCount}
|
||
if channelMsg := tgChannelMessage(userID, res.Message); channelMsg != nil {
|
||
newMsg.Message = channelMsg
|
||
} else {
|
||
newMsg.Message = &tg.MessageEmpty{ID: res.Message.ID}
|
||
}
|
||
updates = append(updates, newMsg)
|
||
date := int(r.clock.Now().Unix())
|
||
if res.Duplicate && res.ReplayDeleteEvent != nil {
|
||
if deleted := tgChannelUpdate(userID, *res.ReplayDeleteEvent); deleted != nil {
|
||
updates = append(updates, deleted)
|
||
}
|
||
if res.ReplayDeleteEvent.Date > date {
|
||
date = res.ReplayDeleteEvent.Date
|
||
}
|
||
}
|
||
dialogs := []domain.MonoforumDialog{{SavedPeer: savedPeer}}
|
||
messages := []domain.ChannelMessage{res.Message}
|
||
if cache == nil {
|
||
cache = newViewerPeerCache(r)
|
||
}
|
||
if overlays == nil {
|
||
ids := monoforumSubscriberUserIDs(dialogs, messages)
|
||
overlays = r.loadMonoforumPeerOverlays(ctx, monoforumProjectionPeers(mono.ID, mono.LinkedMonoforumID, ids))
|
||
}
|
||
chats := r.monoforumChats(ctx, userID, mono)
|
||
users, err := r.monoforumSubscriberUsersWithPeerCacheAndOverlaysStrict(ctx, userID, dialogs, messages, cache, overlays)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
applyMonoforumPeerOverlays(nil, chats, overlays)
|
||
return &tg.Updates{
|
||
Updates: updates,
|
||
Chats: chats,
|
||
Users: users,
|
||
Date: date,
|
||
}, nil
|
||
}
|
||
|
||
func (r *Router) monoforumDeliveryUpdates(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, res domain.SendChannelMessageResult) *tg.Updates {
|
||
return r.monoforumDeliveryUpdatesWithPeerCacheAndOverlays(ctx, userID, mono, savedPeer, res, nil, nil)
|
||
}
|
||
|
||
func (r *Router) monoforumDeliveryUpdatesWithPeerCacheAndOverlays(ctx context.Context, userID int64, mono domain.Channel, savedPeer domain.Peer, res domain.SendChannelMessageResult, cache *viewerPeerCache, overlays *monoforumPeerOverlays) *tg.Updates {
|
||
updates, _ := r.monoforumSendUpdatesWithPeerCacheAndOverlays(ctx, userID, mono, savedPeer, res, cache, overlays).(*tg.Updates)
|
||
if updates == nil {
|
||
return nil
|
||
}
|
||
filtered := make([]tg.UpdateClass, 0, len(updates.Updates))
|
||
for _, update := range updates.Updates {
|
||
if _, randomMapping := update.(*tg.UpdateMessageID); !randomMapping {
|
||
filtered = append(filtered, update)
|
||
}
|
||
}
|
||
updates.Updates = filtered
|
||
return updates
|
||
}
|