owpengram-server/internal/rpc/messages_history.go
2026-08-24 22:42:40 +03:00

1011 lines
33 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 rpc
import (
"context"
"errors"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"sort"
"telesrv/internal/domain"
"unicode/utf8"
)
func (r *Router) onMessagesSearchStickerSets(ctx context.Context, req *tg.MessagesSearchStickerSetsRequest) (tg.MessagesFoundStickerSetsClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if utf8.RuneCountInString(req.Q) > maxStickerSearchQLength {
return nil, limitInvalidErr()
}
if req.Hash != 0 {
return &tg.MessagesFoundStickerSetsNotModified{}, nil
}
return &tg.MessagesFoundStickerSets{
Hash: 0,
Sets: []tg.StickerSetCoveredClass{},
}, nil
}
// onMessagesSearchEmojiStickerSets 之前完全未注册,Android 的 emoji 分类点击/搜索会
// 无限重试这个方法(每次都拿 NOT_IMPLEMENTED,客户端不认为这是终态错误,一直重发),
// 表现为分类点击后结果区永远不出来。和 onMessagesSearchStickerSets 同策略,返回合法空结果。
func (r *Router) onMessagesSearchEmojiStickerSets(ctx context.Context, req *tg.MessagesSearchEmojiStickerSetsRequest) (tg.MessagesFoundStickerSetsClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if utf8.RuneCountInString(req.Q) > maxStickerSearchQLength {
return nil, limitInvalidErr()
}
if req.Hash != 0 {
return &tg.MessagesFoundStickerSetsNotModified{}, nil
}
return &tg.MessagesFoundStickerSets{
Hash: 0,
Sets: []tg.StickerSetCoveredClass{},
}, nil
}
func (r *Router) onMessagesSearchStickers(ctx context.Context, req *tg.MessagesSearchStickersRequest) (tg.MessagesFoundStickersClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if req.Offset < 0 || req.Offset > domain.MaxMessageBoxID || req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
if utf8.RuneCountInString(req.Q) > maxStickerSearchQLength || utf8.RuneCountInString(req.Emoticon) > maxStickerSearchQLength {
return nil, limitInvalidErr()
}
if len(req.LangCode) > maxStickerSearchLangs {
return nil, limitInvalidErr()
}
for _, lang := range req.LangCode {
if err := validateEmojiLangCode(lang); err != nil {
return nil, err
}
}
if req.Hash != 0 {
return &tg.MessagesFoundStickersNotModified{}, nil
}
return &tg.MessagesFoundStickers{
Hash: 0,
Stickers: []tg.DocumentClass{},
}, nil
}
func emoticonEmptyErr() error { return tgerr.New(400, "EMOTICON_EMPTY") }
// onMessagesSearchCustomEmoji 目前没有按 emoticon 反查自定义 emoji 的索引,故与
// onMessagesSearchStickers/onMessagesSearchStickerSets 同策略:返回合法的空结果而非
// NOT_IMPLEMENTED——之前完全未注册此方法时,Android 的 emoji 分类/搜索会拿到
// notImplementedErr(),表现为一直转圈(客户端没有为这个具体错误重置搜索 UI 状态)。
func (r *Router) onMessagesSearchCustomEmoji(ctx context.Context, req *tg.MessagesSearchCustomEmojiRequest) (tg.EmojiListClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if req.Emoticon == "" {
return nil, emoticonEmptyErr()
}
if utf8.RuneCountInString(req.Emoticon) > maxStickerSearchQLength {
return nil, limitInvalidErr()
}
if req.Hash != 0 {
return &tg.EmojiListNotModified{}, nil
}
return &tg.EmojiList{Hash: 0, DocumentID: []int64{}}, nil
}
func (r *Router) onMessagesGetSearchResultsCalendar(ctx context.Context, req *tg.MessagesGetSearchResultsCalendarRequest) (*tg.MessagesSearchResultsCalendar, error) {
if req.OffsetID < 0 || req.OffsetID > domain.MaxMessageBoxID || req.OffsetDate < 0 {
return nil, limitInvalidErr()
}
if err := r.validateSearchResultsPeer(ctx, req.Peer, req.GetSavedPeerID); err != nil {
return nil, err
}
minDate := req.OffsetDate
if minDate == 0 {
minDate = int(r.clock.Now().Unix())
}
return &tg.MessagesSearchResultsCalendar{
Count: 0,
MinDate: minDate,
MinMsgID: req.OffsetID,
Periods: []tg.SearchResultsCalendarPeriod{},
Messages: []tg.MessageClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesGetSearchResultsPositions(ctx context.Context, req *tg.MessagesGetSearchResultsPositionsRequest) (*tg.MessagesSearchResultsPositions, error) {
if req.OffsetID < 0 || req.OffsetID > domain.MaxMessageBoxID || req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
if err := r.validateSearchResultsPeer(ctx, req.Peer, req.GetSavedPeerID); err != nil {
return nil, err
}
return &tg.MessagesSearchResultsPositions{
Count: 0,
Positions: []tg.SearchResultPosition{},
}, nil
}
func (r *Router) validateSearchResultsPeer(ctx context.Context, peer tg.InputPeerClass, savedPeer func() (tg.InputPeerClass, bool)) error {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
return err
}
if savedPeer == nil {
return nil
}
if input, ok := savedPeer(); ok && input != nil {
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input); err != nil {
return err
}
}
return nil
}
func (r *Router) onMessagesGetMessagesViews(ctx context.Context, req *tg.MessagesGetMessagesViewsRequest) (*tg.MessagesMessageViews, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(req.ID) > maxGetMessagesIDs {
return nil, limitInvalidErr()
}
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
}
views := make([]tg.MessageViews, len(req.ID))
peer, peerErr := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if peerErr != nil {
return nil, peerErr
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil && len(req.ID) > 0 {
viewCounters, err := r.deps.Channels.GetMessageViews(ctx, userID, domain.ChannelMessageViewsRequest{
UserID: userID,
ChannelID: peer.ID,
IDs: req.ID,
Increment: req.Increment,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelInvalidErr(err)
}
for i, id := range req.ID {
if count, ok := viewCounters.Views[id]; ok {
views[i].SetViews(count)
}
if replies := tgChannelMessageReplies(viewCounters.Replies[id]); replies != nil {
views[i].SetReplies(*replies)
}
}
channels, users := r.messageViewPeerObjects(ctx, userID, viewCounters)
return r.applyStoryMaxIDsToMessageViews(ctx, userID, &tg.MessagesMessageViews{
Views: views,
Chats: tgChannels(userID, channels),
Users: r.tgUsersForViewer(userID, users),
}), nil
}
return r.applyStoryMaxIDsToMessageViews(ctx, userID, &tg.MessagesMessageViews{
Views: views,
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}), nil
}
func (r *Router) messageViewPeerObjects(ctx context.Context, viewerUserID int64, result domain.ChannelMessageViewsResult) ([]domain.Channel, []domain.User) {
channels := []domain.Channel{}
if result.Channel.ID != 0 {
channels = append(channels, result.Channel)
}
userIDs := make(map[int64]struct{})
channelIDs := make(map[int64]struct{})
for _, peer := range result.Peers {
addDomainPeerRef(peer, result.Channel.ID, userIDs, channelIDs)
}
for _, replies := range result.Replies {
if replies == nil {
continue
}
if replies.ChannelID != 0 && replies.ChannelID != result.Channel.ID {
channelIDs[replies.ChannelID] = struct{}{}
}
for _, peer := range replies.RecentRepliers {
addDomainPeerRef(peer, result.Channel.ID, userIDs, channelIDs)
}
}
removeKnownChannelRefs(channelIDs, channels)
cache := newViewerPeerCache(r)
channels = mergeDomainChannels(channels, cache.channelsForIDs(ctx, viewerUserID, mapKeys(channelIDs))...)
users := cache.usersForIDs(ctx, viewerUserID, mapKeys(userIDs))
return channels, users
}
func (r *Router) onMessagesGetSearchCounters(ctx context.Context, req *tg.MessagesGetSearchCountersRequest) ([]tg.MessagesSearchCounter, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(req.Filters) > maxMessageSearchFilters {
return nil, limitInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
needsPinned := false
needsMedia := false
for _, filter := range req.Filters {
if filter == nil {
continue
}
if _, ok := filter.(*tg.InputMessagesFilterPinned); ok {
needsPinned = true
continue
}
if len(mediaCategoriesForFilter(filter)) > 0 {
needsMedia = true
}
}
pinnedCount := 0
mediaCounts := domain.MediaCategoryCounts{}
if needsPinned {
switch {
case peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil:
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
ChannelID: peer.ID,
PinnedOnly: true,
NeedTotalCount: true,
CountOnly: true,
})
if err != nil {
return nil, err
}
pinnedCount = history.Count
case peer.Type == domain.PeerTypeUser && r.deps.Messages != nil:
list, err := r.deps.Messages.Search(ctx, userID, domain.MessageFilter{
HasPeer: true,
Peer: peer,
PinnedOnly: true,
Limit: 1,
NeedTotalCount: true,
})
if err != nil {
return nil, err
}
pinnedCount = list.Count
}
}
if needsMedia {
counts, err := r.mediaCountsForPeer(ctx, userID, peer)
if err != nil {
return nil, err
}
mediaCounts = counts
}
counters := make([]tg.MessagesSearchCounter, 0, len(req.Filters))
for _, filter := range req.Filters {
if filter == nil {
continue
}
count := 0
if _, ok := filter.(*tg.InputMessagesFilterPinned); ok {
count = pinnedCount
} else if categories := mediaCategoriesForFilter(filter); len(categories) > 0 {
count = mediaCounts.CountAny(categories)
}
counters = append(counters, tg.MessagesSearchCounter{Filter: filter, Count: count})
}
return counters, nil
}
func (r *Router) onMessagesGetReplies(ctx context.Context, req *tg.MessagesGetRepliesRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if err := validateHistoryBounds(req.OffsetID, req.AddOffset, req.Limit, req.MaxID, req.MinID); err != nil {
return nil, err
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
replies, err := r.deps.Channels.GetReplies(ctx, userID, domain.ChannelRepliesFilter{
ChannelID: peer.ID,
RootMessageID: req.MsgID,
OffsetID: req.OffsetID,
OffsetDate: req.OffsetDate,
AddOffset: req.AddOffset,
Limit: req.Limit,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
})
if err != nil {
return nil, channelInvalidErr(err)
}
if req.Hash != 0 && replies.Hash == req.Hash {
return &tg.MessagesMessagesNotModified{Count: replies.Count}, nil
}
return r.tgChannelHistoryMessages(ctx, userID, r.enrichChannelHistory(ctx, userID, replies)), nil
}
result := &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
func (r *Router) onMessagesGetDiscussionMessage(ctx context.Context, req *tg.MessagesGetDiscussionMessageRequest) (*tg.MessagesDiscussionMessage, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type != domain.PeerTypeChannel {
return nil, peerIDInvalidErr()
}
if r.deps.Channels == nil {
return &tg.MessagesDiscussionMessage{Chats: r.chatsForInputPeer(ctx, userID, req.Peer)}, nil
}
discussion, err := r.deps.Channels.GetDiscussionMessage(ctx, userID, peer.ID, req.MsgID)
if err != nil {
return nil, channelInvalidErr(err)
}
// 解析讨论帖/回复作者的 user/channel 实体store 不填 Users否则客户端拿不到
// 作者实体需额外 getUser 兜底。
discussion = r.enrichChannelDiscussion(ctx, userID, discussion)
return r.tgMessagesDiscussionMessage(ctx, userID, discussion), nil
}
func (r *Router) onMessagesReadDiscussion(ctx context.Context, req *tg.MessagesReadDiscussionRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID || req.ReadMaxID < 0 || req.ReadMaxID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if peer.Type != domain.PeerTypeChannel {
return false, peerIDInvalidErr()
}
if r.deps.Channels == nil {
return true, nil
}
now := int(r.clock.Now().Unix())
// forum 话题已读req.MsgID 是话题 id推进 per-topic 水位(不碰频道级,消除话题间已读串扰),
// 向自己其它设备下发 DiscussionInbox、向话题内发送者下发 DiscussionOutbox 回执。不经
// GetDiscussionMessage避免话题根消息被裁剪/删除时整条已读 400root 不存活不应阻塞标已读)。
topicRes, terr := r.deps.Channels.ReadTopicHistory(ctx, userID, domain.ReadChannelTopicHistoryRequest{
UserID: userID,
ChannelID: peer.ID,
TopicID: req.MsgID,
MaxID: req.ReadMaxID,
Date: now,
})
if terr == nil {
if topicRes.Changed {
if err := r.recordChannelDiscussionInbox(ctx, userID, peer.ID, topicRes.TopicID, topicRes.MaxID, topicRes.Pts); err != nil {
return false, err
}
r.pushChannelDiscussionOutboxUpdates(ctx, peer.ID, topicRes.TopicID, topicRes.OutboxUpdates)
}
// 保守叠加:同时推进频道级 inbox 水位,保持 getDialogs/getPeerDialogs 会话总未读不退化。
if read, rerr := r.deps.Channels.ReadHistory(ctx, userID, domain.ReadChannelHistoryRequest{
UserID: userID,
ChannelID: peer.ID,
MaxID: req.ReadMaxID,
Date: now,
}); rerr == nil {
if _, err := r.recordChannelReadInbox(ctx, userID, read); err != nil {
return false, err
}
r.pushChannelReadOutboxUpdates(ctx, read.ChannelID, read.OutboxUpdates)
}
return topicRes.Changed, nil
}
if !errors.Is(terr, domain.ErrChannelForumMissing) {
return false, channelInvalidErr(terr)
}
// 非 forum频道-讨论组 linked comments只解析 target/root/boundary禁止为了一个
// 已读请求加载完整 discussion message、reply stats、reactions 与 unread aggregates。
readChannelID := peer.ID
if provider, ok := r.deps.Channels.(interface {
ResolveDiscussionReadTarget(context.Context, int64, int64, int, int) (domain.ChannelDiscussionReadTarget, error)
}); ok {
target, resolveErr := provider.ResolveDiscussionReadTarget(ctx, userID, peer.ID, req.MsgID, req.ReadMaxID)
if resolveErr != nil {
return false, channelInvalidErr(resolveErr)
}
if target.AlreadyRead {
return false, nil
}
if target.Guest {
// Linked discussion guests have no channel_members/dialog row. Reading
// comments is therefore an authorized, durable-state-free no-op.
return false, nil
}
readChannelID = target.ChannelID
} else {
discussion, resolveErr := r.deps.Channels.GetDiscussionMessage(ctx, userID, peer.ID, req.MsgID)
if resolveErr != nil {
return false, channelInvalidErr(resolveErr)
}
if discussion.DiscussionChannel.ID != 0 {
readChannelID = discussion.DiscussionChannel.ID
}
}
read, err := r.deps.Channels.ReadHistory(ctx, userID, domain.ReadChannelHistoryRequest{
UserID: userID,
ChannelID: readChannelID,
MaxID: req.ReadMaxID,
Date: now,
})
if err != nil {
return false, channelInvalidErr(err)
}
if _, err := r.recordChannelReadInbox(ctx, userID, read); err != nil {
return false, err
}
r.pushChannelReadOutboxUpdates(ctx, read.ChannelID, read.OutboxUpdates)
return read.Changed, nil
}
func (r *Router) onMessagesGetOnlines(ctx context.Context, peer tg.InputPeerClass) (*tg.ChatOnlines, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
domainPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return nil, err
}
if domainPeer.Type == domain.PeerTypeChannel && domainPeer.ID != 0 {
return &tg.ChatOnlines{Onlines: r.channelOnlineCount(ctx, userID, domainPeer.ID)}, nil
}
return &tg.ChatOnlines{Onlines: 1}, nil
}
func (r *Router) onMessagesGetMessages(ctx context.Context, ids []tg.InputMessageClass) (tg.MessagesMessagesClass, error) {
if len(ids) > maxGetMessagesIDs {
return nil, limitInvalidErr()
}
if r.deps.Messages == nil || len(ids) == 0 {
return &tg.MessagesMessages{}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
out := make([]tg.MessageClass, 0, len(ids))
requestedIDs := make([]int, 0, len(ids))
for _, input := range ids {
id, ok := inputMessageBoxID(input)
if !ok || id <= 0 || id > domain.MaxMessageBoxID {
continue
}
requestedIDs = append(requestedIDs, id)
}
list, err := r.deps.Messages.GetMessages(ctx, userID, requestedIDs)
if err != nil {
return nil, internalErr()
}
foundByID := make(map[int]domain.Message, len(list.Messages))
for _, msg := range list.Messages {
foundByID[msg.ID] = msg
}
found := make([]domain.Message, 0, len(list.Messages))
for _, input := range ids {
id, ok := inputMessageBoxID(input)
if !ok || id <= 0 || id > domain.MaxMessageBoxID {
out = append(out, &tg.MessageEmpty{ID: id})
continue
}
msg, ok := foundByID[id]
if !ok {
out = append(out, &tg.MessageEmpty{ID: id})
continue
}
found = append(found, msg)
out = append(out, tgMessage(msg))
}
chats := r.chatsForMessageUpdates(ctx, userID, found)
result := &tg.MessagesMessages{
Messages: out,
Users: r.usersForMessageUpdates(ctx, userID, found),
Chats: chats,
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
// onMessagesGetRichMessage 返回单条消息的完整富文本Layer 227 richMessage。消息列表
// 投影里已带 richMessagetgMessage本 RPC 是客户端按 (peer,id) 拉取完整富文本的入口。
// Phase 1 仅私聊:按 box id 从请求者自己的消息盒取出并投影;频道侧富文本留 Phase 2。
func (r *Router) onMessagesGetRichMessage(ctx context.Context, req *tg.MessagesGetRichMessageRequest) (tg.MessagesMessagesClass, error) {
if r.deps.Messages == nil || req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return &tg.MessagesMessages{}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 {
return nil, peerIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel {
// 频道富文本留 Phase 2channel_message 富文本列已备未接线)。
return &tg.MessagesMessages{}, nil
}
list, err := r.deps.Messages.GetMessages(ctx, userID, []int{req.ID})
if err != nil {
return nil, internalErr()
}
out := make([]tg.MessageClass, 0, 1)
found := make([]domain.Message, 0, 1)
for _, msg := range list.Messages {
if msg.ID != req.ID || msg.Peer != peer {
continue
}
found = append(found, msg)
out = append(out, tgMessage(msg))
}
if len(out) == 0 {
out = append(out, &tg.MessageEmpty{ID: req.ID})
}
result := &tg.MessagesMessages{
Messages: out,
Users: r.usersForMessageUpdates(ctx, userID, found),
Chats: r.chatsForMessageUpdates(ctx, userID, found),
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSearchGlobalRequest) (tg.MessagesMessagesClass, error) {
if req.BroadcastsOnly && req.GroupsOnly {
return &tg.MessagesMessages{}, nil
}
query := normalizeSearchQuery(req.Q)
musicOnly := messagesSearchFilterMusic(req.Filter)
communityInput, hasCommunity := req.GetCommunity()
if !hasCommunity && req.Community != nil {
communityInput, hasCommunity = req.Community, true
}
emptyCommunitySearch := query == "" && !musicOnly && hasCommunity && messagesSearchFilterEmpty(req.Filter)
if query == "" && !musicOnly && !emptyCommunitySearch {
return nil, searchQueryEmptyErr()
}
if utf8.RuneCountInString(query) > maxMessageSearchQLength {
return nil, limitInvalidErr()
}
if !musicOnly && searchFilterNeedsMediaStore(req.Filter) {
return &tg.MessagesMessages{}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
var communityView *domain.CommunityView
var communityScope domain.CommunitySearchScope
if hasCommunity {
view, err := r.communityFromInput(ctx, userID, communityInput)
if err != nil {
return nil, err
}
scope, err := r.deps.Communities.SearchScope(ctx, userID, view.Community.ID)
if err != nil {
return nil, communityErr(err)
}
communityView, communityScope = &view, scope
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelGlobalSearchLimit {
limit = domain.MaxChannelGlobalSearchLimit
}
folderID, hasFolderID := req.GetFolderID()
if hasFolderID && folderID < 0 {
return nil, folderIDInvalidErr()
}
channelOffsetID, err := r.searchGlobalChannelOffsetID(ctx, userID, req.OffsetPeer)
if err != nil {
return nil, err
}
if emptyCommunitySearch {
result := appendCommunitySearchChat(&tg.MessagesMessages{}, communityView)
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
var private domain.MessageList
if !req.BroadcastsOnly && !req.GroupsOnly && r.deps.Messages != nil {
filter := domain.MessageFilter{
Query: query,
OffsetID: req.OffsetID,
OffsetDate: req.OffsetRate,
Limit: limit + 1,
MusicOnly: musicOnly,
}
if communityView != nil {
filter.RestrictPeerIDs = true
filter.PeerIDs = communityScope.BotUserIDs
}
if req.MaxDate > 0 {
filter.OffsetDate = req.MaxDate
}
if req.UsersOnly || !req.BroadcastsOnly && !req.GroupsOnly {
private, err = r.deps.Messages.Search(ctx, userID, filter)
if err != nil {
return nil, internalErr()
}
private = r.enrichMessageList(ctx, userID, private)
}
}
if req.UsersOnly || r.deps.Channels == nil {
result := appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView)
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
channelHistory, err := r.deps.Channels.SearchJoinedMessages(ctx, userID, domain.ChannelGlobalSearchRequest{
Query: query,
ChannelIDs: communityScope.ChannelIDs,
RestrictChannelIDs: communityView != nil,
AllowPublicPreview: communityView != nil,
BroadcastsOnly: req.BroadcastsOnly,
GroupsOnly: req.GroupsOnly,
MusicOnly: musicOnly,
HasFolderID: hasFolderID,
FolderID: folderID,
OffsetRate: req.OffsetRate,
OffsetChannelID: channelOffsetID,
OffsetID: req.OffsetID,
MinDate: req.MinDate,
MaxDate: req.MaxDate,
Limit: limit,
})
if err != nil {
return nil, channelInvalidErr(err)
}
channelHistory = r.enrichChannelHistory(ctx, userID, channelHistory)
if req.BroadcastsOnly || req.GroupsOnly {
return appendCommunitySearchChat(r.tgGlobalChannelMessages(ctx, userID, limitChannelHistory(channelHistory, limit)), communityView), nil
}
return appendCommunitySearchChat(r.tgGlobalSearchMessages(ctx, userID, limit, private, channelHistory), communityView), nil
}
func appendCommunitySearchChat(result tg.MessagesMessagesClass, view *domain.CommunityView) tg.MessagesMessagesClass {
if result == nil || view == nil {
return result
}
chat := tgCommunityChat(*view)
switch out := result.(type) {
case *tg.MessagesMessages:
out.Chats = appendUniqueTGChats(out.Chats, chat)
case *tg.MessagesMessagesSlice:
out.Chats = appendUniqueTGChats(out.Chats, chat)
case *tg.MessagesChannelMessages:
out.Chats = appendUniqueTGChats(out.Chats, chat)
}
return result
}
func limitMessageList(list domain.MessageList, limit int) domain.MessageList {
if limit <= 0 {
limit = domain.MaxChannelGlobalSearchLimit
}
if len(list.Messages) > limit {
list.Messages = list.Messages[:limit]
list.Count = limit + 1
}
return list
}
func limitChannelHistory(history domain.ChannelHistory, limit int) domain.ChannelHistory {
if limit <= 0 {
limit = domain.MaxChannelGlobalSearchLimit
}
if len(history.Messages) > limit {
history.Messages = history.Messages[:limit]
history.Count = limit + 1
}
return history
}
func tgGlobalChannelMessages(viewerUserID int64, history domain.ChannelHistory) tg.MessagesMessagesClass {
out := tgChannelHistoryMessages(viewerUserID, history)
if slice, ok := out.(*tg.MessagesMessagesSlice); ok && len(history.Messages) > 0 {
slice.SetNextRate(history.Messages[len(history.Messages)-1].Date)
}
return out
}
func tgGlobalSearchMessages(viewerUserID int64, limit int, private domain.MessageList, channel domain.ChannelHistory) tg.MessagesMessagesClass {
if limit <= 0 {
limit = domain.MaxChannelGlobalSearchLimit
}
hits := make([]globalSearchHit, 0, len(private.Messages)+len(channel.Messages))
for _, msg := range private.Messages {
item := tgMessage(msg)
if item == nil {
continue
}
hits = append(hits, globalSearchHit{
date: msg.Date,
peerRank: msg.Peer.ID,
messageID: msg.ID,
message: item,
})
}
for _, msg := range channel.Messages {
item := tgChannelMessage(viewerUserID, msg)
if item == nil {
continue
}
hits = append(hits, globalSearchHit{
date: msg.Date,
peerRank: msg.ChannelID,
messageID: msg.ID,
message: item,
})
}
sort.Slice(hits, func(i, j int) bool {
a, b := hits[i], hits[j]
if a.date != b.date {
return a.date > b.date
}
if a.peerRank != b.peerRank {
return a.peerRank > b.peerRank
}
return a.messageID > b.messageID
})
hasMore := private.Count > len(private.Messages) || channel.Count > len(channel.Messages) || len(hits) > limit
if len(hits) > limit {
hits = hits[:limit]
}
messages := make([]tg.MessageClass, 0, len(hits))
for _, hit := range hits {
messages = append(messages, hit.message)
}
// 全局搜索命中自己发的消息时 viewer 自己会出现在 users 里,须带 self 标志。
users := append(tgUsersForViewer(viewerUserID, private.Users), tgUsersForViewer(viewerUserID, channel.Users)...)
chats := tgChannels(viewerUserID, channel.Channels)
if hasMore {
out := &tg.MessagesMessagesSlice{
Count: limit + 1,
Messages: messages,
Chats: chats,
Users: users,
}
if len(hits) > 0 {
out.SetNextRate(hits[len(hits)-1].date)
}
return out
}
return &tg.MessagesMessages{Messages: messages, Chats: chats, Users: users}
}
func (r *Router) messageFilterFromHistoryRequest(userID int64, req *tg.MessagesGetHistoryRequest) (domain.MessageFilter, bool) {
peer, ok := r.domainPeerFromInputPeer(userID, req.Peer)
if !ok {
return domain.MessageFilter{}, false
}
limit := req.Limit
if limit > 50 {
limit = 50
}
return domain.MessageFilter{
HasPeer: true,
Peer: peer,
OffsetID: req.OffsetID,
OffsetDate: req.OffsetDate,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
}, true
}
func (r *Router) messageFilterFromSearchRequest(ctx context.Context, userID int64, req *tg.MessagesSearchRequest) (domain.MessageFilter, error) {
limit := req.Limit
if limit > 500 {
limit = 500
}
filter := domain.MessageFilter{
Query: req.Q,
OffsetID: req.OffsetID,
MinDate: req.MinDate,
MaxDate: req.MaxDate,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
MusicOnly: messagesSearchFilterMusic(req.Filter),
NeedTotalCount: req.OffsetID == 0 && req.MinDate == 0 && req.MaxDate == 0 && req.AddOffset >= 0 && req.Hash == 0,
}
if peer, ok := r.domainPeerFromInputPeer(userID, req.Peer); ok {
filter.HasPeer = true
filter.Peer = peer
}
savedReactions, hasSavedReactions := req.GetSavedReaction()
// An empty optional vector carries no reaction-filtering semantics. Some TL
// clients emit flags.3 with a zero-length vector on ordinary peer searches.
// Keep the wire presence intact at the TL edge, but only apply Saved
// Messages scope and reaction validation when the vector has values.
hasSavedReactionFilter := hasSavedReactions && len(savedReactions) > 0
savedPeerInput, hasSavedPeer := req.GetSavedPeerID()
if hasSavedReactionFilter || hasSavedPeer {
if !filter.HasPeer ||
filter.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: userID}) {
return domain.MessageFilter{}, peerIDInvalidErr()
}
}
if hasSavedPeer {
if savedPeerInput == nil {
return domain.MessageFilter{}, peerIDInvalidErr()
}
savedPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, savedPeerInput)
if err != nil {
return domain.MessageFilter{}, err
}
filter.SavedPeer = savedPeer
}
if hasSavedReactionFilter {
if len(savedReactions) > maxReactionVector {
return domain.MessageFilter{}, reactionInvalidErr()
}
seen := make(map[string]struct{}, len(savedReactions))
for _, item := range savedReactions {
reaction, err := domainMessageReactionFromTL(item)
if err != nil {
return domain.MessageFilter{}, err
}
if _, ok := seen[reaction.Key()]; ok {
continue
}
seen[reaction.Key()] = struct{}{}
filter.SavedReactions = append(filter.SavedReactions, reaction)
}
}
return filter, nil
}
func (r *Router) channelHistoryFilterFromSearchRequest(userID int64, req *tg.MessagesSearchRequest, channelID int64) (domain.ChannelHistoryFilter, bool) {
limit := req.Limit
countOnly := limit == 0
if limit <= 0 || limit > 100 {
limit = 100
}
filter := domain.ChannelHistoryFilter{
ChannelID: channelID,
Query: req.Q,
PinnedOnly: messagesSearchFilterPinned(req.Filter),
MusicOnly: messagesSearchFilterMusic(req.Filter),
NeedTotalCount: countOnly ||
(req.OffsetID == 0 && req.MinDate == 0 && req.MaxDate == 0 && req.AddOffset >= 0 && req.Hash == 0),
CountOnly: countOnly,
OffsetID: req.OffsetID,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MinDate: req.MinDate,
MaxDate: req.MaxDate,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
}
if req.FromID != nil {
from, ok := r.domainPeerFromInputPeer(userID, req.FromID)
if !ok || from.Type != domain.PeerTypeUser || from.ID == 0 {
return domain.ChannelHistoryFilter{}, false
}
filter.SenderUserID = from.ID
}
return filter, true
}
func messagesSearchFilterPinned(filter tg.MessagesFilterClass) bool {
_, ok := filter.(*tg.InputMessagesFilterPinned)
return ok
}
func messagesSearchFilterMusic(filter tg.MessagesFilterClass) bool {
_, ok := filter.(*tg.InputMessagesFilterMusic)
return ok
}
func messagesSearchFilterEmpty(filter tg.MessagesFilterClass) bool {
_, ok := filter.(*tg.InputMessagesFilterEmpty)
return ok
}
func messagesSearchFilterChatPhotos(filter tg.MessagesFilterClass) bool {
_, ok := filter.(*tg.InputMessagesFilterChatPhotos)
return ok
}
func searchFilterNeedsMediaStore(filter tg.MessagesFilterClass) bool {
switch filter.(type) {
case nil, *tg.InputMessagesFilterEmpty:
return false
case *tg.InputMessagesFilterPhotos,
*tg.InputMessagesFilterVideo,
*tg.InputMessagesFilterPhotoVideo,
*tg.InputMessagesFilterDocument,
*tg.InputMessagesFilterMusic,
*tg.InputMessagesFilterURL,
*tg.InputMessagesFilterGif,
*tg.InputMessagesFilterVoice,
*tg.InputMessagesFilterRoundVoice,
*tg.InputMessagesFilterRoundVideo,
*tg.InputMessagesFilterPoll:
return true
default:
return false
}
}
// mediaCategoriesForFilter 把客户端共享媒体标签页过滤器映射为媒体索引的基础类别并集。
// 复合标签页(PhotoVideo / RoundVoice)映射为多个基础类别;返回 nil 表示该过滤器不走媒体索引。
func mediaCategoriesForFilter(filter tg.MessagesFilterClass) []domain.MediaCategory {
switch filter.(type) {
case *tg.InputMessagesFilterPhotos:
return []domain.MediaCategory{domain.MediaCategoryPhoto}
case *tg.InputMessagesFilterVideo:
return []domain.MediaCategory{domain.MediaCategoryVideo}
case *tg.InputMessagesFilterPhotoVideo:
return []domain.MediaCategory{domain.MediaCategoryPhoto, domain.MediaCategoryVideo}
case *tg.InputMessagesFilterDocument:
return []domain.MediaCategory{domain.MediaCategoryFile}
case *tg.InputMessagesFilterMusic:
return []domain.MediaCategory{domain.MediaCategoryMusic}
case *tg.InputMessagesFilterURL:
return []domain.MediaCategory{domain.MediaCategoryURL}
case *tg.InputMessagesFilterGif:
return []domain.MediaCategory{domain.MediaCategoryGif}
case *tg.InputMessagesFilterVoice:
return []domain.MediaCategory{domain.MediaCategoryVoice}
case *tg.InputMessagesFilterRoundVideo:
return []domain.MediaCategory{domain.MediaCategoryRoundVideo}
case *tg.InputMessagesFilterRoundVoice:
return []domain.MediaCategory{domain.MediaCategoryVoice, domain.MediaCategoryRoundVideo}
case *tg.InputMessagesFilterPoll:
return []domain.MediaCategory{domain.MediaCategoryPoll}
default:
return nil
}
}
func messagesNotModifiedOrEmpty(hash int64) tg.MessagesMessagesClass {
if hash != 0 {
return &tg.MessagesMessagesNotModified{Count: 0}
}
return &tg.MessagesMessages{}
}