owpengram-server/internal/rpc/messages.go
A 860e581d06 business: fix contact projection and phone sharing
(cherry picked from commit c0a0e5b52240ed415d3b43ba77659821887bf50b)
2026-06-10 22:06:56 +08:00

6440 lines
196 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"
"hash/fnv"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"telesrv/internal/compat/tdesktop"
"telesrv/internal/domain"
)
const (
maxSendMessageTextLength = domain.MaxMessageTextLength
maxReplyQuoteLength = domain.MaxMessageReplyQuoteLength
maxMessageSearchQLength = 256
maxMessageEntityCount = domain.MaxMessageEntityCount
maxGetMessagesIDs = 100
maxMessageSearchFilters = 32
maxForumTopicIDs = 100
maxDialogInputPeers = 100
maxSearchResultsLimit = 100
maxReactionVector = 16
maxReactionListOffset = 128
maxReportOptionLength = 32
maxReportCommentLength = 1024
maxReportRandomIDLength = 128
maxReadMetrics = 100
maxBusinessConnIDLength = 128
maxSendMultiMediaItems = 10
maxForumTopicTitleLength = 128
maxPollVoteOptions = 10
maxPollOptionBytes = 256
maxPollVotesOffsetLength = 128
maxTodoItems = 30
maxTodoTitleLength = 200
maxCommonChatsLimit = domain.MaxCommonChannelsLimit
maxStickerSearchQLength = 128
maxStickerSearchLangs = 16
maxEmojiLangCodeLength = 32
maxEmojiDocuments = 100
maxSavedReactionTagTitle = 12
defaultTopReactionsLimit = 14
sendMessageRateLimit = 30
sendMessageRateWindow = time.Minute
forumGeneralTopicID = 1
forumGeneralIconColor = 0x6FB9F0
)
type accountDefaultReactionService interface {
SetDefaultReaction(ctx context.Context, userID int64, reaction domain.MessageReaction) (domain.AccountReactionSettings, error)
}
type accountPaidReactionPrivacyService interface {
GetReactionSettings(ctx context.Context, userID int64) (domain.AccountReactionSettings, error)
SetPaidReactionPrivacy(ctx context.Context, userID int64, privacy domain.PaidReactionPrivacy) (domain.AccountReactionSettings, error)
}
type messageReactionUpdateRecorder interface {
RecordMessageReactions(ctx context.Context, authKeyID [8]byte, userID int64, msg domain.Message) (domain.UpdateEvent, domain.UpdateState, error)
}
type messageReactionUsageRecorder interface {
RecordMessageReactionUse(ctx context.Context, userID int64, reactions []domain.MessageReaction, addToRecent bool, date int) error
}
type channelParticipantReactionModerator interface {
DeleteParticipantReaction(ctx context.Context, userID int64, req domain.DeleteChannelParticipantReactionRequest) (domain.ChannelMessageReactionsResult, error)
DeleteParticipantReactions(ctx context.Context, userID int64, req domain.DeleteChannelParticipantReactionsRequest) (domain.DeleteChannelParticipantReactionsResult, error)
}
// registerMessages 注册 messages.* RPC handler。
func (r *Router) registerMessages(d *tg.ServerDispatcher) {
d.OnMessagesSetTyping(r.onMessagesSetTyping)
d.OnMessagesSaveDraft(r.onMessagesSaveDraft)
d.OnMessagesSaveDefaultSendAs(r.onMessagesSaveDefaultSendAs)
d.OnMessagesGetAllDrafts(r.onMessagesGetAllDrafts)
d.OnMessagesClearAllDrafts(r.onMessagesClearAllDrafts)
d.OnMessagesGetAllStickers(r.onMessagesGetAllStickers)
d.OnMessagesGetEmojiStickers(r.onMessagesGetEmojiStickers)
d.OnMessagesGetFeaturedStickers(func(ctx context.Context, hash int64) (tg.MessagesFeaturedStickersClass, error) {
return messagesFeaturedStickersEmpty(hash), nil
})
d.OnMessagesGetFeaturedEmojiStickers(func(ctx context.Context, hash int64) (tg.MessagesFeaturedStickersClass, error) {
return messagesFeaturedStickersEmpty(hash), nil
})
d.OnMessagesGetRecentStickers(func(ctx context.Context, req *tg.MessagesGetRecentStickersRequest) (tg.MessagesRecentStickersClass, error) {
if req.Hash != 0 {
return &tg.MessagesRecentStickersNotModified{}, nil
}
return &tg.MessagesRecentStickers{
Packs: []tg.StickerPack{},
Stickers: []tg.DocumentClass{},
Dates: []int{},
}, nil
})
d.OnMessagesGetFavedStickers(func(ctx context.Context, hash int64) (tg.MessagesFavedStickersClass, error) {
if hash != 0 {
return &tg.MessagesFavedStickersNotModified{}, nil
}
return &tg.MessagesFavedStickers{
Packs: []tg.StickerPack{},
Stickers: []tg.DocumentClass{},
}, nil
})
d.OnMessagesGetSavedGifs(func(ctx context.Context, hash int64) (tg.MessagesSavedGifsClass, error) {
if hash != 0 {
return &tg.MessagesSavedGifsNotModified{}, nil
}
return &tg.MessagesSavedGifs{Gifs: []tg.DocumentClass{}}, nil
})
d.OnMessagesSendMessage(r.onMessagesSendMessage)
d.OnMessagesForwardMessages(r.onMessagesForwardMessages)
d.OnMessagesGetDialogFilters(r.onMessagesGetDialogFilters)
d.OnMessagesGetSuggestedDialogFilters(func(ctx context.Context) ([]tg.DialogFilterSuggested, error) {
return []tg.DialogFilterSuggested{}, nil
})
d.OnMessagesUpdateDialogFilter(r.onMessagesUpdateDialogFilter)
d.OnMessagesUpdateDialogFiltersOrder(r.onMessagesUpdateDialogFiltersOrder)
d.OnMessagesToggleDialogFilterTags(r.onMessagesToggleDialogFilterTags)
d.OnMessagesGetSavedDialogs(func(ctx context.Context, req *tg.MessagesGetSavedDialogsRequest) (tg.MessagesSavedDialogsClass, error) {
if req.Hash != 0 {
return &tg.MessagesSavedDialogsNotModified{Count: 0}, nil
}
return &tg.MessagesSavedDialogs{}, nil
})
d.OnMessagesGetPinnedSavedDialogs(func(ctx context.Context) (tg.MessagesSavedDialogsClass, error) {
return &tg.MessagesSavedDialogs{}, nil
})
d.OnMessagesToggleSavedDialogPin(func(ctx context.Context, req *tg.MessagesToggleSavedDialogPinRequest) (bool, error) {
return true, nil
})
d.OnMessagesReorderPinnedSavedDialogs(func(ctx context.Context, req *tg.MessagesReorderPinnedSavedDialogsRequest) (bool, error) {
return true, nil
})
d.OnMessagesGetSavedDialogsByID(func(ctx context.Context, req *tg.MessagesGetSavedDialogsByIDRequest) (tg.MessagesSavedDialogsClass, error) {
return &tg.MessagesSavedDialogs{}, nil
})
d.OnMessagesGetSavedHistory(r.onMessagesGetSavedHistory)
d.OnMessagesReadSavedHistory(r.onMessagesReadSavedHistory)
d.OnMessagesDeleteSavedHistory(r.onMessagesDeleteSavedHistory)
d.OnMessagesGetCommonChats(r.onMessagesGetCommonChats)
d.OnMessagesGetDefaultHistoryTTL(r.onMessagesGetDefaultHistoryTTL)
d.OnMessagesGetSponsoredMessages(r.onMessagesGetSponsoredMessages)
d.OnMessagesGetWebPagePreview(r.onMessagesGetWebPagePreview)
d.OnMessagesUploadMedia(r.onMessagesUploadMedia)
d.OnMessagesSendMedia(r.onMessagesSendMedia)
d.OnMessagesSendMultiMedia(r.onMessagesSendMultiMedia)
d.OnMessagesReportSpam(r.onMessagesReportSpam)
d.OnMessagesReport(r.onMessagesReport)
d.OnMessagesReportReaction(r.onMessagesReportReaction)
d.OnMessagesReportMessagesDelivery(r.onMessagesReportMessagesDelivery)
d.OnMessagesReportReadMetrics(r.onMessagesReportReadMetrics)
d.OnMessagesReportMusicListen(r.onMessagesReportMusicListen)
d.OnMessagesReportSponsoredMessage(r.onMessagesReportSponsoredMessage)
d.OnMessagesReadMessageContents(r.onMessagesReadMessageContents)
d.OnMessagesGetMessagesViews(r.onMessagesGetMessagesViews)
d.OnMessagesGetUnreadMentions(r.onMessagesGetUnreadMentions)
d.OnMessagesReadMentions(r.onMessagesReadMentions)
d.OnMessagesGetSearchCounters(r.onMessagesGetSearchCounters)
d.OnMessagesGetReplies(r.onMessagesGetReplies)
d.OnMessagesGetDiscussionMessage(r.onMessagesGetDiscussionMessage)
d.OnMessagesReadDiscussion(r.onMessagesReadDiscussion)
d.OnMessagesGetForumTopics(r.onMessagesGetForumTopics)
d.OnMessagesGetForumTopicsByID(r.onMessagesGetForumTopicsByID)
d.OnMessagesGetOnlines(r.onMessagesGetOnlines)
d.OnMessagesGetAvailableReactions(r.onMessagesGetAvailableReactions)
d.OnMessagesGetAvailableEffects(func(ctx context.Context, hash int) (tg.MessagesAvailableEffectsClass, error) {
return &tg.MessagesAvailableEffects{
Hash: 0,
Effects: []tg.AvailableEffect{},
Documents: []tg.DocumentClass{},
}, nil
})
d.OnMessagesGetStickers(func(ctx context.Context, req *tg.MessagesGetStickersRequest) (tg.MessagesStickersClass, error) {
return tdesktop.Stickers(), nil
})
d.OnMessagesGetStickerSet(r.onMessagesGetStickerSet)
d.OnMessagesGetEmojiGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
return tdesktop.EmojiGroups(), nil
})
d.OnMessagesGetEmojiStickerGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
return tdesktop.EmojiGroups(), nil
})
d.OnMessagesGetEmojiProfilePhotoGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
return tdesktop.EmojiProfilePhotoGroups(), nil
})
d.OnMessagesGetEmojiKeywords(r.onMessagesGetEmojiKeywords)
d.OnMessagesGetEmojiKeywordsDifference(r.onMessagesGetEmojiKeywordsDifference)
d.OnMessagesGetEmojiKeywordsLanguages(func(ctx context.Context, langcodes []string) ([]tg.EmojiLanguage, error) {
return []tg.EmojiLanguage{}, nil
})
d.OnMessagesGetCustomEmojiDocuments(r.onMessagesGetCustomEmojiDocuments)
d.OnMessagesGetAttachedStickers(r.onMessagesGetAttachedStickers)
d.OnMessagesSearchStickerSets(r.onMessagesSearchStickerSets)
d.OnMessagesSearchStickers(r.onMessagesSearchStickers)
d.OnMessagesGetAttachMenuBots(func(ctx context.Context, hash int64) (tg.AttachMenuBotsClass, error) {
return tdesktop.AttachMenuBots(), nil
})
d.OnMessagesGetQuickReplies(func(ctx context.Context, hash int64) (tg.MessagesQuickRepliesClass, error) {
return tdesktop.QuickReplies(), nil
})
d.OnMessagesGetWebPage(func(ctx context.Context, req *tg.MessagesGetWebPageRequest) (*tg.MessagesWebPage, error) {
return tdesktop.WebPage(req.URL), nil
})
d.OnMessagesGetDialogs(func(ctx context.Context, req *tg.MessagesGetDialogsRequest) (tg.MessagesDialogsClass, error) {
if r.deps.Dialogs == nil {
return &tg.MessagesDialogs{}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
filter, err := r.dialogFilterFromRequest(ctx, userID, req)
if err != nil {
return nil, err
}
list, err := r.deps.Dialogs.GetDialogs(ctx, userID, filter)
if err != nil {
return nil, internalErr()
}
if filter.Hash != 0 && list.Hash == filter.Hash {
return &tg.MessagesDialogsNotModified{Count: list.Count}, nil
}
return tgMessagesDialogs(userID, r.withDialogListPresence(list)), nil
})
d.OnMessagesGetPinnedDialogs(func(ctx context.Context, folderID int) (*tg.MessagesPeerDialogs, error) {
id, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
var list domain.DialogList
if r.deps.Dialogs != nil {
list, err = r.deps.Dialogs.GetDialogs(ctx, userID, domain.DialogFilter{
PinnedOnly: true,
HasFolderID: true,
FolderID: folderID,
Limit: 100,
})
if err != nil {
return nil, internalErr()
}
}
st := domain.UpdateState{Date: int(r.clock.Now().Unix())}
if r.deps.Updates != nil {
var err error
st, err = r.deps.Updates.GetState(ctx, id, userID)
if err != nil {
return nil, internalErr()
}
}
return tgPeerDialogs(userID, r.withDialogListPresence(list), st), nil
})
d.OnMessagesGetPeerDialogs(func(ctx context.Context, peers []tg.InputDialogPeerClass) (*tg.MessagesPeerDialogs, error) {
id, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
domainPeers, err := r.dialogPeersFromInput(ctx, userID, peers)
if err != nil {
return nil, err
}
var list domain.DialogList
if len(domainPeers) > 0 && r.deps.Dialogs != nil {
var err error
list, err = r.deps.Dialogs.GetPeerDialogs(ctx, userID, domainPeers)
if err != nil {
return nil, internalErr()
}
}
st := domain.UpdateState{Date: int(r.clock.Now().Unix())}
if r.deps.Updates != nil {
var err error
st, err = r.deps.Updates.GetState(ctx, id, userID)
if err != nil {
return nil, internalErr()
}
}
r.trackChannelInterest(ctx, userID, channelIDsFromDialogs(list)...)
return tgPeerDialogs(userID, r.withDialogListPresence(list), st), nil
})
d.OnMessagesGetPeerSettings(r.onMessagesGetPeerSettings)
d.OnMessagesToggleDialogPin(r.onMessagesToggleDialogPin)
d.OnMessagesReorderPinnedDialogs(r.onMessagesReorderPinnedDialogs)
d.OnMessagesMarkDialogUnread(r.onMessagesMarkDialogUnread)
d.OnMessagesGetDialogUnreadMarks(r.onMessagesGetDialogUnreadMarks)
d.OnMessagesHidePeerSettingsBar(r.onMessagesHidePeerSettingsBar)
d.OnMessagesGetMessageEditData(r.onMessagesGetMessageEditData)
d.OnMessagesEditMessage(r.onMessagesEditMessage)
d.OnMessagesGetOutboxReadDate(r.onMessagesGetOutboxReadDate)
d.OnMessagesGetMessageReadParticipants(r.onMessagesGetMessageReadParticipants)
d.OnMessagesDeleteMessages(r.onMessagesDeleteMessages)
d.OnMessagesDeleteHistory(r.onMessagesDeleteHistory)
d.OnMessagesGetMessages(r.onMessagesGetMessages)
d.OnMessagesGetHistory(func(ctx context.Context, req *tg.MessagesGetHistoryRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
filter, ok := r.messageFilterFromHistoryRequest(userID, req)
if !ok {
return messagesNotModifiedOrEmpty(req.Hash), nil
}
if filter.Peer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return messagesNotModifiedOrEmpty(req.Hash), nil
}
if err := r.validateInputPeerChannelAccess(ctx, userID, req.Peer, filter.Peer.ID); err != nil {
return nil, err
}
if isLegacyInputPeerChat(req.Peer) {
return &tg.MessagesMessages{}, nil
}
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
ChannelID: filter.Peer.ID,
OffsetID: filter.OffsetID,
OffsetDate: filter.OffsetDate,
AddOffset: filter.AddOffset,
Limit: filter.Limit,
MaxID: filter.MaxID,
MinID: filter.MinID,
Hash: filter.Hash,
})
if err != nil {
return nil, channelInvalidErr(err)
}
history = r.enrichChannelHistory(ctx, userID, history)
r.trackChannelInterest(ctx, userID, filter.Peer.ID)
if filter.Hash != 0 && history.Hash == filter.Hash {
return &tg.MessagesMessagesNotModified{Count: history.Count}, nil
}
return tgChannelHistoryMessages(userID, history), nil
}
r.clearChannelInterest(ctx, userID)
if r.deps.Messages == nil {
return messagesNotModifiedOrEmpty(req.Hash), nil
}
list, err := r.deps.Messages.GetHistory(ctx, userID, filter)
if err != nil {
return nil, internalErr()
}
if filter.Hash != 0 && list.Hash == filter.Hash {
return &tg.MessagesMessagesNotModified{Count: list.Count}, nil
}
return tgMessagesMessages(userID, r.withMessageListPresence(list)), nil
})
d.OnMessagesReadHistory(func(ctx context.Context, req *tg.MessagesReadHistoryRequest) (*tg.MessagesAffectedMessages, error) {
id, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, peerErr := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if peerErr == nil && peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
read, err := r.deps.Channels.ReadHistory(ctx, userID, domain.ReadChannelHistoryRequest{
UserID: userID,
ChannelID: peer.ID,
MaxID: req.MaxID,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelInvalidErr(err)
}
event, err := r.recordChannelReadInbox(ctx, userID, read)
if err != nil {
return nil, err
}
r.pushChannelReadOutboxUpdates(ctx, read.ChannelID, read.OutboxUpdates)
if event.Pts != 0 {
return &tg.MessagesAffectedMessages{Pts: event.Pts, PtsCount: event.PtsCount}, nil
}
return r.affectedMessages(ctx, id, userID)
}
if peerErr != nil {
return nil, peerErr
}
if r.deps.Messages != nil {
sessionID, _ := SessionIDFrom(ctx)
read, err := r.deps.Messages.ReadHistory(ctx, userID, domain.ReadHistoryRequest{
OwnerUserID: userID,
Peer: peer,
MaxID: req.MaxID,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: id,
OriginSessionID: sessionID,
})
if err != nil {
return nil, internalErr()
}
if read.Changed && read.InboxEvent.Pts != 0 {
r.pushReadHistoryEvent(ctx, read.OwnerUserID, read.InboxEvent)
if read.OutboxChanged && read.OutboxEvent.Pts != 0 {
r.pushReadHistoryEvent(ctx, read.OutboxUserID, read.OutboxEvent)
}
return &tg.MessagesAffectedMessages{Pts: read.InboxEvent.Pts, PtsCount: read.InboxEvent.PtsCount}, nil
}
}
return r.affectedMessages(ctx, id, userID)
})
d.OnMessagesSearch(func(ctx context.Context, req *tg.MessagesSearchRequest) (tg.MessagesMessagesClass, error) {
if utf8.RuneCountInString(req.Q) > maxMessageSearchQLength {
return nil, limitInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
filter := r.messageFilterFromSearchRequest(userID, req)
if filter.HasPeer && filter.Peer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return messagesNotModifiedOrEmpty(req.Hash), nil
}
if err := r.validateInputPeerChannelAccess(ctx, userID, req.Peer, filter.Peer.ID); err != nil {
return nil, err
}
if isLegacyInputPeerChat(req.Peer) {
return &tg.MessagesMessages{}, nil
}
if searchFilterNeedsMediaStore(req.Filter) {
view, err := r.deps.Channels.GetChannel(ctx, userID, filter.Peer.ID)
if err != nil {
return nil, channelInvalidErr(err)
}
return tgChannelHistoryMessages(userID, domain.ChannelHistory{Channel: view.Channel}), nil
}
chFilter, ok := r.channelHistoryFilterFromSearchRequest(userID, req, filter.Peer.ID)
if !ok {
return nil, peerIDInvalidErr()
}
history, err := r.deps.Channels.GetHistory(ctx, userID, chFilter)
if err != nil {
return nil, channelInvalidErr(err)
}
history = r.enrichChannelHistory(ctx, userID, history)
if chFilter.Hash != 0 && history.Hash == chFilter.Hash {
return &tg.MessagesMessagesNotModified{Count: history.Count}, nil
}
return tgChannelHistoryMessages(userID, history), nil
}
if r.deps.Messages == nil {
return messagesNotModifiedOrEmpty(req.Hash), nil
}
if searchFilterNeedsMediaStore(req.Filter) {
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
return &tg.MessagesMessages{}, nil
}
list, err := r.deps.Messages.Search(ctx, userID, filter)
if err != nil {
return nil, internalErr()
}
if filter.Hash != 0 && list.Hash == filter.Hash {
return &tg.MessagesMessagesNotModified{Count: list.Count}, nil
}
return tgMessagesMessages(userID, r.withMessageListPresence(list)), nil
})
d.OnMessagesSearchGlobal(r.onMessagesSearchGlobal)
d.OnMessagesGetSearchResultsCalendar(r.onMessagesGetSearchResultsCalendar)
d.OnMessagesGetSearchResultsPositions(r.onMessagesGetSearchResultsPositions)
d.OnMessagesSendReaction(r.onMessagesSendReaction)
d.OnMessagesGetMessagesReactions(r.onMessagesGetMessagesReactions)
d.OnMessagesGetMessageReactionsList(r.onMessagesGetMessageReactionsList)
d.OnMessagesSetDefaultReaction(r.onMessagesSetDefaultReaction)
d.OnMessagesGetPaidReactionPrivacy(r.onMessagesGetPaidReactionPrivacy)
d.OnMessagesTogglePaidReactionPrivacy(r.onMessagesTogglePaidReactionPrivacy)
d.OnMessagesSendPaidReaction(r.onMessagesSendPaidReaction)
d.OnMessagesDeleteParticipantReactions(r.onMessagesDeleteParticipantReactions)
d.OnMessagesDeleteParticipantReaction(r.onMessagesDeleteParticipantReaction)
d.OnMessagesGetUnreadReactions(r.onMessagesGetUnreadReactions)
d.OnMessagesReadReactions(r.onMessagesReadReactions)
d.OnMessagesGetTopReactions(r.onMessagesGetTopReactions)
d.OnMessagesGetRecentReactions(r.onMessagesGetRecentReactions)
d.OnMessagesClearRecentReactions(r.onMessagesClearRecentReactions)
d.OnMessagesGetSavedReactionTags(r.onMessagesGetSavedReactionTags)
d.OnMessagesUpdateSavedReactionTag(r.onMessagesUpdateSavedReactionTag)
d.OnMessagesGetDefaultTagReactions(r.onMessagesGetDefaultTagReactions)
d.OnMessagesSendVote(r.onMessagesSendVote)
d.OnMessagesGetPollResults(r.onMessagesGetPollResults)
d.OnMessagesGetPollVotes(r.onMessagesGetPollVotes)
d.OnMessagesAddPollAnswer(r.onMessagesAddPollAnswer)
d.OnMessagesDeletePollAnswer(r.onMessagesDeletePollAnswer)
d.OnMessagesGetUnreadPollVotes(r.onMessagesGetUnreadPollVotes)
d.OnMessagesReadPollVotes(r.onMessagesReadPollVotes)
d.OnMessagesAppendTodoList(r.onMessagesAppendTodoList)
d.OnMessagesToggleTodoCompleted(r.onMessagesToggleTodoCompleted)
d.OnMessagesGetScheduledHistory(r.onMessagesGetScheduledHistory)
d.OnMessagesGetScheduledMessages(r.onMessagesGetScheduledMessages)
d.OnMessagesSendScheduledMessages(r.onMessagesSendScheduledMessages)
d.OnMessagesDeleteScheduledMessages(r.onMessagesDeleteScheduledMessages)
d.OnMessagesCreateForumTopic(r.onMessagesCreateForumTopic)
d.OnMessagesEditForumTopic(r.onMessagesEditForumTopic)
d.OnMessagesUpdatePinnedForumTopic(r.onMessagesUpdatePinnedForumTopic)
d.OnMessagesReorderPinnedForumTopics(r.onMessagesReorderPinnedForumTopics)
d.OnMessagesDeleteTopicHistory(r.onMessagesDeleteTopicHistory)
}
func (r *Router) onMessagesGetSavedHistory(ctx context.Context, req *tg.MessagesGetSavedHistoryRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateSavedHistoryBounds(req.OffsetID, req.OffsetDate, req.AddOffset, req.Limit, req.MaxID, req.MinID); err != nil {
return nil, err
}
parentPeer, hasParent, err := r.validateSavedHistoryParentPeer(ctx, userID, req.GetParentPeer)
if err != nil {
return nil, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if req.Hash != 0 {
return &tg.MessagesMessagesNotModified{Count: 0}, nil
}
chats := r.savedHistoryChats(ctx, userID, hasParent, parentPeer, req.Peer)
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: chats,
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesReadSavedHistory(ctx context.Context, req *tg.MessagesReadSavedHistoryRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.MaxID < 0 || req.MaxID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
if err := r.validateRequiredSavedHistoryParentPeer(ctx, userID, req.ParentPeer); err != nil {
return false, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onMessagesDeleteSavedHistory(ctx context.Context, req *tg.MessagesDeleteSavedHistoryRequest) (*tg.MessagesAffectedHistory, error) {
authKeyID, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.MaxID < 0 || req.MaxID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if minDate, ok := req.GetMinDate(); ok && minDate < 0 {
return nil, limitInvalidErr()
}
if maxDate, ok := req.GetMaxDate(); ok && maxDate < 0 {
return nil, limitInvalidErr()
}
if _, _, err := r.validateSavedHistoryParentPeer(ctx, userID, req.GetParentPeer); err != nil {
return nil, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
func (r *Router) onMessagesGetCommonChats(ctx context.Context, req *tg.MessagesGetCommonChatsRequest) (tg.MessagesChatsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit < 0 || req.Limit > maxCommonChatsLimit {
return nil, limitInvalidErr()
}
if req.MaxID < 0 {
return nil, messageIDInvalidErr()
}
target, found, err := r.userFromInput(ctx, userID, req.UserID)
if err != nil {
return nil, internalErr()
}
if !found || target.ID == 0 || target.ID == userID {
return nil, userIDInvalidErr()
}
if req.Limit == 0 || r.deps.Channels == nil {
return &tg.MessagesChats{Chats: []tg.ChatClass{}}, nil
}
common, err := r.deps.Channels.CommonChannels(ctx, userID, domain.CommonChannelsRequest{
UserID: userID,
TargetUserID: target.ID,
MaxID: req.MaxID,
Limit: req.Limit,
})
if err != nil {
return nil, channelInvalidErr(err)
}
chats := make([]tg.ChatClass, 0, len(common.Channels))
for _, ch := range common.Channels {
chats = append(chats, tgChannelChat(userID, ch, nil))
}
return &tg.MessagesChats{Chats: chats}, nil
}
func (r *Router) onMessagesGetAttachedStickers(ctx context.Context, media tg.InputStickeredMediaClass) ([]tg.StickerSetCoveredClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if media == nil {
return nil, mediaEmptyErr()
}
return []tg.StickerSetCoveredClass{}, nil
}
func (r *Router) onMessagesGetCustomEmojiDocuments(ctx context.Context, documentIDs []int64) ([]tg.DocumentClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if len(documentIDs) > maxEmojiDocuments {
return nil, limitInvalidErr()
}
for _, id := range documentIDs {
if id <= 0 {
return nil, messageIDInvalidErr()
}
}
if r.deps.Files == nil || len(documentIDs) == 0 {
return []tg.DocumentClass{}, nil
}
docs, err := r.deps.Files.GetDocuments(ctx, documentIDs)
if err != nil {
return nil, internalErr()
}
byID := documentsByID(docs)
out := make([]tg.DocumentClass, 0, len(documentIDs))
for _, id := range documentIDs {
if d, ok := byID[id]; ok {
out = append(out, tgDocument(d))
} else {
out = append(out, &tg.DocumentEmpty{ID: id})
}
}
return out, nil
}
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
}
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 (r *Router) onMessagesGetEmojiKeywords(ctx context.Context, langcode string) (*tg.EmojiKeywordsDifference, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if err := validateEmojiLangCode(langcode); err != nil {
return nil, err
}
return &tg.EmojiKeywordsDifference{
LangCode: langcode,
FromVersion: 0,
Version: 0,
Keywords: []tg.EmojiKeywordClass{},
}, nil
}
func (r *Router) onMessagesGetEmojiKeywordsDifference(ctx context.Context, req *tg.MessagesGetEmojiKeywordsDifferenceRequest) (*tg.EmojiKeywordsDifference, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if err := validateEmojiLangCode(req.LangCode); err != nil {
return nil, err
}
if req.FromVersion < 0 {
return nil, limitInvalidErr()
}
return &tg.EmojiKeywordsDifference{
LangCode: req.LangCode,
FromVersion: req.FromVersion,
Version: req.FromVersion,
Keywords: []tg.EmojiKeywordClass{},
}, nil
}
func (r *Router) onMessagesGetExtendedMedia(ctx context.Context, req *tg.MessagesGetExtendedMediaRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateMessageIDVector(req.ID); err != nil {
return nil, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
func (r *Router) onMessagesGetTopReactions(ctx context.Context, req *tg.MessagesGetTopReactionsRequest) (tg.MessagesReactionsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
limit := req.Limit
if limit == 0 {
limit = defaultTopReactionsLimit
}
reactions := []domain.MessageReaction{}
if r.deps.Channels != nil {
var err error
reactions, err = r.deps.Channels.TopReactions(ctx, userID, limit)
if err != nil {
return nil, channelInvalidErr(err)
}
}
return messagesReactionsFromDomain(r.reactionsWithCatalogFallback(ctx, reactions, limit), req.Hash), nil
}
func (r *Router) onMessagesGetRecentReactions(ctx context.Context, req *tg.MessagesGetRecentReactionsRequest) (tg.MessagesReactionsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
if r.deps.Channels == nil {
return messagesReactionsEmpty(req.Hash), nil
}
reactions, err := r.deps.Channels.RecentReactions(ctx, userID, req.Limit)
if err != nil {
return nil, channelInvalidErr(err)
}
return messagesReactionsFromDomain(reactions, req.Hash), nil
}
func (r *Router) onMessagesClearRecentReactions(ctx context.Context) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if r.deps.Channels != nil {
if err := r.deps.Channels.ClearRecentReactions(ctx, userID); err != nil {
return false, channelInvalidErr(err)
}
}
return true, nil
}
func (r *Router) onMessagesGetSavedReactionTags(ctx context.Context, req *tg.MessagesGetSavedReactionTagsRequest) (tg.MessagesSavedReactionTagsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if peer, ok := req.GetPeer(); ok && peer != nil {
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
return nil, err
}
return savedReactionTagsEmpty(req.Hash), nil
}
if r.deps.Channels == nil {
return savedReactionTagsEmpty(req.Hash), nil
}
tags, err := r.deps.Channels.SavedReactionTags(ctx, userID, domain.MaxSavedReactionTags)
if err != nil {
return nil, channelInvalidErr(err)
}
return savedReactionTagsFromDomain(tags, req.Hash), nil
}
func (r *Router) onMessagesUpdateSavedReactionTag(ctx context.Context, req *tg.MessagesUpdateSavedReactionTagRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
reaction, err := domainMessageReactionFromTL(req.Reaction)
if err != nil {
return false, err
}
title, ok := req.GetTitle()
if !ok {
title = ""
}
if utf8.RuneCountInString(title) > maxSavedReactionTagTitle {
return false, limitInvalidErr()
}
if r.deps.Channels != nil {
if err := r.deps.Channels.UpdateSavedReactionTag(ctx, userID, domain.SavedReactionTag{
UserID: userID,
Reaction: reaction,
Title: title,
}); err != nil {
return false, channelInvalidErr(err)
}
}
r.pushUserUpdates(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateSavedReactionTags{}},
Date: int(r.clock.Now().Unix()),
Seq: 0,
})
return true, nil
}
func (r *Router) onMessagesGetDefaultTagReactions(ctx context.Context, hash int64) (tg.MessagesReactionsClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
return messagesReactionsEmpty(hash), nil
}
func (r *Router) onMessagesGetWebPagePreview(ctx context.Context, req *tg.MessagesGetWebPagePreviewRequest) (*tg.MessagesWebPagePreview, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
message := strings.TrimSpace(req.Message)
if message == "" {
return nil, messageEmptyErr()
}
if utf8.RuneCountInString(message) > maxSendMessageTextLength || len(req.Entities) > maxMessageEntityCount {
return nil, limitInvalidErr()
}
return &tg.MessagesWebPagePreview{
Media: &tg.MessageMediaEmpty{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
// onMessagesUploadMedia / onMessagesSendMedia / onMessagesSendMultiMedia 实现见 send_media.go。
func (r *Router) onMessagesGetScheduledMessages(ctx context.Context, req *tg.MessagesGetScheduledMessagesRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateMessageIDVector(req.ID); err != nil {
return nil, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesGetScheduledHistory(ctx context.Context, req *tg.MessagesGetScheduledHistoryRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if req.Hash != 0 {
return &tg.MessagesMessagesNotModified{Count: 0}, nil
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesSendScheduledMessages(ctx context.Context, req *tg.MessagesSendScheduledMessagesRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateMessageIDVector(req.ID); err != nil {
return nil, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if len(req.ID) == 0 {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
return nil, messageIDInvalidErr()
}
func (r *Router) onMessagesDeleteScheduledMessages(ctx context.Context, req *tg.MessagesDeleteScheduledMessagesRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateMessageIDVector(req.ID); err != nil {
return nil, err
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
return &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateDeleteScheduledMessages{
Peer: tgPeer(peer),
Messages: append([]int(nil), req.ID...),
}},
Users: []tg.UserClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Date: int(r.clock.Now().Unix()),
Seq: 0,
}, nil
}
func (r *Router) onMessagesCreateForumTopic(ctx context.Context, req *tg.MessagesCreateForumTopicRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateForumTopicTitle(req.Title, req.TitleMissing); err != nil {
return nil, err
}
if req.RandomID == 0 {
return nil, randomIDEmptyErr()
}
peer, err := r.forumTopicPeer(ctx, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Channels == nil {
return nil, internalErr()
}
var sendAs *domain.Peer
if req.SendAs != nil {
sendAs, err = r.forumSendAsPeer(ctx, req.Peer, req.SendAs)
if err != nil {
return nil, err
}
}
res, err := r.deps.Channels.CreateForumTopic(ctx, userID, domain.CreateChannelForumTopicRequest{
UserID: userID,
ChannelID: peer.ID,
Title: strings.TrimSpace(req.Title),
TitleMissing: req.TitleMissing,
IconColor: req.IconColor,
IconEmojiID: req.IconEmojiID,
RandomID: req.RandomID,
SendAs: sendAs,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, forumTopicError(err)
}
sendRes := domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
Duplicate: res.Duplicate,
}
updates := r.channelMessageUpdates(ctx, userID, sendRes, req.RandomID)
if !res.Duplicate {
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelMessageUpdates(ctx, viewerUserID, sendRes, 0)
})
}
return updates, nil
}
func (r *Router) onMessagesEditForumTopic(ctx context.Context, req *tg.MessagesEditForumTopicRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if title, ok := req.GetTitle(); ok {
if err := validateForumTopicTitle(title, false); err != nil {
return nil, err
}
}
if req.TopicID <= 0 || req.TopicID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.forumTopicPeer(ctx, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Channels == nil {
return nil, internalErr()
}
edit := domain.EditChannelForumTopicRequest{
UserID: userID,
ChannelID: peer.ID,
TopicID: req.TopicID,
Date: int(r.clock.Now().Unix()),
}
if title, ok := req.GetTitle(); ok {
edit.Title = &title
}
if iconEmojiID, ok := req.GetIconEmojiID(); ok {
edit.IconEmojiID = &iconEmojiID
}
if closed, ok := req.GetClosed(); ok {
edit.Closed = &closed
}
if hidden, ok := req.GetHidden(); ok {
edit.Hidden = &hidden
}
res, err := r.deps.Channels.EditForumTopic(ctx, userID, edit)
if err != nil {
return nil, forumTopicError(err)
}
sendRes := domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}
updates := r.channelMessageUpdates(ctx, userID, sendRes, 0)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelMessageUpdates(ctx, viewerUserID, sendRes, 0)
})
return updates, nil
}
func (r *Router) onMessagesUpdatePinnedForumTopic(ctx context.Context, req *tg.MessagesUpdatePinnedForumTopicRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.TopicID <= 0 || req.TopicID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.forumTopicPeer(ctx, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Channels == nil {
return nil, internalErr()
}
res, err := r.deps.Channels.UpdatePinnedForumTopic(ctx, userID, domain.UpdateChannelForumTopicPinnedRequest{
UserID: userID,
ChannelID: peer.ID,
TopicID: req.TopicID,
Pinned: req.Pinned,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, forumTopicError(err)
}
updates := r.pinnedForumTopicUpdates(userID, res.Channel, res.Topic.TopicID, res.Topic.Pinned)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.pinnedForumTopicUpdates(viewerUserID, res.Channel, res.Topic.TopicID, res.Topic.Pinned)
})
return updates, nil
}
func (r *Router) onMessagesReorderPinnedForumTopics(ctx context.Context, req *tg.MessagesReorderPinnedForumTopicsRequest) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if err := validateMessageIDVector(req.Order); err != nil {
return nil, err
}
peer, err := r.forumTopicPeer(ctx, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Channels == nil {
return nil, internalErr()
}
res, err := r.deps.Channels.ReorderPinnedForumTopics(ctx, userID, domain.ReorderChannelPinnedForumTopicsRequest{
UserID: userID,
ChannelID: peer.ID,
Order: append([]int(nil), req.Order...),
Force: req.Force,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, forumTopicError(err)
}
updates := r.pinnedForumTopicsOrderUpdates(userID, res.Channel, res.Order)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.pinnedForumTopicsOrderUpdates(viewerUserID, res.Channel, res.Order)
})
return updates, nil
}
func (r *Router) onMessagesDeleteTopicHistory(ctx context.Context, req *tg.MessagesDeleteTopicHistoryRequest) (*tg.MessagesAffectedHistory, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.TopMsgID <= 0 || req.TopMsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.forumTopicPeer(ctx, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Channels == nil {
return nil, internalErr()
}
res, err := r.deps.Channels.DeleteForumTopicHistory(ctx, userID, domain.DeleteChannelForumTopicHistoryRequest{
UserID: userID,
ChannelID: peer.ID,
TopicID: req.TopMsgID,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, forumTopicError(err)
}
if res.Event.Pts != 0 {
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return &tg.Updates{
Updates: []tg.UpdateClass{tgChannelUpdate(viewerUserID, res.Event)},
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, nil)},
Date: res.Event.Date,
Seq: 0,
}
})
return &tg.MessagesAffectedHistory{Pts: res.Event.Pts, PtsCount: res.Event.PtsCount, Offset: res.Offset}, nil
}
return &tg.MessagesAffectedHistory{Pts: res.Channel.Pts, PtsCount: 0, Offset: res.Offset}, nil
}
func validateMessageIDVector(ids []int) error {
if len(ids) > maxGetMessagesIDs {
return limitInvalidErr()
}
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return messageIDInvalidErr()
}
}
return nil
}
func validateForumTopicTitle(title string, titleMissing bool) error {
title = strings.TrimSpace(title)
if title == "" && !titleMissing {
return topicTitleEmptyErr()
}
if utf8.RuneCountInString(title) > maxForumTopicTitleLength {
return limitInvalidErr()
}
return nil
}
func forumTopicError(err error) error {
switch {
case errors.Is(err, domain.ErrChannelForumMissing):
return channelForumMissingErr()
case errors.Is(err, domain.ErrMessageIDInvalid):
return topicIDInvalidErr()
case errors.Is(err, domain.ErrChannelNotModified):
return tgerr400("CHAT_NOT_MODIFIED")
default:
return channelInvalidErr(err)
}
}
func (r *Router) forumTopicPeer(ctx context.Context, input tg.InputPeerClass) (domain.Peer, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return domain.Peer{}, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
if err != nil {
return domain.Peer{}, err
}
if peer.Type != domain.PeerTypeChannel {
return domain.Peer{}, peerIDInvalidErr()
}
return peer, nil
}
func (r *Router) validateForumSendAs(ctx context.Context, peerInput, sendAsInput tg.InputPeerClass) error {
_, err := r.forumSendAsPeer(ctx, peerInput, sendAsInput)
return err
}
func (r *Router) forumSendAsPeer(ctx context.Context, peerInput, sendAsInput tg.InputPeerClass) (*domain.Peer, error) {
peer, err := r.forumTopicPeer(ctx, peerInput)
if err != nil {
return nil, err
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
sendAs, err := r.checkedDomainPeerFromInputPeer(ctx, userID, sendAsInput)
if err != nil {
return nil, sendAsPeerInvalidErr()
}
if sendAs.Type == domain.PeerTypeUser && sendAs.ID == userID {
return &sendAs, nil
}
if sendAs.Type == domain.PeerTypeChannel && sendAs.ID == peer.ID {
return &sendAs, nil
}
return nil, sendAsPeerInvalidErr()
}
func (r *Router) onMessagesReportSpam(ctx context.Context, peer tg.InputPeerClass) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onMessagesReport(ctx context.Context, req *tg.MessagesReportRequest) (tg.ReportResultClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if len(req.ID) == 0 {
return nil, tgerr.New(400, "MESSAGE_REQUIRED")
}
if len(req.ID) > maxGetMessagesIDs || len(req.Option) > maxReportOptionLength || utf8.RuneCountInString(req.Message) > maxReportCommentLength {
return nil, limitInvalidErr()
}
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
}
return reportResultForOption(string(req.Option))
}
func reportResultForOption(option string) (tg.ReportResultClass, error) {
switch option {
case "":
return &tg.ReportResultChooseOption{
Title: "Report",
Options: []tg.MessageReportOption{
{Text: "Spam", Option: []byte("spam")},
{Text: "Violence", Option: []byte("violence")},
{Text: "Illegal goods", Option: []byte("illegal_goods")},
{Text: "Child abuse", Option: []byte("child_abuse")},
{Text: "Personal data", Option: []byte("personal_data")},
{Text: "Copyright", Option: []byte("copyright")},
{Text: "Other", Option: []byte("other")},
},
}, nil
case "other":
return &tg.ReportResultAddComment{Optional: false, Option: []byte("other:comment")}, nil
case "spam", "violence", "illegal_goods", "child_abuse", "personal_data", "copyright", "other:comment":
return &tg.ReportResultReported{}, nil
default:
return nil, tgerr.New(400, "OPTION_INVALID")
}
}
func (r *Router) onMessagesReportReaction(ctx context.Context, req *tg.MessagesReportReactionRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return false, err
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.ReactionPeer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onMessagesReportMessagesDelivery(ctx context.Context, req *tg.MessagesReportMessagesDeliveryRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if len(req.ID) > maxGetMessagesIDs {
return false, limitInvalidErr()
}
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onMessagesReportReadMetrics(ctx context.Context, req *tg.MessagesReportReadMetricsRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if len(req.Metrics) > maxReadMetrics {
return false, limitInvalidErr()
}
for _, metric := range req.Metrics {
if metric.MsgID <= 0 || metric.MsgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
if metric.TimeInViewMs < 0 || metric.ActiveTimeInViewMs < 0 || metric.HeightToViewportRatioPermille < 0 || metric.SeenRangeRatioPermille < 0 {
return false, limitInvalidErr()
}
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return false, err
}
return true, nil
}
func (r *Router) onMessagesReportMusicListen(ctx context.Context, req *tg.MessagesReportMusicListenRequest) (bool, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return false, internalErr()
}
if req.ID == nil {
return false, tgerr.New(400, "DOCUMENT_INVALID")
}
if req.ListenedDuration < 0 {
return false, limitInvalidErr()
}
return true, nil
}
func (r *Router) onMessagesReportSponsoredMessage(ctx context.Context, req *tg.MessagesReportSponsoredMessageRequest) (tg.ChannelsSponsoredMessageReportResultClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
if len(req.RandomID) == 0 || len(req.RandomID) > maxReportRandomIDLength || len(req.Option) > maxReportOptionLength {
return nil, limitInvalidErr()
}
return &tg.ChannelsSponsoredMessageReportResultReported{}, 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) onMessagesSendReaction(ctx context.Context, req *tg.MessagesSendReactionRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if reactions, ok := req.GetReaction(); ok && len(reactions) > maxReactionVector {
return nil, limitInvalidErr()
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
reactions, err := domainMessageReactionsFromTL(req)
if err != nil {
return nil, err
}
date := int(r.clock.Now().Unix())
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
res, err := r.deps.Channels.SetMessageReactions(ctx, userID, domain.SetChannelMessageReactionsRequest{
UserID: userID,
ChannelID: peer.ID,
MessageID: req.MsgID,
Reactions: reactions,
Big: req.Big,
AddToRecent: req.GetAddToRecent(),
Date: date,
})
if err != nil {
return nil, channelInvalidErr(err)
}
updates := r.channelMessageReactionsUpdates(ctx, userID, res)
r.pushChannelViewerUpdates(ctx, userID, res.Channel.ID, []int64{userID}, func(viewerUserID int64) *tg.Updates {
return r.channelMessageReactionsUpdates(ctx, viewerUserID, res)
})
return updates, nil
}
if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
res, err := r.deps.Messages.SetMessageReactions(ctx, userID, domain.SetPrivateMessageReactionsRequest{
UserID: userID,
Peer: peer,
MessageID: req.MsgID,
Reactions: reactions,
Big: req.Big,
AddToRecent: req.GetAddToRecent(),
Date: date,
})
if err != nil {
return nil, messageReactionErr(err)
}
if err := r.recordMessageReactionUse(ctx, userID, reactions, req.GetAddToRecent(), date); err != nil {
return nil, internalErr()
}
if err := r.recordPrivateMessageReactionEvents(ctx, userID, res); err != nil {
return nil, internalErr()
}
updates := r.privateMessageReactionsUpdates(ctx, userID, peer, res)
r.pushUserUpdates(ctx, userID, updates)
for _, msg := range res.Messages {
if msg.OwnerUserID == 0 || msg.OwnerUserID == userID {
continue
}
viewerPeer := msg.Peer
viewerUpdates := r.privateMessageReactionsUpdates(ctx, msg.OwnerUserID, viewerPeer, res)
r.pushUserUpdates(ctx, msg.OwnerUserID, viewerUpdates)
}
return updates, nil
}
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
func (r *Router) recordMessageReactionUse(ctx context.Context, userID int64, reactions []domain.MessageReaction, addToRecent bool, date int) error {
if len(reactions) == 0 || r.deps.Channels == nil {
return nil
}
recorder, ok := r.deps.Channels.(messageReactionUsageRecorder)
if !ok {
return nil
}
return recorder.RecordMessageReactionUse(ctx, userID, reactions, addToRecent, date)
}
func (r *Router) recordPrivateMessageReactionEvents(ctx context.Context, requestUserID int64, res domain.PrivateMessageReactionsResult) error {
if r.deps.Updates == nil {
return nil
}
recorder, ok := r.deps.Updates.(messageReactionUpdateRecorder)
if !ok {
return nil
}
authKeyID, _ := AuthKeyIDFrom(ctx)
for _, msg := range res.Messages {
if msg.OwnerUserID == 0 || msg.ID == 0 {
continue
}
eventAuthKeyID := [8]byte{}
if msg.OwnerUserID == requestUserID {
eventAuthKeyID = authKeyID
}
if _, _, err := recorder.RecordMessageReactions(ctx, eventAuthKeyID, msg.OwnerUserID, msg); err != nil {
return err
}
}
return nil
}
func (r *Router) onMessagesSetDefaultReaction(ctx context.Context, reaction tg.ReactionClass) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
parsed, err := domainMessageReactionFromTL(reaction)
if err != nil {
return false, err
}
if svc, ok := r.deps.Account.(accountDefaultReactionService); ok {
if _, err := svc.SetDefaultReaction(ctx, userID, parsed); err != nil {
return false, internalErr()
}
}
return true, nil
}
func (r *Router) onMessagesGetPaidReactionPrivacy(ctx context.Context) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
settings := domain.DefaultAccountReactionSettings()
if svc, ok := r.deps.Account.(accountPaidReactionPrivacyService); ok {
next, err := svc.GetReactionSettings(ctx, userID)
if err != nil {
return nil, internalErr()
}
settings = next
}
return &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdatePaidReactionPrivacy{
Private: r.tgPaidReactionPrivacy(ctx, userID, settings.PaidPrivacy),
}},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}, nil
}
func (r *Router) onMessagesTogglePaidReactionPrivacy(ctx context.Context, req *tg.MessagesTogglePaidReactionPrivacyRequest) (bool, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return false, messageIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return false, err
}
privacy, err := r.domainPaidReactionPrivacy(ctx, userID, req.Private)
if err != nil {
return false, err
}
if svc, ok := r.deps.Account.(accountPaidReactionPrivacyService); ok {
next, err := svc.SetPaidReactionPrivacy(ctx, userID, privacy)
if err != nil {
return false, internalErr()
}
privacy = next.PaidPrivacy
}
r.pushUserUpdates(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdatePaidReactionPrivacy{Private: r.tgPaidReactionPrivacy(ctx, userID, privacy)}},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: int(r.clock.Now().Unix()),
Seq: 0,
})
return true, nil
}
func (r *Router) onMessagesSendPaidReaction(ctx context.Context, req *tg.MessagesSendPaidReactionRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if req.Count <= 0 {
return nil, starsAmountInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if private, ok := req.GetPrivate(); ok {
if _, err := r.domainPaidReactionPrivacy(ctx, userID, private); err != nil {
return nil, err
}
}
return nil, balanceTooLowErr()
}
func (r *Router) onMessagesDeleteParticipantReaction(ctx context.Context, req *tg.MessagesDeleteParticipantReactionRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
participant, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Participant)
if err != nil {
return nil, err
}
if participant.Type != domain.PeerTypeUser || participant.ID == 0 {
return nil, userIDInvalidErr()
}
if peer.Type != domain.PeerTypeChannel || r.deps.Channels == nil {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
moderator, ok := r.deps.Channels.(channelParticipantReactionModerator)
if !ok {
return nil, channelInvalidErr(domain.ErrChannelInvalid)
}
res, err := moderator.DeleteParticipantReaction(ctx, userID, domain.DeleteChannelParticipantReactionRequest{
UserID: userID,
ChannelID: peer.ID,
MessageID: req.MsgID,
ParticipantUserID: participant.ID,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelInvalidErr(err)
}
updates := r.channelMessageReactionsUpdates(ctx, userID, res)
r.pushChannelViewerUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelMessageReactionsUpdates(ctx, viewerUserID, res)
})
return updates, nil
}
func (r *Router) onMessagesDeleteParticipantReactions(ctx context.Context, req *tg.MessagesDeleteParticipantReactionsRequest) (bool, error) {
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return false, err
}
participant, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Participant)
if err != nil {
return false, err
}
if participant.Type != domain.PeerTypeUser || participant.ID == 0 {
return false, userIDInvalidErr()
}
if peer.Type != domain.PeerTypeChannel || r.deps.Channels == nil {
return true, nil
}
moderator, ok := r.deps.Channels.(channelParticipantReactionModerator)
if !ok {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
res, err := moderator.DeleteParticipantReactions(ctx, userID, domain.DeleteChannelParticipantReactionsRequest{
UserID: userID,
ChannelID: peer.ID,
ParticipantUserID: participant.ID,
Limit: domain.MaxDeleteParticipantReactionsBatch,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return false, channelInvalidErr(err)
}
if len(res.Messages) > 0 {
reactionRes := domain.ChannelMessageReactionsResult{
Channel: res.Channel,
Messages: res.Messages,
Recipients: res.Recipients,
}
ids := make([]int, 0, len(res.Messages))
for _, msg := range res.Messages {
if msg.ID > 0 {
ids = append(ids, msg.ID)
}
}
r.pushChannelViewerUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelMessagesReactionsUpdates(ctx, viewerUserID, reactionRes, ids)
})
}
return true, nil
}
func (r *Router) onMessagesGetMessagesReactions(ctx context.Context, req *tg.MessagesGetMessagesReactionsRequest) (tg.UpdatesClass, error) {
if len(req.ID) > maxGetMessagesIDs {
return nil, limitInvalidErr()
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
res, err := r.deps.Channels.GetMessageReactions(ctx, userID, domain.ChannelMessageReactionsRequest{
UserID: userID,
ChannelID: peer.ID,
IDs: append([]int(nil), req.ID...),
})
if err != nil {
return nil, channelInvalidErr(err)
}
return r.channelMessagesReactionsUpdates(ctx, userID, res, req.ID), nil
}
if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
res, err := r.deps.Messages.GetMessageReactions(ctx, userID, domain.PrivateMessageReactionsRequest{
OwnerUserID: userID,
Peer: peer,
IDs: append([]int(nil), req.ID...),
})
if err != nil {
return nil, messageReactionErr(err)
}
return r.privateMessagesReactionsUpdates(ctx, userID, peer, res, req.ID), nil
}
updates := make([]tg.UpdateClass, 0, len(req.ID))
tgPeer := tgPeer(peer)
for _, msgID := range req.ID {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
updates = append(updates, &tg.UpdateMessageReactions{
Peer: tgPeer,
MsgID: msgID,
Reactions: tg.MessageReactions{
Results: []tg.ReactionCount{},
},
})
}
return &tg.Updates{
Updates: updates,
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}, nil
}
func (r *Router) onMessagesGetMessageReactionsList(ctx context.Context, req *tg.MessagesGetMessageReactionsListRequest) (*tg.MessagesMessageReactionsList, error) {
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
if offset, ok := req.GetOffset(); ok && len(offset) > maxReactionListOffset {
return nil, limitInvalidErr()
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
filter, err := optionalDomainMessageReaction(req.Reaction)
if err != nil {
return nil, err
}
res, err := r.deps.Channels.ListMessageReactions(ctx, userID, domain.ChannelMessageReactionsListRequest{
UserID: userID,
ChannelID: peer.ID,
MessageID: req.ID,
Reaction: filter,
Offset: optionalString(req.GetOffset),
Limit: req.Limit,
})
if errors.Is(err, domain.ErrChannelRightForbidden) {
return nil, tgerr.New(403, "BROADCAST_FORBIDDEN")
}
if err != nil {
return nil, channelInvalidErr(err)
}
userIDs := make([]int64, 0, len(res.Reactions))
reactions := make([]tg.MessagePeerReaction, 0, len(res.Reactions))
for _, item := range res.Reactions {
if item.UserID != 0 {
userIDs = append(userIDs, item.UserID)
}
if converted := tgMessagePeerReaction(userID, item); converted != nil {
reactions = append(reactions, *converted)
}
}
out := &tg.MessagesMessageReactionsList{
Count: res.Count,
Reactions: reactions,
Chats: tgChannels(userID, []domain.Channel{res.Channel}),
Users: r.tgUsersForIDs(ctx, userID, userIDs),
}
if res.NextOffset != "" {
out.SetNextOffset(res.NextOffset)
}
return out, nil
}
if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
filter, err := optionalDomainMessageReaction(req.Reaction)
if err != nil {
return nil, err
}
res, err := r.deps.Messages.GetMessageReactions(ctx, userID, domain.PrivateMessageReactionsRequest{
OwnerUserID: userID,
Peer: peer,
IDs: []int{req.ID},
})
if err != nil {
return nil, messageReactionErr(err)
}
var source domain.ChannelMessageReactions
if len(res.Messages) > 0 && res.Messages[0].Reactions != nil {
source = *res.Messages[0].Reactions
} else {
source = res.Reactions
}
limit := req.Limit
if limit <= 0 || limit > len(source.Recent) {
limit = len(source.Recent)
}
userIDs := []int64{userID, peer.ID}
reactions := make([]tg.MessagePeerReaction, 0, limit)
count := 0
for _, item := range source.Recent {
if filter != nil && (item.Reaction.Type != filter.Type || item.Reaction.Emoticon != filter.Emoticon) {
continue
}
count++
if len(reactions) >= limit {
continue
}
if item.UserID != 0 {
userIDs = append(userIDs, item.UserID)
}
if converted := tgMessagePeerReaction(userID, item); converted != nil {
reactions = append(reactions, *converted)
}
}
return &tg.MessagesMessageReactionsList{
Count: count,
Reactions: reactions,
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: r.tgUsersForIDs(ctx, userID, userIDs),
}, nil
}
return &tg.MessagesMessageReactionsList{
Count: 0,
Reactions: []tg.MessagePeerReaction{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) channelMessageReactionsUpdates(ctx context.Context, viewerUserID int64, res domain.ChannelMessageReactionsResult) *tg.Updates {
ids := []int{res.Message.ID}
if res.Message.ID <= 0 && len(res.Messages) > 0 {
ids = []int{res.Messages[0].ID}
}
return r.channelMessagesReactionsUpdates(ctx, viewerUserID, res, ids)
}
func (r *Router) privateMessageReactionsUpdates(ctx context.Context, viewerUserID int64, peer domain.Peer, res domain.PrivateMessageReactionsResult) *tg.Updates {
ids := make([]int, 0, 1)
for _, msg := range res.Messages {
if msg.OwnerUserID == viewerUserID && msg.ID > 0 {
ids = append(ids, msg.ID)
break
}
}
return r.privateMessagesReactionsUpdates(ctx, viewerUserID, peer, res, ids)
}
func (r *Router) privateMessagesReactionsUpdates(ctx context.Context, viewerUserID int64, peer domain.Peer, res domain.PrivateMessageReactionsResult, ids []int) *tg.Updates {
updates := make([]tg.UpdateClass, 0, len(ids))
messagesByID := make(map[int]domain.Message, len(res.Messages))
userIDs := []int64{viewerUserID}
if peer.Type == domain.PeerTypeUser && peer.ID != 0 {
userIDs = append(userIDs, peer.ID)
}
for _, msg := range res.Messages {
if msg.OwnerUserID != viewerUserID || msg.ID == 0 {
continue
}
messagesByID[msg.ID] = msg
if msg.Peer.Type == domain.PeerTypeUser && msg.Peer.ID != 0 {
userIDs = append(userIDs, msg.Peer.ID)
}
if msg.From.Type == domain.PeerTypeUser && msg.From.ID != 0 {
userIDs = append(userIDs, msg.From.ID)
}
if msg.Reactions != nil {
userIDs = append(userIDs, channelMessageReactionUserIDs(*msg.Reactions)...)
}
}
userIDs = append(userIDs, channelMessageReactionUserIDs(res.Reactions)...)
fallbackPeer := tgPeer(peer)
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
continue
}
msg, ok := messagesByID[id]
outPeer := fallbackPeer
reactions := domain.ChannelMessageReactions{
CanSeeList: true,
Results: []domain.ChannelMessageReactionCount{},
Recent: []domain.ChannelMessagePeerReaction{},
}
if ok {
outPeer = tgPeer(msg.Peer)
if msg.Reactions != nil {
reactions = *msg.Reactions
}
}
if outPeer == nil {
continue
}
converted := tgMessageReactions(viewerUserID, &reactions)
if converted == nil {
converted = &tg.MessageReactions{Results: []tg.ReactionCount{}}
}
updates = append(updates, &tg.UpdateMessageReactions{
Peer: outPeer,
MsgID: id,
Reactions: *converted,
})
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, userIDs),
Chats: []tg.ChatClass{},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) channelMessagesReactionsUpdates(ctx context.Context, viewerUserID int64, res domain.ChannelMessageReactionsResult, ids []int) *tg.Updates {
updates := make([]tg.UpdateClass, 0, len(ids))
messagesByID := make(map[int]domain.ChannelMessage, len(res.Messages)+1)
if res.Message.ID != 0 {
messagesByID[res.Message.ID] = res.Message
}
for _, msg := range res.Messages {
if msg.ID != 0 {
messagesByID[msg.ID] = msg
}
}
userIDs := make([]int64, 0)
for _, msg := range messagesByID {
if msg.Reactions != nil {
userIDs = append(userIDs, channelMessageReactionUserIDs(*msg.Reactions)...)
}
}
userIDs = append(userIDs, channelMessageReactionUserIDs(res.Reactions)...)
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
continue
}
msg, ok := messagesByID[id]
reactions := domain.ChannelMessageReactions{
CanSeeList: !res.Channel.Broadcast || res.Channel.Megagroup,
Results: []domain.ChannelMessageReactionCount{},
Recent: []domain.ChannelMessagePeerReaction{},
}
if ok && msg.Reactions != nil {
reactions = *msg.Reactions
} else if ok && len(res.Reactions.Results) > 0 && res.Message.ID == id {
reactions = res.Reactions
}
converted := tgMessageReactions(viewerUserID, &reactions)
if converted == nil {
converted = &tg.MessageReactions{Results: []tg.ReactionCount{}}
}
update := &tg.UpdateMessageReactions{
Peer: &tg.PeerChannel{ChannelID: res.Channel.ID},
MsgID: id,
Reactions: *converted,
}
if ok {
if topID := channelMessageThreadRootID(msg); topID > 0 && topID != id {
update.SetTopMsgID(topID)
}
}
updates = append(updates, update)
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, userIDs),
Chats: tgChannels(viewerUserID, []domain.Channel{res.Channel}),
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func channelMessageReactionUserIDs(reactions domain.ChannelMessageReactions) []int64 {
out := make([]int64, 0, len(reactions.Recent))
for _, item := range reactions.Recent {
if item.UserID != 0 {
out = append(out, item.UserID)
}
}
return out
}
func (r *Router) onMessagesGetUnreadReactions(ctx context.Context, req *tg.MessagesGetUnreadReactionsRequest) (tg.MessagesMessagesClass, error) {
if err := validateHistoryBounds(req.OffsetID, req.AddOffset, req.Limit, req.MaxID, req.MinID); err != nil {
return nil, err
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, req.GetSavedPeerID)
if err != nil {
return nil, err
}
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, messageIDInvalidErr()
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
history, err := r.deps.Channels.GetUnreadReactions(ctx, userID, domain.ChannelUnreadReactionsFilter{
ChannelID: peer.ID,
TopMsgID: req.TopMsgID,
OffsetID: req.OffsetID,
AddOffset: req.AddOffset,
Limit: req.Limit,
MaxID: req.MaxID,
MinID: req.MinID,
})
if err != nil {
return nil, channelInvalidErr(err)
}
return tgChannelHistoryMessages(userID, r.enrichChannelHistory(ctx, userID, history)), nil
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Topics: []tg.ForumTopicClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesReadReactions(ctx context.Context, req *tg.MessagesReadReactionsRequest) (*tg.MessagesAffectedHistory, error) {
authKeyID, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if savedPeer, ok := req.GetSavedPeerID(); ok && savedPeer != nil {
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, savedPeer); err != nil {
return nil, err
}
}
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, messageIDInvalidErr()
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
res, err := r.deps.Channels.ReadReactions(ctx, userID, domain.ReadChannelReactionsRequest{
UserID: userID,
ChannelID: peer.ID,
TopMsgID: req.TopMsgID,
Limit: domain.MaxChannelReadReactionsBatch,
})
if err != nil {
return nil, channelInvalidErr(err)
}
return &tg.MessagesAffectedHistory{Pts: res.ChannelPts, PtsCount: 0, Offset: res.Offset}, nil
}
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
func (r *Router) onMessagesSendVote(ctx context.Context, req *tg.MessagesSendVoteRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if err := validatePollOptions(req.Options, true); err != nil {
return nil, err
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return nil, messageIDInvalidErr()
}
func (r *Router) onMessagesGetPollResults(ctx context.Context, req *tg.MessagesGetPollResultsRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
func (r *Router) onMessagesGetPollVotes(ctx context.Context, req *tg.MessagesGetPollVotesRequest) (*tg.MessagesVotesList, error) {
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
return nil, limitInvalidErr()
}
if option, ok := req.GetOption(); ok {
if err := validatePollOption(option); err != nil {
return nil, err
}
}
if offset, ok := req.GetOffset(); ok && len(offset) > maxPollVotesOffsetLength {
return nil, limitInvalidErr()
}
userID, peer, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
if err != nil {
return nil, channelInvalidErr(err)
}
if view.Channel.Broadcast && !view.Channel.Megagroup {
return nil, tgerr.New(403, "BROADCAST_FORBIDDEN")
}
}
return &tg.MessagesVotesList{
Count: 0,
Votes: []tg.MessagePeerVoteClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesAddPollAnswer(ctx context.Context, req *tg.MessagesAddPollAnswerRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if err := validatePollAnswer(req.Answer); err != nil {
return nil, err
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return nil, messageIDInvalidErr()
}
func (r *Router) onMessagesDeletePollAnswer(ctx context.Context, req *tg.MessagesDeletePollAnswerRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if err := validatePollOption(req.Option); err != nil {
return nil, err
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return nil, messageIDInvalidErr()
}
func (r *Router) onMessagesGetUnreadPollVotes(ctx context.Context, req *tg.MessagesGetUnreadPollVotesRequest) (tg.MessagesMessagesClass, error) {
if err := validateHistoryBounds(req.OffsetID, req.AddOffset, req.Limit, req.MaxID, req.MinID); err != nil {
return nil, err
}
userID, _, err := r.reactionPeer(ctx, req.Peer, nil)
if err != nil {
return nil, err
}
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, messageIDInvalidErr()
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Topics: []tg.ForumTopicClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesReadPollVotes(ctx context.Context, req *tg.MessagesReadPollVotesRequest) (*tg.MessagesAffectedHistory, error) {
authKeyID, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, messageIDInvalidErr()
}
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
func (r *Router) onMessagesAppendTodoList(ctx context.Context, req *tg.MessagesAppendTodoListRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if len(req.List) == 0 {
return nil, todoNotModifiedErr()
}
if err := validateTodoItems(req.List); err != nil {
return nil, err
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return nil, messageIDInvalidErr()
}
func (r *Router) onMessagesToggleTodoCompleted(ctx context.Context, req *tg.MessagesToggleTodoCompletedRequest) (tg.UpdatesClass, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if len(req.Completed) == 0 && len(req.Incompleted) == 0 {
return nil, todoNotModifiedErr()
}
if err := validateTodoIDVector(req.Completed, req.Incompleted); err != nil {
return nil, err
}
if _, _, err := r.reactionPeer(ctx, req.Peer, nil); err != nil {
return nil, err
}
return nil, messageIDInvalidErr()
}
func (r *Router) reactionPeer(ctx context.Context, peer tg.InputPeerClass, savedPeer func() (tg.InputPeerClass, bool)) (int64, domain.Peer, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return 0, domain.Peer{}, internalErr()
}
out, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return 0, domain.Peer{}, err
}
if savedPeer != nil {
if input, ok := savedPeer(); ok && input != nil {
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input); err != nil {
return 0, domain.Peer{}, err
}
}
}
return userID, out, nil
}
func (r *Router) domainPaidReactionPrivacy(ctx context.Context, userID int64, in tg.PaidReactionPrivacyClass) (domain.PaidReactionPrivacy, error) {
switch typed := in.(type) {
case nil, *tg.PaidReactionPrivacyDefault:
return domain.PaidReactionPrivacy{Kind: domain.PaidReactionPrivacyDefault}, nil
case *tg.PaidReactionPrivacyAnonymous:
return domain.PaidReactionPrivacy{Kind: domain.PaidReactionPrivacyAnonymous}, nil
case *tg.PaidReactionPrivacyPeer:
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, typed.Peer)
if err != nil {
return domain.PaidReactionPrivacy{}, err
}
return domain.PaidReactionPrivacy{Kind: domain.PaidReactionPrivacyPeer, Peer: &peer}, nil
default:
return domain.PaidReactionPrivacy{}, inputConstructorInvalidErr()
}
}
func (r *Router) tgPaidReactionPrivacy(ctx context.Context, userID int64, in domain.PaidReactionPrivacy) tg.PaidReactionPrivacyClass {
switch in.Kind {
case domain.PaidReactionPrivacyAnonymous:
return &tg.PaidReactionPrivacyAnonymous{}
case domain.PaidReactionPrivacyPeer:
if in.Peer == nil {
return &tg.PaidReactionPrivacyDefault{}
}
if peer := r.inputPeerForDomainPeer(ctx, userID, *in.Peer); peer != nil {
return &tg.PaidReactionPrivacyPeer{Peer: peer}
}
}
return &tg.PaidReactionPrivacyDefault{}
}
func (r *Router) inputPeerForDomainPeer(ctx context.Context, currentUserID int64, peer domain.Peer) tg.InputPeerClass {
switch peer.Type {
case domain.PeerTypeUser:
switch {
case peer.ID == domain.OfficialSystemUserID:
u := domain.OfficialSystemUser()
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
case r.deps.Users == nil:
return nil
case peer.ID == currentUserID:
u, err := r.deps.Users.Self(ctx, currentUserID)
if err != nil || u.ID == 0 {
return nil
}
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
default:
u, found, err := r.deps.Users.ByID(ctx, currentUserID, peer.ID)
if err != nil || !found {
return nil
}
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
}
case domain.PeerTypeChannel:
if r.deps.Channels == nil || peer.ID == 0 {
return nil
}
view, err := r.deps.Channels.GetChannel(ctx, currentUserID, peer.ID)
if err != nil || view.Channel.ID == 0 {
return nil
}
return &tg.InputPeerChannel{ChannelID: view.Channel.ID, AccessHash: view.Channel.AccessHash}
default:
return nil
}
}
func domainMessageReactionsFromTL(req *tg.MessagesSendReactionRequest) ([]domain.MessageReaction, error) {
if req == nil {
return nil, nil
}
reactions, ok := req.GetReaction()
if !ok || len(reactions) == 0 {
return nil, nil
}
out := make([]domain.MessageReaction, 0, len(reactions))
seen := make(map[string]struct{}, len(reactions))
for _, reaction := range reactions {
parsed, err := domainMessageReactionFromTL(reaction)
if err != nil {
return nil, err
}
key := string(parsed.Type) + "\x00" + parsed.Emoticon
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, parsed)
}
return out, nil
}
func optionalDomainMessageReaction(reaction tg.ReactionClass) (*domain.MessageReaction, error) {
if reaction == nil {
return nil, nil
}
out, err := domainMessageReactionFromTL(reaction)
if err != nil {
return nil, err
}
return &out, nil
}
func domainMessageReactionFromTL(reaction tg.ReactionClass) (domain.MessageReaction, error) {
switch typed := reaction.(type) {
case *tg.ReactionEmoji:
emoticon := strings.TrimSpace(typed.Emoticon)
if emoticon == "" || utf8.RuneCountInString(emoticon) > domain.MaxChannelReactionEmoticonLength {
return domain.MessageReaction{}, reactionInvalidErr()
}
return domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon}, nil
case nil, *tg.ReactionEmpty, *tg.ReactionCustomEmoji, *tg.ReactionPaid:
return domain.MessageReaction{}, reactionInvalidErr()
default:
return domain.MessageReaction{}, inputConstructorInvalidErr()
}
}
func optionalString(get func() (string, bool)) string {
if get == nil {
return ""
}
value, ok := get()
if !ok {
return ""
}
return value
}
func validateEmojiLangCode(langcode string) error {
if langcode == "" || len(langcode) > maxEmojiLangCodeLength {
return limitInvalidErr()
}
for _, c := range langcode {
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '-' || c == '_':
default:
return limitInvalidErr()
}
}
return nil
}
func messagesReactionsEmpty(hash int64) tg.MessagesReactionsClass {
if hash != 0 {
return &tg.MessagesReactionsNotModified{}
}
return &tg.MessagesReactions{
Hash: 0,
Reactions: []tg.ReactionClass{},
}
}
func messagesReactionsFromDomain(reactions []domain.MessageReaction, requestHash int64) tg.MessagesReactionsClass {
hash := messageReactionListHash(reactions)
if hash != 0 && requestHash == hash {
return &tg.MessagesReactionsNotModified{}
}
out := make([]tg.ReactionClass, 0, len(reactions))
for _, reaction := range reactions {
tgReaction := tgMessageReaction(reaction)
if tgReaction != nil {
out = append(out, tgReaction)
}
}
return &tg.MessagesReactions{
Hash: hash,
Reactions: out,
}
}
func savedReactionTagsEmpty(_ int64) tg.MessagesSavedReactionTagsClass {
return &tg.MessagesSavedReactionTags{
Tags: []tg.SavedReactionTag{},
Hash: 0,
}
}
func savedReactionTagsFromDomain(tags []domain.SavedReactionTag, requestHash int64) tg.MessagesSavedReactionTagsClass {
hash := savedReactionTagListHash(tags)
if hash != 0 && requestHash == hash {
return &tg.MessagesSavedReactionTagsNotModified{}
}
out := make([]tg.SavedReactionTag, 0, len(tags))
for _, tag := range tags {
reaction := tgMessageReaction(tag.Reaction)
if reaction == nil {
continue
}
item := tg.SavedReactionTag{
Reaction: reaction,
Count: tag.Count,
}
if tag.Title != "" {
item.SetTitle(tag.Title)
}
out = append(out, item)
}
if len(out) == 0 {
return savedReactionTagsEmpty(requestHash)
}
return &tg.MessagesSavedReactionTags{
Tags: out,
Hash: hash,
}
}
func (r *Router) reactionsWithCatalogFallback(ctx context.Context, reactions []domain.MessageReaction, limit int) []domain.MessageReaction {
return mergeReactionCatalogFallback(reactions, r.availableReactionCatalog(ctx, limit), limit)
}
func reactionsWithCatalogFallback(reactions []domain.MessageReaction, limit int) []domain.MessageReaction {
return mergeReactionCatalogFallback(reactions, staticReactionCatalog(), limit)
}
func (r *Router) availableReactionCatalog(ctx context.Context, limit int) []domain.MessageReaction {
if limit <= 0 {
return nil
}
if r.deps.Files != nil {
catalog, err := r.deps.Files.ListAvailableReactions(ctx)
if err == nil {
out := make([]domain.MessageReaction, 0, min(limit, len(catalog)))
for _, item := range catalog {
emoticon := strings.TrimSpace(item.Reaction)
if item.Inactive || emoticon == "" {
continue
}
out = append(out, domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon})
if len(out) >= limit {
return out
}
}
if len(out) > 0 {
return out
}
}
}
return staticReactionCatalog()
}
func staticReactionCatalog() []domain.MessageReaction {
emoticons := tdesktop.DefaultReactionEmoticons()
out := make([]domain.MessageReaction, 0, len(emoticons))
for _, emoticon := range emoticons {
out = append(out, domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon})
}
return out
}
func mergeReactionCatalogFallback(reactions, fallback []domain.MessageReaction, limit int) []domain.MessageReaction {
if limit <= 0 {
return []domain.MessageReaction{}
}
if limit > domain.MaxTopMessageReactions {
limit = domain.MaxTopMessageReactions
}
out := make([]domain.MessageReaction, 0, limit)
seen := make(map[string]struct{}, limit)
for _, reaction := range reactions {
if reaction.Type != domain.MessageReactionEmoji || strings.TrimSpace(reaction.Emoticon) == "" {
continue
}
key := string(reaction.Type) + "\x00" + reaction.Emoticon
if _, ok := seen[key]; ok {
continue
}
out = append(out, reaction)
seen[key] = struct{}{}
if len(out) >= limit {
return out
}
}
for _, reaction := range fallback {
key := string(reaction.Type) + "\x00" + reaction.Emoticon
if _, ok := seen[key]; ok {
continue
}
out = append(out, reaction)
seen[key] = struct{}{}
if len(out) >= limit {
return out
}
}
return out
}
func messageReactionListHash(reactions []domain.MessageReaction) int64 {
if len(reactions) == 0 {
return 0
}
h := fnv.New64a()
for _, reaction := range reactions {
_, _ = h.Write([]byte(reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(reaction.Emoticon))
_, _ = h.Write([]byte{0xff})
}
sum := int64(h.Sum64() & 0x7fffffffffffffff)
if sum == 0 {
return 1
}
return sum
}
func savedReactionTagListHash(tags []domain.SavedReactionTag) int64 {
if len(tags) == 0 {
return 0
}
h := fnv.New64a()
for _, tag := range tags {
_, _ = h.Write([]byte(tag.Reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(tag.Reaction.Emoticon))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(tag.Title))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(strconv.Itoa(tag.Count)))
_, _ = h.Write([]byte{0xff})
}
sum := int64(h.Sum64() & 0x7fffffffffffffff)
if sum == 0 {
return 1
}
return sum
}
func validateReactionClass(reaction tg.ReactionClass) error {
switch typed := reaction.(type) {
case *tg.ReactionEmoji:
if strings.TrimSpace(typed.Emoticon) == "" || utf8.RuneCountInString(typed.Emoticon) > maxReportOptionLength {
return reactionInvalidErr()
}
case *tg.ReactionCustomEmoji:
if typed.DocumentID <= 0 {
return reactionInvalidErr()
}
case *tg.ReactionPaid:
case nil, *tg.ReactionEmpty:
return reactionInvalidErr()
default:
return inputConstructorInvalidErr()
}
return nil
}
func validatePollOptions(options [][]byte, allowEmpty bool) error {
if len(options) == 0 {
if allowEmpty {
return nil
}
return optionInvalidErr()
}
if len(options) > maxPollVoteOptions {
return optionsTooMuchErr()
}
for _, option := range options {
if err := validatePollOption(option); err != nil {
return err
}
}
return nil
}
func validatePollOption(option []byte) error {
if len(option) == 0 || len(option) > maxPollOptionBytes {
return optionInvalidErr()
}
return nil
}
func validatePollAnswer(answer tg.PollAnswerClass) error {
if answer == nil {
return pollAnswerInvalidErr()
}
text := answer.GetText()
if strings.TrimSpace(text.Text) == "" || utf8.RuneCountInString(text.Text) > maxTodoTitleLength {
return pollAnswerInvalidErr()
}
if len(text.Entities) > maxMessageEntityCount {
return limitInvalidErr()
}
switch typed := answer.(type) {
case *tg.PollAnswer:
if err := validatePollOption(typed.Option); err != nil {
return err
}
if typed.Media != nil {
return mediaInvalidErr()
}
case *tg.InputPollAnswer:
if typed.Media != nil {
return mediaInvalidErr()
}
default:
return inputConstructorInvalidErr()
}
return nil
}
func validateTodoItems(items []tg.TodoItem) error {
if len(items) == 0 {
return todoItemsEmptyErr()
}
if len(items) > maxTodoItems {
return limitInvalidErr()
}
seen := make(map[int]struct{}, len(items))
for _, item := range items {
if item.ID < 0 || item.ID > maxTodoItems {
return messageIDInvalidErr()
}
if item.ID != 0 {
if _, ok := seen[item.ID]; ok {
return tgerr.New(400, "TODO_ITEM_DUPLICATE")
}
seen[item.ID] = struct{}{}
}
if strings.TrimSpace(item.Title.Text) == "" || utf8.RuneCountInString(item.Title.Text) > maxTodoTitleLength {
return limitInvalidErr()
}
if len(item.Title.Entities) > maxMessageEntityCount {
return limitInvalidErr()
}
}
return nil
}
func validateTodoIDVector(vectors ...[]int) error {
total := 0
seen := map[int]struct{}{}
for _, ids := range vectors {
total += len(ids)
if total > maxTodoItems {
return limitInvalidErr()
}
for _, id := range ids {
if id <= 0 || id > maxTodoItems {
return messageIDInvalidErr()
}
if _, ok := seen[id]; ok {
return todoNotModifiedErr()
}
seen[id] = struct{}{}
}
}
return nil
}
func validateHistoryBounds(offsetID, addOffset, limit, maxID, minID int) error {
if offsetID < 0 || offsetID > domain.MaxMessageBoxID || maxID < 0 || maxID > domain.MaxMessageBoxID || minID < 0 || minID > domain.MaxMessageBoxID {
return messageIDInvalidErr()
}
if addOffset < -100 || addOffset > 100 || limit < 0 || limit > maxSearchResultsLimit {
return limitInvalidErr()
}
return nil
}
func validateSavedHistoryBounds(offsetID, offsetDate, addOffset, limit, maxID, minID int) error {
if err := validateHistoryBounds(offsetID, addOffset, limit, maxID, minID); err != nil {
return err
}
if offsetDate < 0 {
return limitInvalidErr()
}
return nil
}
func (r *Router) validateSavedHistoryParentPeer(ctx context.Context, userID int64, getParent func() (tg.InputPeerClass, bool)) (domain.Peer, bool, error) {
parentPeer, ok := getParent()
if !ok {
return domain.Peer{}, false, nil
}
if err := r.validateRequiredSavedHistoryParentPeer(ctx, userID, parentPeer); err != nil {
return domain.Peer{}, false, err
}
parent, _ := r.domainPeerFromInputPeer(userID, parentPeer)
return parent, true, nil
}
func (r *Router) validateRequiredSavedHistoryParentPeer(ctx context.Context, userID int64, parentPeer tg.InputPeerClass) error {
parent, err := r.checkedDomainPeerFromInputPeer(ctx, userID, parentPeer)
if err != nil || parent.Type != domain.PeerTypeChannel {
return parentPeerInvalidErr()
}
return nil
}
func (r *Router) savedHistoryChats(ctx context.Context, userID int64, hasParent bool, parent domain.Peer, peer tg.InputPeerClass) []tg.ChatClass {
if r.deps.Channels == nil {
return []tg.ChatClass{}
}
seen := make(map[int64]struct{}, 2)
out := make([]tg.ChatClass, 0, 2)
add := func(channelID int64) {
if channelID == 0 {
return
}
if _, ok := seen[channelID]; ok {
return
}
seen[channelID] = struct{}{}
view, err := r.deps.Channels.GetChannel(ctx, userID, channelID)
if err != nil || view.Channel.ID == 0 {
return
}
out = append(out, tgChannelChat(userID, view.Channel, &view.Self))
}
if hasParent && parent.Type == domain.PeerTypeChannel {
add(parent.ID)
}
if p, ok := r.domainPeerFromInputPeer(userID, peer); ok && p.Type == domain.PeerTypeChannel {
add(p.ID)
}
return out
}
func (r *Router) onMessagesGetDefaultHistoryTTL(ctx context.Context) (*tg.DefaultHistoryTTL, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
return &tg.DefaultHistoryTTL{Period: 0}, nil
}
func (r *Router) onMessagesGetSponsoredMessages(ctx context.Context, req *tg.MessagesGetSponsoredMessagesRequest) (tg.MessagesSponsoredMessagesClass, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
}
return &tg.MessagesSponsoredMessagesEmpty{}, nil
}
func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (*tg.MessagesAffectedMessages, error) {
id, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(ids) > maxGetMessagesIDs {
return nil, limitInvalidErr()
}
for _, msgID := range ids {
if msgID <= 0 || msgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
}
read := domain.ReadMessageContentsResult{OwnerUserID: userID}
if r.deps.Messages != nil {
read, err = r.deps.Messages.ReadMessageContents(ctx, userID, domain.ReadMessageContentsRequest{
OwnerUserID: userID,
IDs: ids,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: id,
OriginSessionID: sessionID,
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
return nil, messageIDInvalidErr()
}
return nil, internalErr()
}
}
affected := &tg.MessagesAffectedMessages{Pts: read.Event.Pts, PtsCount: read.Event.PtsCount}
if read.Event.Pts == 0 {
affected, err = r.affectedMessages(ctx, id, userID)
if err != nil {
return nil, err
}
}
if contentIDs := readMessageContentIDs(read.MessageIDs); len(contentIDs) > 0 {
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateReadMessagesContents{
Messages: contentIDs,
Pts: affected.Pts,
PtsCount: affected.PtsCount,
}},
Date: int(r.clock.Now().Unix()),
Seq: 0,
})
}
return affected, nil
}
func readMessageContentIDs(ids []int) []int {
if len(ids) == 0 {
return nil
}
out := make([]int, 0, len(ids))
seen := make(map[int]struct{}, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
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 {
history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, req.ID)
if err != nil {
return nil, channelInvalidErr(err)
}
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)
}
byID := make(map[int]domain.ChannelMessage, len(history.Messages))
for _, msg := range history.Messages {
byID[msg.ID] = msg
}
for i, id := range req.ID {
if count, ok := viewCounters.Views[id]; ok {
views[i].SetViews(count)
}
if replies := tgChannelMessageReplies(byID[id].Replies); replies != nil {
views[i].SetReplies(*replies)
}
}
channels := make([]domain.Channel, 0, 1+len(history.Channels))
channels = append(channels, history.Channel)
channels = append(channels, history.Channels...)
return &tg.MessagesMessageViews{
Views: views,
Chats: tgChannels(userID, channels),
Users: r.tgUsers(history.Users),
}, nil
}
return &tg.MessagesMessageViews{
Views: views,
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesGetUnreadMentions(ctx context.Context, req *tg.MessagesGetUnreadMentionsRequest) (tg.MessagesMessagesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit < 0 || req.Limit > domain.MaxChannelUnreadMentionsLimit {
return nil, limitInvalidErr()
}
if req.TopMsgID < 0 || req.TopMsgID > domain.MaxMessageBoxID ||
req.OffsetID < 0 || req.OffsetID > domain.MaxMessageBoxID ||
req.MaxID < 0 || req.MaxID > domain.MaxMessageBoxID ||
req.MinID < 0 || req.MinID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
history, err := r.deps.Channels.GetUnreadMentions(ctx, userID, domain.ChannelUnreadMentionsFilter{
ChannelID: peer.ID,
TopMsgID: req.TopMsgID,
OffsetID: req.OffsetID,
AddOffset: req.AddOffset,
Limit: req.Limit,
MaxID: req.MaxID,
MinID: req.MinID,
})
if err != nil {
return nil, channelInvalidErr(err)
}
return tgChannelHistoryMessages(userID, r.enrichChannelHistory(ctx, userID, history)), nil
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
func (r *Router) onMessagesReadMentions(ctx context.Context, req *tg.MessagesReadMentionsRequest) (*tg.MessagesAffectedHistory, error) {
id, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.TopMsgID < 0 || req.TopMsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
res, err := r.deps.Channels.ReadMentions(ctx, userID, domain.ReadChannelMentionsRequest{
UserID: userID,
ChannelID: peer.ID,
TopMsgID: req.TopMsgID,
Limit: domain.MaxChannelReadMentionsBatch,
})
if err != nil {
return nil, channelInvalidErr(err)
}
return &tg.MessagesAffectedHistory{Pts: res.ChannelPts, PtsCount: 0, Offset: res.Offset}, nil
}
return r.affectedHistory(ctx, id, userID, 0)
}
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()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
return nil, err
}
counters := make([]tg.MessagesSearchCounter, 0, len(req.Filters))
for _, filter := range req.Filters {
if filter == nil {
continue
}
counters = append(counters, tg.MessagesSearchCounter{Filter: filter, Count: 0})
}
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 tgChannelHistoryMessages(userID, r.enrichChannelHistory(ctx, userID, replies)), nil
}
return &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, 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)
}
return tgMessagesDiscussionMessage(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
}
discussion, err := r.deps.Channels.GetDiscussionMessage(ctx, userID, peer.ID, req.MsgID)
if err != nil {
return false, channelInvalidErr(err)
}
readChannelID := peer.ID
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: int(r.clock.Now().Unix()),
})
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) onMessagesGetForumTopics(ctx context.Context, req *tg.MessagesGetForumTopicsRequest) (*tg.MessagesForumTopics, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit < 0 || req.Limit > 100 || utf8.RuneCountInString(req.Q) > maxMessageSearchQLength {
return nil, limitInvalidErr()
}
view, err := r.forumTopicPeerView(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if !view.Channel.Forum {
return nil, channelForumMissingErr()
}
includeGeneral := req.Limit > 0 && forumTopicQueryMatchesGeneral(req.Q)
list := domain.ChannelForumTopicList{}
if r.deps.Channels != nil && req.Limit > 0 {
limit := req.Limit
if includeGeneral {
limit--
}
if limit > 0 {
list, err = r.deps.Channels.GetForumTopics(ctx, userID, domain.ChannelForumTopicFilter{
ChannelID: view.Channel.ID,
Query: req.Q,
OffsetDate: req.OffsetDate,
OffsetID: req.OffsetID,
OffsetTopic: req.OffsetTopic,
Limit: limit,
})
if err != nil {
return nil, forumTopicError(err)
}
}
}
return r.forumTopicsResponse(ctx, userID, view, list, includeGeneral), nil
}
func (r *Router) onMessagesGetForumTopicsByID(ctx context.Context, req *tg.MessagesGetForumTopicsByIDRequest) (*tg.MessagesForumTopics, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(req.Topics) > maxForumTopicIDs {
return nil, limitInvalidErr()
}
view, err := r.forumTopicPeerView(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if !view.Channel.Forum {
return nil, channelForumMissingErr()
}
includeGeneral := false
ids := make([]int, 0, len(req.Topics))
for _, topicID := range req.Topics {
if topicID <= 0 || topicID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if topicID == forumGeneralTopicID {
includeGeneral = true
continue
}
ids = append(ids, topicID)
}
list := domain.ChannelForumTopicList{}
if r.deps.Channels != nil && len(ids) > 0 {
list, err = r.deps.Channels.GetForumTopicsByID(ctx, userID, view.Channel.ID, ids)
if err != nil {
return nil, forumTopicError(err)
}
}
return r.forumTopicsResponse(ctx, userID, view, list, includeGeneral), 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) channelOnlineCount(ctx context.Context, userID, channelID int64) int {
if channelID == 0 || r.deps.Channels == nil || r.deps.Sessions == nil {
return 1
}
provider, ok := r.deps.Sessions.(OnlineUserProvider)
if !ok {
return 1
}
online := provider.OnlineChannelUserIDs(channelID, domain.MaxChannelRealtimeFanout)
candidates := make([]int64, 0, len(online)+1)
if userID != 0 {
candidates = append(candidates, userID)
}
candidates = append(candidates, online...)
active, err := r.deps.Channels.FilterActiveMemberIDs(ctx, channelID, candidates)
if err != nil {
return 1
}
return len(active)
}
func (r *Router) onMessagesSetTyping(ctx context.Context, req *tg.MessagesSetTypingRequest) (bool, error) {
if req == nil {
return false, inputRequestInvalidErr()
}
topMsgID, topMsgIDSet := req.GetTopMsgID()
if !topMsgIDSet && req.TopMsgID != 0 {
topMsgID, topMsgIDSet = req.TopMsgID, true
}
if topMsgIDSet && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return false, msgIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
if peer.Type == domain.PeerTypeChannel && peer.ID != 0 && r.deps.Channels != nil {
action := req.Action
if action == nil {
action = &tg.SendMessageCancelAction{}
}
updates := &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateChannelUserTyping{
ChannelID: peer.ID,
FromID: &tg.PeerUser{UserID: userID},
TopMsgID: topMsgID,
Action: action,
}},
Date: int(r.clock.Now().Unix()),
}
r.pushChannelViewerUpdates(ctx, 0, peer.ID, nil, func(int64) *tg.Updates {
return updates
})
}
return true, nil
}
action := req.Action
if action == nil {
action = &tg.SendMessageCancelAction{}
}
update := &tg.UpdateUserTyping{
UserID: userID,
TopMsgID: topMsgID,
Action: action,
}
updates := &tg.UpdateShort{
Update: update,
Date: int(r.clock.Now().Unix()),
}
r.pushTypingUpdate(ctx, peer.ID, updates)
return true, nil
}
func (r *Router) pushTypingUpdate(ctx context.Context, targetUserID int64, updates *tg.UpdateShort) {
r.pushUserMessage(ctx, targetUserID, "push typing update", updates)
}
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)
return &tg.MessagesMessages{
Messages: out,
Users: r.usersForMessageUpdates(ctx, userID, found),
Chats: chats,
}, nil
}
func inputMessageBoxID(input tg.InputMessageClass) (int, bool) {
switch msg := input.(type) {
case *tg.InputMessageID:
return msg.ID, true
default:
return 0, false
}
}
func (r *Router) lookupOwnerMessage(ctx context.Context, userID int64, id int) (domain.Message, bool, error) {
filter := domain.MessageFilter{
MinID: id - 1,
Limit: 1,
}
if id < domain.MaxMessageBoxID {
filter.MaxID = id + 1
}
list, err := r.deps.Messages.Search(ctx, userID, filter)
if err != nil {
return domain.Message{}, false, err
}
if len(list.Messages) == 0 || list.Messages[0].ID != id {
return domain.Message{}, false, nil
}
return list.Messages[0], true, nil
}
func (r *Router) onMessagesSaveDraft(ctx context.Context, req *tg.MessagesSaveDraftRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
peerTL := tgPeer(peer)
if peerTL == nil {
return true, nil
}
date := int(r.clock.Now().Unix())
draft, err := r.dialogDraftFromSaveDraft(ctx, userID, peer, req, date)
if err != nil {
return false, err
}
if r.deps.Dialogs != nil {
if err := r.deps.Dialogs.SaveDraft(ctx, userID, draft); err != nil {
return false, dialogDraftErr(err)
}
}
update := &tg.UpdateDraftMessage{
Peer: peerTL,
Draft: tgDraftMessageFromSaveDraft(req, date),
}
if draft.TopMessageID > 0 {
update.SetTopMsgID(draft.TopMessageID)
}
updates := &tg.Updates{
Updates: []tg.UpdateClass{update},
Users: r.usersForDraftUpdate(ctx, userID, peer),
Chats: r.chatsForDraftUpdate(ctx, userID, peer),
Date: date,
Seq: 0,
}
r.pushDraftUpdate(ctx, userID, updates)
return true, nil
}
func (r *Router) onMessagesGetAllDrafts(ctx context.Context) (tg.UpdatesClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
date := int(r.clock.Now().Unix())
if r.deps.Dialogs == nil {
return &tg.Updates{Updates: []tg.UpdateClass{}, Users: []tg.UserClass{}, Chats: []tg.ChatClass{}, Date: date, Seq: 0}, nil
}
drafts, err := r.deps.Dialogs.ListDrafts(ctx, userID, domain.MaxDialogDraftsPerUser)
if err != nil {
return nil, dialogDraftErr(err)
}
updates := make([]tg.UpdateClass, 0, len(drafts))
users := r.usersForDrafts(ctx, userID, drafts)
chats := r.chatsForDrafts(ctx, userID, drafts)
for _, draft := range drafts {
peer := tgPeer(draft.Peer)
if peer == nil {
continue
}
update := &tg.UpdateDraftMessage{Peer: peer, Draft: tgDialogDraft(draft)}
if draft.TopMessageID > 0 {
update.SetTopMsgID(draft.TopMessageID)
}
updates = append(updates, update)
}
return &tg.Updates{Updates: updates, Users: users, Chats: chats, Date: date, Seq: 0}, nil
}
func (r *Router) onMessagesClearAllDrafts(ctx context.Context) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if r.deps.Dialogs == nil {
return true, nil
}
drafts, err := r.deps.Dialogs.ClearDrafts(ctx, userID, domain.MaxDialogDraftsPerUser)
if err != nil {
return false, dialogDraftErr(err)
}
if len(drafts) == 0 {
return true, nil
}
date := int(r.clock.Now().Unix())
updates := make([]tg.UpdateClass, 0, len(drafts))
for _, draft := range drafts {
update := draftClearUpdate(draft.Peer, draft.TopMessageID, date)
if update != nil {
updates = append(updates, update)
}
}
r.pushDraftUpdate(ctx, userID, &tg.Updates{
Updates: updates,
Users: r.usersForDrafts(ctx, userID, drafts),
Chats: r.chatsForDrafts(ctx, userID, drafts),
Date: date,
Seq: 0,
})
return true, nil
}
func (r *Router) dialogDraftFromSaveDraft(ctx context.Context, userID int64, peer domain.Peer, req *tg.MessagesSaveDraftRequest, date int) (domain.DialogDraft, error) {
if req == nil {
return domain.DialogDraft{Peer: peer, Date: date}, nil
}
if utf8.RuneCountInString(req.Message) > maxSendMessageTextLength {
return domain.DialogDraft{}, messageTooLongErr()
}
if len(req.Entities) > maxMessageEntityCount {
return domain.DialogDraft{}, limitInvalidErr()
}
if !req.SuggestedPost.Zero() {
return domain.DialogDraft{}, suggestedPostPeerInvalidErr()
}
replyTo, err := r.messageReplyFromInput(ctx, userID, peer, req.ReplyTo)
if err != nil {
return domain.DialogDraft{}, err
}
webpage, err := dialogDraftWebPageFromInput(req.Media)
if err != nil {
return domain.DialogDraft{}, err
}
topMessageID := 0
if replyTo != nil && peer.Type == domain.PeerTypeChannel && replyTo.TopMessageID > 0 {
topMessageID = replyTo.TopMessageID
}
return domain.DialogDraft{
Peer: peer,
TopMessageID: topMessageID,
Date: date,
NoWebpage: req.NoWebpage,
InvertMedia: req.InvertMedia,
Message: req.Message,
Entities: domainMessageEntities(req.Entities),
ReplyTo: replyTo,
WebPage: webpage,
Effect: req.Effect,
}, nil
}
func dialogDraftWebPageFromInput(media tg.InputMediaClass) (*domain.DialogDraftWebPage, error) {
switch m := media.(type) {
case nil, *tg.InputMediaEmpty:
return nil, nil
case *tg.InputMediaWebPage:
if m.URL == "" {
return nil, mediaInvalidErr()
}
return &domain.DialogDraftWebPage{
URL: m.URL,
ForceLargeMedia: m.ForceLargeMedia,
ForceSmallMedia: m.ForceSmallMedia,
Optional: m.Optional,
}, nil
default:
return nil, mediaInvalidErr()
}
}
func draftClearUpdate(peer domain.Peer, topMessageID, date int) *tg.UpdateDraftMessage {
peerTL := tgPeer(peer)
if peerTL == nil {
return nil
}
draft := &tg.DraftMessageEmpty{}
draft.SetDate(date)
update := &tg.UpdateDraftMessage{Peer: peerTL, Draft: draft}
if topMessageID > 0 {
update.SetTopMsgID(topMessageID)
}
return update
}
func tgDraftMessageFromSaveDraft(req *tg.MessagesSaveDraftRequest, date int) tg.DraftMessageClass {
if req == nil || saveDraftIsEmpty(req) {
draft := &tg.DraftMessageEmpty{}
draft.SetDate(date)
return draft
}
return &tg.DraftMessage{
NoWebpage: req.NoWebpage,
InvertMedia: req.InvertMedia,
ReplyTo: req.ReplyTo,
Message: req.Message,
Entities: req.Entities,
Media: draftInputMedia(req.Media),
Date: date,
Effect: req.Effect,
SuggestedPost: req.SuggestedPost,
}
}
func saveDraftIsEmpty(req *tg.MessagesSaveDraftRequest) bool {
return !req.NoWebpage &&
!req.InvertMedia &&
draftReplyIsEmpty(req.ReplyTo) &&
req.Message == "" &&
len(req.Entities) == 0 &&
draftInputMedia(req.Media) == nil &&
req.Effect == 0 &&
req.SuggestedPost.Zero()
}
func draftReplyIsEmpty(reply tg.InputReplyToClass) bool {
if reply == nil {
return true
}
input, ok := reply.(*tg.InputReplyToMessage)
if !ok {
return false
}
topMsgID, hasTopMsgID := input.GetTopMsgID()
return input.ReplyToMsgID == 0 && hasTopMsgID && topMsgID > 0
}
func draftInputMedia(media tg.InputMediaClass) tg.InputMediaClass {
switch media.(type) {
case nil, *tg.InputMediaEmpty:
return nil
default:
return media
}
}
func (r *Router) usersForDraftUpdate(ctx context.Context, userID int64, peer domain.Peer) []tg.UserClass {
if r.deps.Users == nil {
return []tg.UserClass{}
}
users := make([]tg.UserClass, 0, 2)
seen := map[int64]struct{}{}
if self, err := r.deps.Users.Self(ctx, userID); err == nil && self.ID != 0 {
users = append(users, r.tgSelfUser(self))
seen[self.ID] = struct{}{}
}
if peer.Type == domain.PeerTypeUser {
if _, ok := seen[peer.ID]; !ok {
if u, found, err := r.deps.Users.ByID(ctx, userID, peer.ID); err == nil && found && u.ID != 0 {
users = append(users, r.tgUser(u))
}
}
}
return users
}
func (r *Router) usersForDrafts(ctx context.Context, userID int64, drafts []domain.DialogDraft) []tg.UserClass {
users := make([]tg.UserClass, 0, len(drafts)+1)
seen := map[int64]struct{}{}
if r.deps.Users != nil {
if self, err := r.deps.Users.Self(ctx, userID); err == nil && self.ID != 0 {
users = append(users, r.tgSelfUser(self))
seen[self.ID] = struct{}{}
}
}
for _, draft := range drafts {
if draft.Peer.Type != domain.PeerTypeUser || draft.Peer.ID == 0 {
continue
}
if _, ok := seen[draft.Peer.ID]; ok {
continue
}
if r.deps.Users != nil {
if u, found, err := r.deps.Users.ByID(ctx, userID, draft.Peer.ID); err == nil && found && u.ID != 0 {
users = append(users, r.tgUser(u))
seen[u.ID] = struct{}{}
}
}
}
return users
}
func (r *Router) chatsForDraftUpdate(ctx context.Context, userID int64, peer domain.Peer) []tg.ChatClass {
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 || r.deps.Channels == nil {
return []tg.ChatClass{}
}
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
if err != nil || view.Channel.ID == 0 {
return []tg.ChatClass{}
}
return []tg.ChatClass{tgChannelChat(userID, view.Channel, &view.Self)}
}
func (r *Router) chatsForDrafts(ctx context.Context, userID int64, drafts []domain.DialogDraft) []tg.ChatClass {
if r.deps.Channels == nil {
return []tg.ChatClass{}
}
chats := make([]tg.ChatClass, 0)
seen := map[int64]struct{}{}
for _, draft := range drafts {
if draft.Peer.Type != domain.PeerTypeChannel || draft.Peer.ID == 0 {
continue
}
if _, ok := seen[draft.Peer.ID]; ok {
continue
}
seen[draft.Peer.ID] = struct{}{}
view, err := r.deps.Channels.GetChannel(ctx, userID, draft.Peer.ID)
if err != nil || view.Channel.ID == 0 {
continue
}
chats = append(chats, tgChannelChat(userID, view.Channel, &view.Self))
}
return chats
}
func dialogDraftErr(err error) error {
switch {
case err == nil:
return nil
case errors.Is(err, domain.ErrReplyMessageIDInvalid):
return replyMessageIDInvalidErr()
case errors.Is(err, domain.ErrChannelInvalid):
return peerIDInvalidErr()
default:
return internalErr()
}
}
func (r *Router) pushDraftUpdate(ctx context.Context, userID int64, updates *tg.Updates) {
r.pushUserMessage(ctx, userID, "push draft update", updates)
}
func (r *Router) clearDraftAfterSend(ctx context.Context, userID int64, peer domain.Peer, replyTo *domain.MessageReply) {
if r.deps.Dialogs == nil || userID == 0 || peer.ID == 0 {
return
}
topMessageID := 0
if peer.Type == domain.PeerTypeChannel && replyTo != nil && replyTo.TopMessageID > 0 {
topMessageID = replyTo.TopMessageID
}
changed, err := r.deps.Dialogs.DeleteDraft(ctx, userID, peer, topMessageID)
if err != nil {
r.log.Debug("clear draft after send", zap.Int64("user_id", userID), zap.Error(err))
return
}
if !changed {
return
}
date := int(r.clock.Now().Unix())
update := draftClearUpdate(peer, topMessageID, date)
if update == nil {
return
}
r.pushDraftUpdate(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{update},
Users: r.usersForDraftUpdate(ctx, userID, peer),
Chats: r.chatsForDraftUpdate(ctx, userID, peer),
Date: date,
Seq: 0,
})
}
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)
if query == "" {
return nil, searchQueryEmptyErr()
}
if utf8.RuneCountInString(query) > maxMessageSearchQLength {
return nil, limitInvalidErr()
}
if searchFilterNeedsMediaStore(req.Filter) {
return &tg.MessagesMessages{}, nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
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
}
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,
}
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.withMessageListPresence(private)
}
}
if req.UsersOnly || r.deps.Channels == nil {
return tgMessagesMessages(userID, r.withMessageListPresence(limitMessageList(private, limit))), nil
}
channelHistory, err := r.deps.Channels.SearchJoinedMessages(ctx, userID, domain.ChannelGlobalSearchRequest{
Query: query,
BroadcastsOnly: req.BroadcastsOnly,
GroupsOnly: req.GroupsOnly,
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 tgGlobalChannelMessages(userID, limitChannelHistory(channelHistory, limit)), nil
}
return tgGlobalSearchMessages(userID, limit, private, channelHistory), nil
}
func (r *Router) searchGlobalChannelOffsetID(ctx context.Context, userID int64, peer tg.InputPeerClass) (int64, error) {
if peer == nil {
return 0, nil
}
switch peer.(type) {
case *tg.InputPeerEmpty:
return 0, nil
}
ref, ok := inputPeerChannelRef(peer)
if !ok {
return 0, nil
}
if ref.ID <= 0 {
return 0, peerIDInvalidErr()
}
if ref.CheckAccessHash && r.deps.Channels != nil {
view, err := r.deps.Channels.GetChannel(ctx, userID, ref.ID)
if err != nil {
return 0, channelInvalidErr(err)
}
if !inputChannelAccessHashMatches(ref, view.Channel) {
return 0, channelInvalidErr(domain.ErrChannelPrivate)
}
}
return ref.ID, nil
}
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
}
type globalSearchHit struct {
date int
peerRank int64
messageID int
message tg.MessageClass
}
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)
}
users := append(tgUsers(private.Users), tgUsers(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) onMessagesGetDialogFilters(ctx context.Context) (*tg.MessagesDialogFilters, error) {
if r.deps.Dialogs == nil {
return tgDialogFilters(domain.DialogFolderList{}), nil
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
list, err := r.deps.Dialogs.GetDialogFolders(ctx, userID)
if err != nil {
return nil, internalErr()
}
return tgDialogFilters(list), nil
}
func (r *Router) onMessagesUpdateDialogFilter(ctx context.Context, req *tg.MessagesUpdateDialogFilterRequest) (bool, error) {
if req.ID < domain.DialogCustomFolderMinID {
return false, filterIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
filter, ok := req.GetFilter()
var folder *domain.DialogFolder
if ok {
parsed, err := r.dialogFolderFromTG(ctx, userID, req.ID, filter)
if err != nil {
return false, err
}
folder = &parsed
if r.deps.Dialogs != nil {
if err := r.deps.Dialogs.SaveDialogFolder(ctx, userID, parsed); err != nil {
return false, internalErr()
}
}
} else if r.deps.Dialogs != nil {
if err := r.deps.Dialogs.DeleteDialogFolder(ctx, userID, req.ID); err != nil {
return false, internalErr()
}
}
event := domain.UpdateEvent{
Type: domain.UpdateEventDialogFilter,
FilterID: req.ID,
DialogFilter: folder,
Date: int(r.clock.Now().Unix()),
}
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
event, _, err = r.deps.Updates.RecordDialogFilter(ctx, authKeyID, userID, req.ID, folder, sessionID)
if err != nil {
return false, internalErr()
}
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event))
return true, nil
}
func (r *Router) onMessagesUpdateDialogFiltersOrder(ctx context.Context, order []int) (bool, error) {
if len(order) > domain.MaxDialogFolders {
return false, limitInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
clean := cleanDialogFilterOrder(order)
if r.deps.Dialogs != nil {
if err := r.deps.Dialogs.ReorderDialogFolders(ctx, userID, clean); err != nil {
return false, internalErr()
}
}
event := domain.UpdateEvent{
Type: domain.UpdateEventDialogFilterOrder,
FilterOrder: clean,
Date: int(r.clock.Now().Unix()),
}
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
event, _, err = r.deps.Updates.RecordDialogFilterOrder(ctx, authKeyID, userID, clean, sessionID)
if err != nil {
return false, internalErr()
}
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event))
return true, nil
}
func (r *Router) onMessagesToggleDialogFilterTags(ctx context.Context, enabled bool) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if r.deps.Dialogs != nil {
if err := r.deps.Dialogs.ToggleDialogFolderTags(ctx, userID, enabled); err != nil {
return false, internalErr()
}
}
event := domain.UpdateEvent{
Type: domain.UpdateEventDialogFilters,
TagsEnabled: enabled,
Date: int(r.clock.Now().Unix()),
}
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
event, _, err = r.deps.Updates.RecordDialogFiltersReload(ctx, authKeyID, userID, sessionID)
if err != nil {
return false, internalErr()
}
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event))
return true, nil
}
func (r *Router) onMessagesSaveDefaultSendAs(ctx context.Context, req *tg.MessagesSaveDefaultSendAsRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if userID == 0 {
return false, peerIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if peer.Type != domain.PeerTypeChannel {
return false, peerIDInvalidErr()
}
sendAs, err := r.sendAsPeerFromInput(ctx, userID, peer, req.SendAs)
if err != nil {
return false, err
}
if r.deps.Channels == nil {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
if _, err := r.deps.Channels.SaveDefaultSendAs(ctx, userID, domain.SaveChannelDefaultSendAsRequest{
UserID: userID,
ChannelID: peer.ID,
SendAs: sendAs,
}); err != nil {
return false, channelInvalidErr(err)
}
return true, nil
}
func (r *Router) sendAsPeerFromInput(ctx context.Context, userID int64, to domain.Peer, input tg.InputPeerClass) (*domain.Peer, error) {
if input == nil {
return nil, nil
}
if to.Type != domain.PeerTypeChannel {
return nil, sendAsPeerInvalidErr()
}
sendAs, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
if err != nil {
return nil, sendAsPeerInvalidErr()
}
switch sendAs.Type {
case domain.PeerTypeUser:
if sendAs.ID != userID {
return nil, sendAsPeerInvalidErr()
}
return nil, nil
case domain.PeerTypeChannel:
if sendAs.ID != to.ID {
return nil, sendAsPeerInvalidErr()
}
if err := r.validateCurrentChannelSendAs(ctx, userID, to.ID); err != nil {
return nil, err
}
out := sendAs
return &out, nil
default:
return nil, sendAsPeerInvalidErr()
}
}
func (r *Router) resolveSendAsPeer(ctx context.Context, userID int64, to domain.Peer, input tg.InputPeerClass) (*domain.Peer, error) {
if input != nil {
return r.sendAsPeerFromInput(ctx, userID, to, input)
}
if to.Type != domain.PeerTypeChannel || r.deps.Channels == nil {
return nil, nil
}
view, err := r.deps.Channels.GetChannel(ctx, userID, to.ID)
if err != nil {
return nil, nil
}
return validDefaultSendAsPeer(view), nil
}
func validDefaultSendAsPeer(view domain.ChannelView) *domain.Peer {
if view.Dialog.DefaultSendAs == nil || view.Dialog.DefaultSendAs.ID == 0 {
return nil
}
switch view.Dialog.DefaultSendAs.Type {
case domain.PeerTypeUser:
return nil
case domain.PeerTypeChannel:
if view.Dialog.DefaultSendAs.ID != view.Channel.ID || !canCurrentChannelSendAs(view) {
return nil
}
out := *view.Dialog.DefaultSendAs
return &out
default:
return nil
}
}
func (r *Router) validateCurrentChannelSendAs(ctx context.Context, userID, channelID int64) error {
if r.deps.Channels == nil {
return sendAsPeerInvalidErr()
}
view, err := r.deps.Channels.GetChannel(ctx, userID, channelID)
if err != nil {
return sendAsPeerInvalidErr()
}
if canCurrentChannelSendAs(view) {
return nil
}
return sendAsPeerInvalidErr()
}
func canCurrentChannelSendAs(view domain.ChannelView) bool {
if view.Self.Role == domain.ChannelRoleCreator {
return true
}
if view.Channel.Broadcast && view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.PostMessages {
return true
}
if !view.Channel.Broadcast && view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.Anonymous {
return true
}
return false
}
func (r *Router) onMessagesSendMessage(ctx context.Context, req *tg.MessagesSendMessageRequest) (tg.UpdatesClass, error) {
start := r.clock.Now()
var duplicate bool
var sendErr error
defer func() {
r.metrics().MessageSend(r.clock.Now().Sub(start), duplicate, sendErr)
}()
if req.Message == "" {
sendErr = messageEmptyErr()
return nil, messageEmptyErr()
}
if utf8.RuneCountInString(req.Message) > maxSendMessageTextLength {
sendErr = messageTooLongErr()
return nil, sendErr
}
if len(req.Entities) > maxMessageEntityCount {
sendErr = limitInvalidErr()
return nil, sendErr
}
if req.RandomID == 0 {
sendErr = randomIDEmptyErr()
return nil, sendErr
}
if req.ScheduleDate != 0 || req.ScheduleRepeatPeriod != 0 {
sendErr = scheduleDateInvalidErr()
return nil, sendErr
}
if err := sendMessageUnsupportedOptionErr(req); err != nil {
sendErr = err
return nil, sendErr
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
sendErr = internalErr()
return nil, sendErr
}
if userID == 0 {
sendErr = peerIDInvalidErr()
return nil, sendErr
}
if r.deps.Limiter != nil {
allowed, retryAfter, err := r.deps.Limiter.Allow(ctx, "messages:send:"+strconv.FormatInt(userID, 10), sendMessageRateLimit, sendMessageRateWindow)
if err != nil {
sendErr = internalErr()
return nil, sendErr
}
if !allowed {
r.metrics().MessageRateLimited(retryAfter)
sendErr = floodWaitErr(retryAfter)
return nil, sendErr
}
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
sendErr = err
return nil, sendErr
}
updates, dup, err := r.sendOutgoing(ctx, userID, peer, outgoingSend{
randomID: req.RandomID,
message: req.Message,
entities: req.Entities,
silent: req.Silent,
noforwards: req.Noforwards,
replyToInput: req.ReplyTo,
sendAsInput: req.SendAs,
clearDraft: req.ClearDraft,
})
duplicate = dup
if err != nil {
sendErr = err
return nil, sendErr
}
return updates, nil
}
func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.MessagesForwardMessagesRequest) (tg.UpdatesClass, error) {
if len(req.ID) == 0 || len(req.ID) != len(req.RandomID) {
return nil, inputRequestInvalidErr()
}
if len(req.ID) > domain.MaxForwardMessageIDs {
return nil, limitInvalidErr()
}
if req.ScheduleDate != 0 || req.ScheduleRepeatPeriod != 0 {
return nil, scheduleDateInvalidErr()
}
if err := forwardMessagesUnsupportedOptionErr(req); err != nil {
return nil, err
}
topMsgID, topMsgIDSet := req.GetTopMsgID()
if !topMsgIDSet && req.TopMsgID != 0 {
topMsgID, topMsgIDSet = req.TopMsgID, true
}
if topMsgIDSet && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, replyMessageIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 {
return nil, peerIDInvalidErr()
}
fromPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.FromPeer)
if err != nil {
return nil, err
}
toPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.ToPeer)
if err != nil {
return nil, err
}
sendAs, err := r.resolveSendAsPeer(ctx, userID, toPeer, req.SendAs)
if err != nil {
return nil, err
}
replyTo, err := r.messageReplyFromInput(ctx, userID, toPeer, req.ReplyTo)
if err != nil {
return nil, err
}
replyTo, err = mergeForwardTopMsgID(toPeer, replyTo, topMsgID, topMsgIDSet)
if err != nil {
return nil, err
}
if r.deps.Users != nil {
for _, peer := range []domain.Peer{fromPeer, toPeer} {
if peer.Type != domain.PeerTypeUser || peer.ID == userID {
continue
}
if _, found, err := r.deps.Users.ByID(ctx, userID, peer.ID); err != nil {
return nil, internalErr()
} else if !found {
return nil, peerIDInvalidErr()
}
}
}
for i, id := range req.ID {
if id <= 0 || id > domain.MaxMessageBoxID || req.RandomID[i] == 0 {
return nil, messageIDInvalidErr()
}
}
if r.deps.Limiter != nil {
allowed, retryAfter, err := r.deps.Limiter.Allow(ctx, "messages:forward:"+strconv.FormatInt(userID, 10), sendMessageRateLimit, sendMessageRateWindow)
if err != nil {
return nil, internalErr()
}
if !allowed {
r.metrics().MessageRateLimited(retryAfter)
return nil, floodWaitErr(retryAfter)
}
}
if toPeer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
sources, err := r.forwardSources(ctx, userID, fromPeer, req.ID)
if err != nil {
return nil, messageForwardErr(err)
}
recipients := make([]int64, 0)
results := make([]domain.SendChannelMessageResult, 0, len(sources))
extraUserIDs := make([]int64, 0, len(sources))
for i, source := range sources {
forward := source.forward
if req.DropAuthor {
forward = nil
}
res, err := r.deps.Channels.SendMessage(ctx, userID, domain.SendChannelMessageRequest{
UserID: userID,
ChannelID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
SendAs: sendAs,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelInvalidErr(err)
}
results = append(results, res)
recipients = append(recipients, res.Recipients...)
if sourceUserID := source.userID(); sourceUserID != 0 {
extraUserIDs = append(extraUserIDs, sourceUserID)
}
}
updates := r.channelMessagesUpdates(ctx, userID, results, req.RandomID, true, extraUserIDs)
r.pushChannelUpdates(ctx, userID, toPeer.ID, recipients, func(viewerUserID int64) *tg.Updates {
return r.channelMessagesUpdates(ctx, viewerUserID, results, nil, false, extraUserIDs)
})
for _, res := range results {
r.pushChannelDiscussionUpdate(ctx, userID, res.Discussion)
}
return updates, nil
}
if fromPeer.Type == domain.PeerTypeChannel && toPeer.Type == domain.PeerTypeUser {
if r.deps.Channels == nil || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
sources, err := r.forwardSources(ctx, userID, fromPeer, req.ID)
if err != nil {
return nil, messageForwardErr(err)
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res := domain.ForwardPrivateMessagesResult{OwnerUserID: userID}
for i, source := range sources {
forward := source.forward
if req.DropAuthor {
forward = nil
}
sent, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
}
res.SenderMessages = append(res.SenderMessages, sent.SenderMessage)
res.RecipientMessages = append(res.RecipientMessages, sent.RecipientMessage)
res.SenderEvents = append(res.SenderEvents, sent.SenderEvent)
res.RecipientEvents = append(res.RecipientEvents, sent.RecipientEvent)
res.Duplicates = append(res.Duplicates, sent.Duplicate)
}
return tgForwardMessagesUpdates(res, req.RandomID, r.usersForMessageUpdates(ctx, userID, res.SenderMessages), r.chatsForMessageUpdates(ctx, userID, res.SenderMessages)), nil
}
if fromPeer.Type != domain.PeerTypeUser || toPeer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
res, err := r.deps.Messages.ForwardPrivateMessages(ctx, userID, domain.ForwardPrivateMessagesRequest{
OwnerUserID: userID,
FromPeer: fromPeer,
ToUserID: toPeer.ID,
MessageIDs: append([]int(nil), req.ID...),
RandomIDs: append([]int64(nil), req.RandomID...),
Silent: req.Silent,
NoForwards: req.Noforwards,
DropAuthor: req.DropAuthor,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
}
return tgForwardMessagesUpdates(res, req.RandomID, r.usersForMessageUpdates(ctx, userID, res.SenderMessages), r.chatsForMessageUpdates(ctx, userID, res.SenderMessages)), nil
}
func mergeForwardTopMsgID(toPeer domain.Peer, replyTo *domain.MessageReply, topMsgID int, topMsgIDSet bool) (*domain.MessageReply, error) {
if !topMsgIDSet || topMsgID == 0 {
return replyTo, nil
}
if topMsgID < 0 || topMsgID > domain.MaxMessageBoxID || toPeer.Type != domain.PeerTypeChannel {
return nil, replyMessageIDInvalidErr()
}
if replyTo == nil {
return &domain.MessageReply{
Peer: toPeer,
TopMessageID: topMsgID,
ForumTopic: true,
}, nil
}
if replyTo.Peer.ID != 0 && replyTo.Peer != toPeer {
return nil, replyMessageIDInvalidErr()
}
if replyTo.TopMessageID != 0 && replyTo.TopMessageID != topMsgID {
return nil, replyMessageIDInvalidErr()
}
merged := *replyTo
merged.Peer = toPeer
merged.TopMessageID = topMsgID
merged.QuoteEntities = append([]domain.MessageEntity(nil), replyTo.QuoteEntities...)
if merged.MessageID == 0 {
merged.ForumTopic = true
}
return &merged, nil
}
type forwardSource struct {
body string
entities []domain.MessageEntity
media *domain.MessageMedia
forward *domain.MessageForward
from domain.Peer
date int
noForward bool
}
func (s forwardSource) userID() int64 {
if s.from.Type == domain.PeerTypeUser {
return s.from.ID
}
if s.forward != nil && s.forward.From.Type == domain.PeerTypeUser {
return s.forward.From.ID
}
return 0
}
func (r *Router) forwardSources(ctx context.Context, userID int64, fromPeer domain.Peer, ids []int) ([]forwardSource, error) {
out := make([]forwardSource, 0, len(ids))
for _, id := range ids {
switch fromPeer.Type {
case domain.PeerTypeUser:
if r.deps.Messages == nil {
return nil, domain.ErrMessageIDInvalid
}
list, err := r.deps.Messages.GetMessages(ctx, userID, []int{id})
if err != nil || len(list.Messages) != 1 || list.Messages[0].ID != id {
return nil, domain.ErrMessageIDInvalid
}
msg := list.Messages[0]
if msg.Peer != fromPeer {
return nil, domain.ErrMessageIDInvalid
}
if msg.NoForwards {
return nil, domain.ErrChatForwardsRestricted
}
forward := cloneDomainMessageForward(msg.Forward)
if forward == nil {
forward = &domain.MessageForward{From: msg.From, Date: msg.Date}
}
out = append(out, forwardSource{
body: msg.Body,
entities: append([]domain.MessageEntity(nil),
msg.Entities...),
media: msg.Media,
forward: forward,
from: msg.From,
date: msg.Date,
})
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return nil, domain.ErrMessageIDInvalid
}
history, err := r.deps.Channels.GetMessages(ctx, userID, fromPeer.ID, []int{id})
if err != nil || len(history.Messages) != 1 || history.Messages[0].ID != id {
return nil, domain.ErrMessageIDInvalid
}
msg := history.Messages[0]
if msg.NoForwards || history.Channel.NoForwards {
return nil, domain.ErrChatForwardsRestricted
}
if msg.Action != nil || (msg.Body == "" && msg.Media.IsZero()) {
return nil, domain.ErrMessageIDInvalid
}
forward := cloneDomainMessageForward(msg.Forward)
from := msg.From
if from.ID == 0 && msg.SenderUserID != 0 {
from = domain.Peer{Type: domain.PeerTypeUser, ID: msg.SenderUserID}
}
if msg.Post {
from = domain.Peer{Type: domain.PeerTypeChannel, ID: msg.ChannelID}
}
if forward == nil {
forward = &domain.MessageForward{From: from, Date: msg.Date}
if from.Type == domain.PeerTypeChannel {
forward.ChannelPost = msg.ID
}
}
out = append(out, forwardSource{
body: msg.Body,
entities: append([]domain.MessageEntity(nil),
msg.Entities...),
media: msg.Media,
forward: forward,
from: from,
date: msg.Date,
})
default:
return nil, domain.ErrMessageIDInvalid
}
}
return out, nil
}
func cloneDomainMessageForward(in *domain.MessageForward) *domain.MessageForward {
if in == nil {
return nil
}
out := *in
return &out
}
func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEditMessageRequest) (tg.UpdatesClass, error) {
if _, ok := req.GetScheduleDate(); ok {
return nil, scheduleDateInvalidErr()
}
if _, ok := req.GetScheduleRepeatPeriod(); ok {
return nil, scheduleDateInvalidErr()
}
if _, ok := req.GetQuickReplyShortcutID(); ok {
return nil, messageIDInvalidErr()
}
if media, ok := req.GetMedia(); ok && !editMessageMediaCanDegradeToText(media) {
return nil, mediaInvalidErr()
}
if _, ok := req.GetReplyMarkup(); ok {
return nil, replyMarkupInvalidErr()
}
message, ok := req.GetMessage()
if !ok {
return nil, messageEmptyErr()
}
if message == "" {
return nil, messageEmptyErr()
}
if utf8.RuneCountInString(message) > maxSendMessageTextLength {
return nil, messageTooLongErr()
}
entities, _ := req.GetEntities()
if len(entities) > maxMessageEntityCount {
return nil, limitInvalidErr()
}
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 {
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
res, err := r.deps.Channels.EditMessage(ctx, userID, domain.EditChannelMessageRequest{
UserID: userID,
ChannelID: peer.ID,
ID: req.ID,
Message: message,
Entities: domainMessageEntities(entities),
EditDate: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelEditErr(err)
}
updates := r.channelEditMessageUpdates(ctx, userID, res)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelEditMessageUpdates(ctx, viewerUserID, res)
})
return updates, nil
}
if peer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageEditForbiddenErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.EditMessage(ctx, userID, domain.EditMessageRequest{
OwnerUserID: userID,
Peer: peer,
ID: req.ID,
Message: message,
Entities: domainMessageEntities(entities),
EditDate: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
})
if err != nil {
return nil, messageEditErr(err)
}
self := res.Self()
if self.Event.Pts == 0 || self.Message.ID == 0 {
return nil, messageIDInvalidErr()
}
users := r.usersForMessageUpdate(ctx, userID, self.Message)
chats := r.chatsForMessageUpdate(ctx, userID, self.Message)
return tgEditMessageUpdates(self.Event, self.Message, users, chats), nil
}
func editMessageMediaCanDegradeToText(media tg.InputMediaClass) bool {
switch media.(type) {
case *tg.InputMediaEmpty, *tg.InputMediaWebPage:
return true
default:
return false
}
}
func (r *Router) onMessagesGetMessageEditData(ctx context.Context, req *tg.MessagesGetMessageEditDataRequest) (*tg.MessagesMessageEditData, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
switch peer.Type {
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return nil, peerIDInvalidErr()
}
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
ChannelID: peer.ID,
Limit: 1,
MaxID: req.ID,
MinID: req.ID - 1,
})
if err != nil {
return nil, channelInvalidErr(err)
}
if len(history.Messages) != 1 || history.Messages[0].ID != req.ID {
return nil, messageIDInvalidErr()
}
msg := history.Messages[0]
if msg.Deleted || msg.Action != nil {
return nil, messageIDInvalidErr()
}
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
if err != nil {
return nil, channelInvalidErr(err)
}
if msg.SenderUserID != userID && !canEditChannelMessageForRPC(view.Self) {
return nil, messageAuthorRequiredErr()
}
case domain.PeerTypeUser:
if r.deps.Messages == nil {
return nil, messageIDInvalidErr()
}
msg, ok, err := r.lookupOwnerMessage(ctx, userID, req.ID)
if err != nil {
return nil, internalErr()
}
if !ok || msg.Peer != peer {
return nil, messageIDInvalidErr()
}
if !msg.Out || msg.From.ID != userID {
return nil, messageAuthorRequiredErr()
}
default:
return nil, peerIDInvalidErr()
}
return &tg.MessagesMessageEditData{}, nil
}
func canEditChannelMessageForRPC(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator ||
(member.Role == domain.ChannelRoleAdmin && member.AdminRights.EditMessages)
}
func (r *Router) onMessagesGetOutboxReadDate(ctx context.Context, req *tg.MessagesGetOutboxReadDateRequest) (*tg.OutboxReadDate, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 || r.deps.Messages == nil {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type != domain.PeerTypeUser || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
date, err := r.deps.Messages.GetOutboxReadDate(ctx, userID, domain.OutboxReadDateRequest{
OwnerUserID: userID,
Peer: peer,
ID: req.MsgID,
})
if err != nil {
return nil, messageReadDateErr(err)
}
return &tg.OutboxReadDate{Date: date}, nil
}
func (r *Router) onMessagesGetMessageReadParticipants(ctx context.Context, req *tg.MessagesGetMessageReadParticipantsRequest) ([]tg.ReadParticipantDate, 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 || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
if r.deps.Channels == nil {
return []tg.ReadParticipantDate{}, nil
}
res, err := r.deps.Channels.GetMessageReadParticipants(ctx, userID, domain.ChannelReadParticipantsRequest{
UserID: userID,
ChannelID: peer.ID,
MessageID: req.MsgID,
Limit: domain.MaxChannelReadParticipants,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
return nil, messageIDInvalidErr()
}
return nil, channelInvalidErr(err)
}
out := make([]tg.ReadParticipantDate, 0, len(res.Participants))
for _, p := range res.Participants {
if p.UserID == 0 {
continue
}
out = append(out, tg.ReadParticipantDate{UserID: p.UserID, Date: p.Date})
}
return out, nil
}
func (r *Router) onMessagesDeleteMessages(ctx context.Context, req *tg.MessagesDeleteMessagesRequest) (*tg.MessagesAffectedMessages, error) {
authKeyID, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if len(req.ID) == 0 || r.deps.Messages == nil {
return r.affectedMessages(ctx, authKeyID, userID)
}
if len(req.ID) > domain.MaxDeleteMessageIDs {
return nil, limitInvalidErr()
}
if req.GetRevoke() {
list, err := r.deps.Messages.GetMessages(ctx, userID, req.ID)
if err != nil {
return nil, internalErr()
}
blocked, err := r.messagesTouchBlockedPeer(ctx, userID, list.Messages)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteMessages(ctx, userID, domain.DeleteMessagesRequest{
OwnerUserID: userID,
IDs: req.ID,
Revoke: req.GetRevoke(),
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
})
if err != nil {
return nil, internalErr()
}
self := res.Self()
if len(self.MessageIDs) == 0 || self.Event.Pts == 0 {
return r.affectedMessages(ctx, authKeyID, userID)
}
return &tg.MessagesAffectedMessages{Pts: self.Event.Pts, PtsCount: self.Event.PtsCount}, nil
}
func (r *Router) onMessagesDeleteHistory(ctx context.Context, req *tg.MessagesDeleteHistoryRequest) (*tg.MessagesAffectedHistory, error) {
authKeyID, _ := AuthKeyIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, ok := req.GetMinDate(); ok {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
if _, ok := req.GetMaxDate(); ok {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type == domain.PeerTypeChannel {
if r.deps.Channels == nil {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
if req.MaxID < 0 || req.MaxID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
res, err := r.deps.Channels.DeleteHistory(ctx, userID, domain.DeleteChannelHistoryRequest{
UserID: userID,
ChannelID: peer.ID,
MaxID: req.MaxID,
ForEveryone: req.GetRevoke(),
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelDeleteErr(err)
}
if res.Event.Pts != 0 {
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelDeleteMessagesUpdates(viewerUserID, res.Channel, res.Event)
})
return &tg.MessagesAffectedHistory{Pts: res.Event.Pts, PtsCount: res.Event.PtsCount, Offset: res.Offset}, nil
}
if res.AvailableMinID > 0 {
event := r.recordChannelAvailableMessages(ctx, userID, res.Channel.ID, res.AvailableMinID)
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, event.MaxID)
r.pushUserUpdates(ctx, userID, updates)
if event.Pts != 0 {
return &tg.MessagesAffectedHistory{Pts: event.Pts, PtsCount: event.PtsCount, Offset: res.Offset}, nil
}
}
return &tg.MessagesAffectedHistory{Pts: res.Channel.Pts, PtsCount: 0, Offset: res.Offset}, nil
}
if peer.Type != domain.PeerTypeUser {
return nil, peerIDInvalidErr()
}
if r.deps.Messages == nil {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
if req.GetRevoke() {
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteHistory(ctx, userID, domain.DeleteHistoryRequest{
OwnerUserID: userID,
Peer: peer,
MaxID: req.MaxID,
JustClear: req.GetJustClear(),
Revoke: req.GetRevoke(),
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
})
if err != nil {
return nil, internalErr()
}
self := res.Self()
if len(self.MessageIDs) == 0 || self.Event.Pts == 0 {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
return &tg.MessagesAffectedHistory{
Pts: self.Event.Pts,
PtsCount: self.Event.PtsCount,
Offset: res.Offset,
}, nil
}
func messageEditErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageAuthorRequired):
return messageAuthorRequiredErr()
case errors.Is(err, domain.ErrMessageNotModified):
return messageNotModifiedErr()
default:
return internalErr()
}
}
func channelEditErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageAuthorRequired):
return messageAuthorRequiredErr()
case errors.Is(err, domain.ErrMessageNotModified):
return messageNotModifiedErr()
default:
return channelInvalidErr(err)
}
}
func messageReadDateErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageNotReadYet):
return messageNotReadYetErr()
default:
return internalErr()
}
}
func messageReactionErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
default:
return internalErr()
}
}
func messageSendErr(err error) error {
switch {
case errors.Is(err, domain.ErrReplyMessageIDInvalid):
return replyMessageIDInvalidErr()
default:
return internalErr()
}
}
func (r *Router) peerBlocksUser(ctx context.Context, userID, peerUserID int64) (bool, error) {
if userID == 0 || peerUserID == 0 || userID == peerUserID || r.deps.Contacts == nil {
return false, nil
}
blocked, err := r.deps.Contacts.IsBlocked(ctx, peerUserID, userID)
if err != nil {
return false, internalErr()
}
return blocked, nil
}
func (r *Router) messagesTouchBlockedPeer(ctx context.Context, userID int64, messages []domain.Message) (bool, error) {
seen := make(map[int64]struct{}, len(messages))
for _, msg := range messages {
if msg.Peer.Type != domain.PeerTypeUser || msg.Peer.ID == 0 {
continue
}
if _, ok := seen[msg.Peer.ID]; ok {
continue
}
seen[msg.Peer.ID] = struct{}{}
blocked, err := r.peerBlocksUser(ctx, userID, msg.Peer.ID)
if err != nil {
return false, err
}
if blocked {
return true, nil
}
}
return false, nil
}
func messageForwardErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrChatForwardsRestricted):
return chatForwardsRestrictedErr()
case errors.Is(err, domain.ErrReplyMessageIDInvalid):
return replyMessageIDInvalidErr()
default:
return internalErr()
}
}
func (r *Router) messageReplyFromInput(ctx context.Context, userID int64, peer domain.Peer, input tg.InputReplyToClass) (*domain.MessageReply, error) {
if input == nil {
return nil, nil
}
reply, ok := input.(*tg.InputReplyToMessage)
if !ok {
switch input.(type) {
case *tg.InputReplyToStory:
return nil, storyIDInvalidErr()
case *tg.InputReplyToMonoForum:
return nil, replyToMonoforumPeerInvalidErr()
default:
return nil, inputConstructorInvalidErr()
}
}
if _, ok := reply.GetMonoforumPeerID(); ok {
return nil, replyToMonoforumPeerInvalidErr()
}
if _, ok := reply.GetTodoItemID(); ok {
return nil, replyMessageIDInvalidErr()
}
if _, ok := reply.GetPollOption(); ok {
return nil, pollOptionInvalidErr()
}
replyPeer := peer
if inputPeer, ok := reply.GetReplyToPeerID(); ok {
parsed, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer)
if err != nil || parsed != peer {
return nil, replyMessageIDInvalidErr()
}
replyPeer = parsed
}
topMsgID, ok := reply.GetTopMsgID()
if ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, replyMessageIDInvalidErr()
}
if reply.ReplyToMsgID < 0 || reply.ReplyToMsgID > domain.MaxMessageBoxID {
return nil, replyMessageIDInvalidErr()
}
if reply.ReplyToMsgID == 0 && topMsgID == 0 {
return nil, replyMessageIDInvalidErr()
}
quoteText, _ := reply.GetQuoteText()
if utf8.RuneCountInString(quoteText) > maxReplyQuoteLength {
return nil, limitInvalidErr()
}
quoteEntities, _ := reply.GetQuoteEntities()
if len(quoteEntities) > maxMessageEntityCount {
return nil, limitInvalidErr()
}
quoteOffset, ok := reply.GetQuoteOffset()
if ok && (quoteOffset < 0 || quoteOffset > domain.MaxMessageReplyQuoteOffset) {
return nil, replyMessageIDInvalidErr()
}
return &domain.MessageReply{
MessageID: reply.ReplyToMsgID,
Peer: replyPeer,
TopMessageID: topMsgID,
QuoteText: quoteText,
QuoteEntities: domainMessageEntities(quoteEntities),
QuoteOffset: quoteOffset,
}, nil
}
func sendMessageUnsupportedOptionErr(req *tg.MessagesSendMessageRequest) error {
switch {
case req.ReplyMarkup != nil:
return replyMarkupInvalidErr()
case req.QuickReplyShortcut != nil:
return shortcutInvalidErr()
case req.Effect != 0:
return effectIDInvalidErr()
case req.AllowPaidStars < 0:
return starsAmountInvalidErr()
case req.AllowPaidStars > 0 || req.AllowPaidFloodskip:
return paymentUnsupportedErr()
case !req.SuggestedPost.Zero():
return suggestedPostPeerInvalidErr()
default:
return nil
}
}
func forwardMessagesUnsupportedOptionErr(req *tg.MessagesForwardMessagesRequest) error {
switch {
case req.QuickReplyShortcut != nil:
return shortcutInvalidErr()
case req.Effect != 0:
return effectIDInvalidErr()
case req.VideoTimestamp != 0:
return mediaInvalidErr()
case req.AllowPaidStars < 0:
return starsAmountInvalidErr()
case req.AllowPaidStars > 0 || req.AllowPaidFloodskip:
return paymentUnsupportedErr()
case !req.SuggestedPost.Zero():
return suggestedPostPeerInvalidErr()
default:
return nil
}
}
func (r *Router) onMessagesGetPeerSettings(ctx context.Context, input tg.InputPeerClass) (*tg.MessagesPeerSettings, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
if err != nil {
return nil, err
}
settings := domain.PeerSettings{}
if r.deps.Contacts != nil {
settings, err = r.deps.Contacts.GetPeerSettings(ctx, userID, peer)
if err != nil {
return nil, internalErr()
}
}
if r.deps.Dialogs != nil {
hidden, err := r.deps.Dialogs.PeerSettingsBarHidden(ctx, userID, peer)
if err != nil {
return nil, internalErr()
}
settings.HiddenPeerSettingsBar = hidden
}
return &tg.MessagesPeerSettings{
Settings: tgPeerSettings(settings),
Users: r.peerSettingsUsers(ctx, userID, input),
}, nil
}
func (r *Router) onMessagesToggleDialogPin(ctx context.Context, req *tg.MessagesToggleDialogPinRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peers, err := r.dialogPeersFromInput(ctx, userID, []tg.InputDialogPeerClass{req.Peer})
if err != nil {
return false, err
}
if len(peers) != 1 {
return false, peerIDInvalidErr()
}
pinned := req.GetPinned()
if r.deps.Dialogs == nil {
return true, nil
}
changed, err := r.deps.Dialogs.TogglePinned(ctx, userID, peers[0], pinned)
if err != nil {
return false, internalErr()
}
if changed {
date := int(r.clock.Now().Unix())
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
_, state, err := r.deps.Updates.RecordDialogPinned(ctx, authKeyID, userID, peers[0], pinned, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateDialogPinned{
Pinned: pinned,
Peer: tgDialogPeer(peers[0]),
}},
Date: date,
Seq: 0,
})
}
return true, nil
}
func (r *Router) onMessagesReorderPinnedDialogs(ctx context.Context, req *tg.MessagesReorderPinnedDialogsRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req.FolderID != 0 {
return false, folderIDInvalidErr()
}
if len(req.Order) > maxDialogInputPeers {
return false, limitInvalidErr()
}
peers, err := r.dialogPeersFromInput(ctx, userID, req.Order)
if err != nil {
return false, err
}
if r.deps.Dialogs == nil {
return true, nil
}
if err := r.deps.Dialogs.ReorderPinned(ctx, userID, peers, req.GetForce()); err != nil {
return false, internalErr()
}
date := int(r.clock.Now().Unix())
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
_, state, err := r.deps.Updates.RecordPinnedDialogs(ctx, authKeyID, userID, peers, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdatePinnedDialogs{Order: tgDialogPeers(peers)}},
Date: date,
Seq: 0,
})
return true, nil
}
func (r *Router) onMessagesMarkDialogUnread(ctx context.Context, req *tg.MessagesMarkDialogUnreadRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if parentPeer, ok := req.GetParentPeer(); ok {
if err := r.validateDialogUnreadParentPeer(ctx, userID, parentPeer); err != nil {
return false, err
}
peers, err := r.dialogPeersFromInput(ctx, userID, []tg.InputDialogPeerClass{req.Peer})
if err != nil {
return false, err
}
if len(peers) != 1 {
return false, peerIDInvalidErr()
}
return true, nil
}
peers, err := r.dialogPeersFromInput(ctx, userID, []tg.InputDialogPeerClass{req.Peer})
if err != nil {
return false, err
}
if len(peers) != 1 {
return false, peerIDInvalidErr()
}
unread := req.GetUnread()
if r.deps.Dialogs == nil {
return true, nil
}
changed, err := r.deps.Dialogs.MarkUnread(ctx, userID, peers[0], unread)
if err != nil {
return false, internalErr()
}
if changed {
date := int(r.clock.Now().Unix())
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
_, state, err := r.deps.Updates.RecordDialogUnreadMark(ctx, authKeyID, userID, peers[0], unread, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateDialogUnreadMark{
Unread: unread,
Peer: tgDialogPeer(peers[0]),
}},
Date: date,
Seq: 0,
})
}
return true, nil
}
func (r *Router) onMessagesGetDialogUnreadMarks(ctx context.Context, req *tg.MessagesGetDialogUnreadMarksRequest) ([]tg.DialogPeerClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if parentPeer, ok := req.GetParentPeer(); ok {
if err := r.validateDialogUnreadParentPeer(ctx, userID, parentPeer); err != nil {
return nil, err
}
return []tg.DialogPeerClass{}, nil
}
if r.deps.Dialogs == nil {
return nil, nil
}
peers, err := r.deps.Dialogs.UnreadMarks(ctx, userID)
if err != nil {
return nil, internalErr()
}
return tgDialogPeers(peers), nil
}
func (r *Router) validateDialogUnreadParentPeer(ctx context.Context, userID int64, parentPeer tg.InputPeerClass) error {
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, parentPeer)
if err != nil {
return parentPeerInvalidErr()
}
if peer.Type != domain.PeerTypeChannel {
return parentPeerInvalidErr()
}
return nil
}
func (r *Router) onMessagesHidePeerSettingsBar(ctx context.Context, input tg.InputPeerClass) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
if err != nil {
return false, err
}
changed := true
if r.deps.Dialogs != nil {
var err error
changed, err = r.deps.Dialogs.HidePeerSettingsBar(ctx, userID, peer)
if err != nil {
return false, internalErr()
}
}
if !changed {
return true, nil
}
date := int(r.clock.Now().Unix())
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
_, state, err := r.deps.Updates.RecordPeerSettings(ctx, authKeyID, userID, peer, domain.PeerSettings{HiddenPeerSettingsBar: true}, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdatePeerSettings{
Peer: tgPeer(peer),
Settings: tg.PeerSettings{},
}},
Date: date,
Seq: 0,
})
return true, nil
}
func (r *Router) metrics() Metrics {
if r.deps.Metrics == nil {
return NopMetrics{}
}
return r.deps.Metrics
}
func (r *Router) dialogFilterFromRequest(ctx context.Context, userID int64, req *tg.MessagesGetDialogsRequest) (domain.DialogFilter, error) {
limit := req.Limit
if limit > 500 {
limit = 500
}
filter := domain.DialogFilter{
ExcludePinned: req.ExcludePinned,
OffsetDate: req.OffsetDate,
OffsetID: req.OffsetID,
Limit: limit,
Hash: req.Hash,
}
if folderID, ok := req.GetFolderID(); ok {
filter.HasFolderID = true
filter.FolderID = folderID
}
if peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.OffsetPeer); err == nil {
filter.HasOffsetPeer = true
filter.OffsetPeer = peer
} else if _, ok := req.OffsetPeer.(*tg.InputPeerEmpty); !ok && req.OffsetPeer != nil {
return domain.DialogFilter{}, err
}
return filter, nil
}
func (r *Router) dialogPeersFromInput(ctx context.Context, userID int64, items []tg.InputDialogPeerClass) ([]domain.Peer, error) {
if len(items) > maxDialogInputPeers {
return nil, limitInvalidErr()
}
peers := make([]domain.Peer, 0, len(items))
hasFolder := false
for _, item := range items {
switch p := item.(type) {
case *tg.InputDialogPeer:
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, p.Peer)
if err != nil {
return nil, err
}
peers = append(peers, peer)
case *tg.InputDialogPeerFolder:
if hasFolder {
return nil, folderIDInvalidErr()
}
hasFolder = true
// 第一阶段不维护 archived/folder 会话。若请求同时包含普通 peer
// 按 Telegram 企业版路径优先返回普通 peer纯 folder 请求返回空摘要。
default:
return nil, inputConstructorInvalidErr()
}
}
return peers, nil
}
func (r *Router) dialogFolderFromTG(ctx context.Context, userID int64, id int, filter tg.DialogFilterClass) (domain.DialogFolder, error) {
if id < domain.DialogCustomFolderMinID {
return domain.DialogFolder{}, filterIDInvalidErr()
}
switch f := filter.(type) {
case *tg.DialogFilter:
title := f.Title.Text
if title == "" {
return domain.DialogFolder{}, filterTitleEmptyErr()
}
if utf8.RuneCountInString(title) > domain.MaxDialogFolderTitleRunes {
return domain.DialogFolder{}, limitInvalidErr()
}
pinned, err := r.dialogFolderPeersFromInput(ctx, userID, f.PinnedPeers)
if err != nil {
return domain.DialogFolder{}, err
}
include, err := r.dialogFolderPeersFromInput(ctx, userID, f.IncludePeers)
if err != nil {
return domain.DialogFolder{}, err
}
exclude, err := r.dialogFolderPeersFromInput(ctx, userID, f.ExcludePeers)
if err != nil {
return domain.DialogFolder{}, err
}
emoticon, hasEmoticon := f.GetEmoticon()
color, hasColor := f.GetColor()
return domain.DialogFolder{
ID: id,
Contacts: f.Contacts,
NonContacts: f.NonContacts,
Groups: f.Groups,
Broadcasts: f.Broadcasts,
Bots: f.Bots,
ExcludeMuted: f.ExcludeMuted,
ExcludeRead: f.ExcludeRead,
ExcludeArchived: f.ExcludeArchived,
TitleNoanimate: f.TitleNoanimate,
Title: title,
TitleEntities: domainMessageEntities(f.Title.Entities),
Emoticon: emoticon,
HasEmoticon: hasEmoticon,
Color: color,
HasColor: hasColor,
PinnedPeers: pinned,
IncludePeers: include,
ExcludePeers: exclude,
}, nil
case *tg.DialogFilterChatlist:
title := f.Title.Text
if title == "" {
return domain.DialogFolder{}, filterTitleEmptyErr()
}
if utf8.RuneCountInString(title) > domain.MaxDialogFolderTitleRunes {
return domain.DialogFolder{}, limitInvalidErr()
}
pinned, err := r.dialogFolderPeersFromInput(ctx, userID, f.PinnedPeers)
if err != nil {
return domain.DialogFolder{}, err
}
include, err := r.dialogFolderPeersFromInput(ctx, userID, f.IncludePeers)
if err != nil {
return domain.DialogFolder{}, err
}
emoticon, hasEmoticon := f.GetEmoticon()
color, hasColor := f.GetColor()
return domain.DialogFolder{
ID: id,
TitleNoanimate: f.TitleNoanimate,
Title: title,
TitleEntities: domainMessageEntities(f.Title.Entities),
Emoticon: emoticon,
HasEmoticon: hasEmoticon,
Color: color,
HasColor: hasColor,
PinnedPeers: pinned,
IncludePeers: include,
IsChatlist: true,
}, nil
default:
return domain.DialogFolder{}, inputConstructorInvalidErr()
}
}
func (r *Router) dialogFolderPeersFromInput(ctx context.Context, userID int64, peers []tg.InputPeerClass) ([]domain.DialogFolderPeer, error) {
if len(peers) > domain.MaxDialogFolderPeers {
return nil, limitInvalidErr()
}
out := make([]domain.DialogFolderPeer, 0, len(peers))
seen := make(map[domain.Peer]struct{}, len(peers))
for _, input := range peers {
peer, accessHash, err := r.domainFolderPeerFromInputPeer(ctx, userID, input)
if err != nil {
return nil, err
}
if _, ok := seen[peer]; ok {
continue
}
seen[peer] = struct{}{}
out = append(out, domain.DialogFolderPeer{Peer: peer, AccessHash: accessHash})
}
return out, nil
}
func (r *Router) domainFolderPeerFromInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.Peer, int64, error) {
switch p := peer.(type) {
case *tg.InputPeerUser:
return domain.Peer{Type: domain.PeerTypeUser, ID: p.UserID}, p.AccessHash, nil
case *tg.InputPeerChannel:
out := domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}
if err := r.validateInputPeerChannelAccess(ctx, userID, peer, p.ChannelID); err != nil {
return domain.Peer{}, 0, err
}
return out, p.AccessHash, nil
case *tg.InputPeerChannelFromMessage:
out := domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}
if p.ChannelID <= 0 {
return domain.Peer{}, 0, peerIDInvalidErr()
}
return out, 0, nil
case *tg.InputPeerSelf:
if userID == 0 {
return domain.Peer{}, 0, peerIDInvalidErr()
}
var accessHash int64
if r.deps.Users != nil {
if self, err := r.deps.Users.Self(ctx, userID); err == nil {
accessHash = self.AccessHash
}
}
return domain.Peer{Type: domain.PeerTypeUser, ID: userID}, accessHash, nil
default:
return domain.Peer{}, 0, peerIDInvalidErr()
}
}
func (r *Router) domainPeerFromInputPeer(userID int64, peer tg.InputPeerClass) (domain.Peer, bool) {
switch p := peer.(type) {
case *tg.InputPeerEmpty, nil:
return domain.Peer{}, false
case *tg.InputPeerUser:
return domain.Peer{Type: domain.PeerTypeUser, ID: p.UserID}, true
case *tg.InputPeerChannel:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}, true
case *tg.InputPeerChannelFromMessage:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}, p.ChannelID > 0
case *tg.InputPeerChat:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChatID}, p.ChatID > 0
case *tg.InputPeerSelf:
if userID == 0 {
return domain.Peer{}, false
}
return domain.Peer{Type: domain.PeerTypeUser, ID: userID}, true
default:
return domain.Peer{}, false
}
}
func isLegacyInputPeerChat(peer tg.InputPeerClass) bool {
_, ok := peer.(*tg.InputPeerChat)
return ok
}
func inputPeerChannelRef(peer tg.InputPeerClass) (channelInputRef, bool) {
switch p := peer.(type) {
case *tg.InputPeerChannel:
return channelInputRef{
ID: p.ChannelID,
AccessHash: p.AccessHash,
CheckAccessHash: p.AccessHash != 0,
}, p.ChannelID > 0
case *tg.InputPeerChannelFromMessage:
return channelInputRef{ID: p.ChannelID}, p.ChannelID > 0
default:
return channelInputRef{}, false
}
}
func (r *Router) validateInputPeerChannelAccess(ctx context.Context, userID int64, peer tg.InputPeerClass, channelID int64) error {
ref, ok := inputPeerChannelRef(peer)
if !ok || ref.ID != channelID || channelID <= 0 {
return nil
}
if !ref.CheckAccessHash || r.deps.Channels == nil {
return nil
}
view, err := r.deps.Channels.GetChannel(ctx, userID, channelID)
if err != nil {
return channelInvalidErr(err)
}
if !inputChannelAccessHashMatches(ref, view.Channel) {
return channelInvalidErr(domain.ErrChannelPrivate)
}
return nil
}
func (r *Router) checkedDomainPeerFromInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.Peer, error) {
out, ok := r.domainPeerFromInputPeer(userID, peer)
if !ok || out.ID == 0 {
return domain.Peer{}, peerIDInvalidErr()
}
if out.Type == domain.PeerTypeChannel {
if err := r.validateInputPeerChannelAccess(ctx, userID, peer, out.ID); err != nil {
return domain.Peer{}, err
}
}
return out, nil
}
func cleanDialogFilterOrder(order []int) []int {
out := make([]int, 0, len(order))
seen := make(map[int]struct{}, len(order))
for _, id := range order {
if id < domain.DialogCustomFolderMinID {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
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(userID int64, req *tg.MessagesSearchRequest) domain.MessageFilter {
limit := req.Limit
if limit > 500 {
limit = 500
}
filter := domain.MessageFilter{
Query: req.Q,
OffsetID: req.OffsetID,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
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
}
return filter
}
func (r *Router) channelHistoryFilterFromSearchRequest(userID int64, req *tg.MessagesSearchRequest, channelID int64) (domain.ChannelHistoryFilter, bool) {
limit := req.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
filter := domain.ChannelHistoryFilter{
ChannelID: channelID,
Query: req.Q,
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 searchFilterNeedsMediaStore(filter tg.MessagesFilterClass) bool {
switch filter.(type) {
case nil, *tg.InputMessagesFilterEmpty:
return false
case *tg.InputMessagesFilterPhotos,
*tg.InputMessagesFilterVideo,
*tg.InputMessagesFilterPhotoVideo,
*tg.InputMessagesFilterDocument,
*tg.InputMessagesFilterURL,
*tg.InputMessagesFilterGif,
*tg.InputMessagesFilterVoice,
*tg.InputMessagesFilterRoundVoice,
*tg.InputMessagesFilterRoundVideo,
*tg.InputMessagesFilterMusic,
*tg.InputMessagesFilterPoll:
return true
default:
return false
}
}
func (r *Router) peerSettingsUsers(ctx context.Context, userID int64, peer tg.InputPeerClass) []tg.UserClass {
if r.deps.Users == nil {
return nil
}
switch p := peer.(type) {
case *tg.InputPeerSelf:
u, err := r.deps.Users.Self(ctx, userID)
if err == nil && u.ID != 0 {
return []tg.UserClass{r.tgSelfUser(u)}
}
case *tg.InputPeerUser:
u, found, err := r.deps.Users.ByID(ctx, userID, p.UserID)
if err == nil && found {
return []tg.UserClass{r.tgUser(u)}
}
}
return nil
}
func (r *Router) affectedMessages(ctx context.Context, authKeyID [8]byte, userID int64) (*tg.MessagesAffectedMessages, error) {
st := domain.UpdateState{Date: int(r.clock.Now().Unix())}
if r.deps.Updates != nil {
var err error
st, err = r.deps.Updates.GetState(ctx, authKeyID, userID)
if err != nil {
return nil, internalErr()
}
}
return &tg.MessagesAffectedMessages{Pts: st.Pts, PtsCount: 0}, nil
}
func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID int64, message string, entities []tg.MessageEntityClass) ([]int64, error) {
if r.deps.Users == nil {
return nil, nil
}
identity, _ := r.deps.Users.(UserIdentityService)
seen := make(map[int64]struct{})
out := make([]int64, 0)
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
out = append(out, id)
}
for _, entity := range entities {
input, ok := entity.(*tg.InputMessageEntityMentionName)
if !ok || input.UserID == nil {
continue
}
user, found, err := r.userFromInput(ctx, currentUserID, input.UserID)
if err != nil {
return nil, internalErr()
}
if found {
add(user.ID)
}
if len(out) >= domain.MaxChannelMentionRecipients {
return out, nil
}
}
if identity != nil {
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) {
user, found, err := identity.ResolveUsername(ctx, currentUserID, username)
if err != nil {
return nil, internalErr()
}
if found {
add(user.ID)
}
if len(out) >= domain.MaxChannelMentionRecipients {
return out, nil
}
}
}
return out, nil
}
func extractMentionUsernames(message string, limit int) []string {
if limit <= 0 || message == "" {
return nil
}
seen := make(map[string]struct{})
out := make([]string, 0)
for i := 0; i < len(message); i++ {
if message[i] != '@' {
continue
}
if i > 0 && isUsernameByte(message[i-1]) {
continue
}
j := i + 1
for j < len(message) && isUsernameByte(message[j]) {
j++
}
if j == i+1 {
continue
}
username := strings.ToLower(message[i+1 : j])
if _, ok := seen[username]; ok {
continue
}
seen[username] = struct{}{}
out = append(out, username)
if len(out) == limit {
return out
}
i = j - 1
}
return out
}
func isUsernameByte(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
}
func (r *Router) affectedHistory(ctx context.Context, authKeyID [8]byte, userID int64, offset int) (*tg.MessagesAffectedHistory, error) {
st := domain.UpdateState{Date: int(r.clock.Now().Unix())}
if r.deps.Updates != nil {
var err error
st, err = r.deps.Updates.GetState(ctx, authKeyID, userID)
if err != nil {
return nil, internalErr()
}
}
return &tg.MessagesAffectedHistory{Pts: st.Pts, PtsCount: 0, Offset: offset}, nil
}
func (r *Router) chatsForInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) []tg.ChatClass {
p, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil || p.Type != domain.PeerTypeChannel || p.ID == 0 || r.deps.Channels == nil {
return []tg.ChatClass{}
}
view, err := r.deps.Channels.GetChannel(ctx, userID, p.ID)
if err != nil || view.Channel.ID == 0 {
return []tg.ChatClass{}
}
return []tg.ChatClass{tgChannelChat(userID, view.Channel, &view.Self)}
}
func (r *Router) forumTopicPeerView(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.ChannelView, error) {
p, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return domain.ChannelView{}, err
}
if p.Type != domain.PeerTypeChannel || p.ID == 0 {
return domain.ChannelView{}, peerIDInvalidErr()
}
if r.deps.Channels == nil {
return domain.ChannelView{}, nil
}
view, err := r.deps.Channels.GetChannel(ctx, userID, p.ID)
if err != nil {
return domain.ChannelView{}, channelInvalidErr(err)
}
return view, nil
}
func (r *Router) forumTopicsResponse(ctx context.Context, userID int64, view domain.ChannelView, list domain.ChannelForumTopicList, includeGeneral bool) *tg.MessagesForumTopics {
if list.Channel.ID != 0 {
view.Channel = list.Channel
}
if list.Dialog.ChannelID != 0 {
view.Dialog = list.Dialog
}
channels := []domain.Channel{view.Channel}
messages := []tg.MessageClass{}
messageIDs := map[int]struct{}{}
topics := []tg.ForumTopicClass{}
userIDs := make([]int64, 0, len(list.Topics)+len(list.Messages)+1)
count := list.Count
if includeGeneral {
count++
topics = append(topics, tgForumGeneralTopic(userID, view))
userIDs = append(userIDs, view.Channel.CreatorUserID)
if view.Channel.TopMessageID > 0 && r.deps.Channels != nil {
if history, err := r.deps.Channels.GetMessages(ctx, userID, view.Channel.ID, []int{view.Channel.TopMessageID}); err == nil {
for _, msg := range history.Messages {
if _, ok := messageIDs[msg.ID]; ok {
continue
}
messageIDs[msg.ID] = struct{}{}
if item := tgChannelMessage(userID, msg); item != nil {
messages = append(messages, item)
}
userIDs = append(userIDs, msg.SenderUserID)
}
channels = append(channels, history.Channels...)
for _, u := range history.Users {
userIDs = append(userIDs, u.ID)
}
}
}
}
for _, topic := range list.Topics {
topics = append(topics, tgForumTopicFromDomain(userID, topic))
userIDs = append(userIDs, topic.CreatorUserID)
}
for _, msg := range list.Messages {
if _, ok := messageIDs[msg.ID]; ok {
continue
}
messageIDs[msg.ID] = struct{}{}
if item := tgChannelMessage(userID, msg); item != nil {
messages = append(messages, item)
}
userIDs = append(userIDs, msg.SenderUserID)
if msg.SendAs != nil && msg.SendAs.Type == domain.PeerTypeUser {
userIDs = append(userIDs, msg.SendAs.ID)
}
}
for _, u := range list.Users {
userIDs = append(userIDs, u.ID)
}
return &tg.MessagesForumTopics{
Count: count,
Topics: topics,
Messages: messages,
Chats: tgChannels(userID, channels),
Users: r.tgUsersForIDs(ctx, userID, userIDs),
Pts: view.Channel.Pts,
}
}
func tgForumGeneralTopic(viewerUserID int64, view domain.ChannelView) *tg.ForumTopic {
return &tg.ForumTopic{
My: view.Channel.CreatorUserID == viewerUserID && viewerUserID != 0,
ID: forumGeneralTopicID,
Date: view.Channel.Date,
Peer: &tg.PeerChannel{ChannelID: view.Channel.ID},
Title: "General",
IconColor: forumGeneralIconColor,
TopMessage: view.Channel.TopMessageID,
ReadInboxMaxID: view.Dialog.ReadInboxMaxID,
ReadOutboxMaxID: view.Dialog.ReadOutboxMaxID,
UnreadCount: view.Dialog.UnreadCount,
UnreadMentionsCount: view.Dialog.UnreadMentions,
FromID: &tg.PeerUser{UserID: view.Channel.CreatorUserID},
NotifySettings: *tdesktop.NotifySettings(),
}
}
func tgForumTopicFromDomain(viewerUserID int64, topic domain.ChannelForumTopic) *tg.ForumTopic {
iconColor := topic.IconColor
if iconColor == 0 {
iconColor = domain.DefaultForumTopicIconColor
}
return &tg.ForumTopic{
My: topic.CreatorUserID == viewerUserID && viewerUserID != 0,
Closed: topic.Closed,
Pinned: topic.Pinned,
Hidden: topic.Hidden,
TitleMissing: topic.TitleMissing,
ID: topic.TopicID,
Date: topic.Date,
Peer: &tg.PeerChannel{ChannelID: topic.ChannelID},
Title: topic.Title,
IconColor: iconColor,
IconEmojiID: topic.IconEmojiID,
TopMessage: topic.TopMessageID,
ReadInboxMaxID: topic.ReadInboxMaxID,
ReadOutboxMaxID: topic.ReadOutboxMaxID,
UnreadCount: topic.UnreadCount,
UnreadMentionsCount: topic.UnreadMentionsCount,
UnreadReactionsCount: topic.UnreadReactionsCount,
UnreadPollVotesCount: topic.UnreadPollVotesCount,
FromID: &tg.PeerUser{UserID: topic.CreatorUserID},
NotifySettings: *tdesktop.NotifySettings(),
}
}
func (r *Router) pinnedForumTopicUpdates(viewerUserID int64, channel domain.Channel, topicID int, pinned bool) *tg.Updates {
update := &tg.UpdatePinnedForumTopic{
Peer: &tg.PeerChannel{ChannelID: channel.ID},
TopicID: topicID,
}
update.SetPinned(pinned)
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) pinnedForumTopicsOrderUpdates(viewerUserID int64, channel domain.Channel, order []int) *tg.Updates {
update := &tg.UpdatePinnedForumTopics{
Peer: &tg.PeerChannel{ChannelID: channel.ID},
Order: append([]int(nil), order...),
}
if order != nil {
update.SetOrder(append([]int(nil), order...))
}
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func forumTopicQueryMatchesGeneral(query string) bool {
query = strings.TrimSpace(strings.ToLower(query))
return query == "" || strings.Contains("general", query)
}
func (r *Router) pushReadHistoryEvent(ctx context.Context, userID int64, event domain.UpdateEvent) {
if r.hasReliableUpdateDispatch() {
return
}
if r.deps.Sessions == nil || userID == 0 {
return
}
var update tg.UpdateClass
switch event.Type {
case domain.UpdateEventReadHistoryInbox:
update = tgReadHistoryInboxUpdate(event)
case domain.UpdateEventReadHistoryOutbox:
update = tgReadHistoryOutboxUpdate(event)
}
if update == nil {
return
}
updates := &tg.Updates{
Updates: []tg.UpdateClass{update},
Date: event.Date,
Seq: 0,
}
r.pushUserMessage(ctx, userID, "push read history", updates)
}
func tgReadHistoryInbox(event domain.UpdateEvent) *tg.UpdateReadHistoryInbox {
peer := tgPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdateReadHistoryInbox{
Peer: peer,
MaxID: event.MaxID,
StillUnreadCount: event.StillUnreadCount,
Pts: event.Pts,
PtsCount: event.PtsCount,
}
}
func tgReadHistoryInboxUpdate(event domain.UpdateEvent) tg.UpdateClass {
if event.Peer.Type == domain.PeerTypeChannel && event.Peer.ID != 0 {
return &tg.UpdateReadChannelInbox{
ChannelID: event.Peer.ID,
MaxID: event.MaxID,
StillUnreadCount: event.StillUnreadCount,
Pts: event.Pts,
}
}
return tgReadHistoryInbox(event)
}
func tgReadHistoryOutbox(event domain.UpdateEvent) *tg.UpdateReadHistoryOutbox {
peer := tgPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdateReadHistoryOutbox{
Peer: peer,
MaxID: event.MaxID,
Pts: event.Pts,
PtsCount: event.PtsCount,
}
}
func tgReadHistoryOutboxUpdate(event domain.UpdateEvent) tg.UpdateClass {
if event.Peer.Type == domain.PeerTypeChannel && event.Peer.ID != 0 {
return &tg.UpdateReadChannelOutbox{
ChannelID: event.Peer.ID,
MaxID: event.MaxID,
}
}
return tgReadHistoryOutbox(event)
}
func messagesNotModifiedOrEmpty(hash int64) tg.MessagesMessagesClass {
if hash != 0 {
return &tg.MessagesMessagesNotModified{Count: 0}
}
return &tg.MessagesMessages{}
}
func messagesAllStickersEmpty(hash int64) tg.MessagesAllStickersClass {
if hash != 0 {
return &tg.MessagesAllStickersNotModified{}
}
return &tg.MessagesAllStickers{Sets: []tg.StickerSet{}}
}
func messagesFeaturedStickersEmpty(hash int64) tg.MessagesFeaturedStickersClass {
if hash != 0 {
return &tg.MessagesFeaturedStickersNotModified{Count: 0}
}
return &tg.MessagesFeaturedStickers{
Count: 0,
Sets: []tg.StickerSetCoveredClass{},
Unread: []int64{},
}
}
func tgEditMessageUpdates(event domain.UpdateEvent, msg domain.Message, users []tg.UserClass, chats []tg.ChatClass) *tg.Updates {
update := tgOtherUpdateFromEvent(domain.UpdateEvent{
Type: domain.UpdateEventEditMessage,
Pts: event.Pts,
PtsCount: event.PtsCount,
Message: msg,
})
if update == nil {
return &tg.Updates{Date: event.Date}
}
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Users: users,
Chats: chats,
Date: event.Date,
Seq: 0,
}
}
func tgPrivateMessageUpdates(event domain.UpdateEvent, msg domain.Message, randomID int64, includeMessageID bool, users []tg.UserClass, chats []tg.ChatClass) *tg.Updates {
updates := make([]tg.UpdateClass, 0, 2)
if includeMessageID {
updates = append(updates, &tg.UpdateMessageID{ID: msg.ID, RandomID: randomID})
}
item := tgMessage(msg)
if item == nil {
item = &tg.MessageEmpty{ID: msg.ID}
}
updates = append(updates, &tg.UpdateNewMessage{
Message: item,
Pts: event.Pts,
PtsCount: event.PtsCount,
})
date := event.Date
if date == 0 {
date = msg.Date
}
return &tg.Updates{
Updates: updates,
Users: users,
Chats: chats,
Date: date,
Seq: 0, // 私聊不维护账号级 seq恒 0客户端仅靠 pts 同步)
}
}
func tgForwardMessagesUpdates(res domain.ForwardPrivateMessagesResult, randomIDs []int64, users []tg.UserClass, chats []tg.ChatClass) *tg.Updates {
updates := make([]tg.UpdateClass, 0, len(res.SenderMessages)*2)
date := 0
for i, msg := range res.SenderMessages {
randomID := int64(0)
if i < len(randomIDs) {
randomID = randomIDs[i]
}
updates = append(updates, &tg.UpdateMessageID{ID: msg.ID, RandomID: randomID})
event := domain.UpdateEvent{}
if i < len(res.SenderEvents) {
event = res.SenderEvents[i]
}
item := tgMessage(msg)
if item == nil {
item = &tg.MessageEmpty{ID: msg.ID}
}
pts := event.Pts
if pts == 0 {
pts = msg.Pts
}
ptsCount := event.PtsCount
if ptsCount == 0 {
ptsCount = 1
}
updates = append(updates, &tg.UpdateNewMessage{
Message: item,
Pts: pts,
PtsCount: ptsCount,
})
if date == 0 {
date = event.Date
}
if date == 0 {
date = msg.Date
}
}
return &tg.Updates{
Updates: updates,
Users: users,
Chats: chats,
Date: date,
Seq: 0,
}
}
func (r *Router) usersForMessageUpdate(ctx context.Context, ownerUserID int64, msg domain.Message) []tg.UserClass {
seen := make(map[int64]struct{}, 2)
users := make([]tg.UserClass, 0, 2)
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
switch {
case id == domain.OfficialSystemUserID:
users = append(users, r.tgUser(domain.OfficialSystemUser()))
case id == ownerUserID:
if r.deps.Users == nil {
return
}
u, err := r.deps.Users.Self(ctx, ownerUserID)
if err == nil && u.ID != 0 {
users = append(users, r.tgSelfUser(u))
}
default:
if r.deps.Users == nil {
return
}
u, found, err := r.deps.Users.ByID(ctx, ownerUserID, id)
if err == nil && found {
users = append(users, r.tgUser(u))
}
}
}
if msg.From.Type == domain.PeerTypeUser {
add(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeUser {
add(msg.Peer.ID)
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeUser {
add(msg.Forward.From.ID)
}
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeUser {
add(msg.ReplyTo.Peer.ID)
}
return users
}
func (r *Router) usersForMessageUpdates(ctx context.Context, ownerUserID int64, messages []domain.Message) []tg.UserClass {
seen := make(map[int64]struct{}, len(messages)*2)
ids := make([]int64, 0, len(messages)*2)
addID := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
ids = append(ids, id)
}
for _, msg := range messages {
if msg.From.Type == domain.PeerTypeUser {
addID(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeUser {
addID(msg.Peer.ID)
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeUser {
addID(msg.Forward.From.ID)
}
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeUser {
addID(msg.ReplyTo.Peer.ID)
}
}
if len(ids) == 0 {
return nil
}
loaded := make(map[int64]domain.User, len(ids))
if r.deps.Users != nil {
if users, err := r.deps.Users.ByIDs(ctx, ownerUserID, ids); err == nil {
for _, user := range users {
loaded[user.ID] = user
}
}
}
users := make([]tg.UserClass, 0, len(ids))
for _, id := range ids {
switch {
case id == domain.OfficialSystemUserID:
users = append(users, r.tgUser(domain.OfficialSystemUser()))
case id == ownerUserID:
if user, ok := loaded[id]; ok {
users = append(users, r.tgSelfUser(user))
}
default:
if user, ok := loaded[id]; ok {
users = append(users, r.tgUser(user))
}
}
}
return users
}
func (r *Router) chatsForMessageUpdate(ctx context.Context, ownerUserID int64, msg domain.Message) []tg.ChatClass {
if r.deps.Channels == nil {
return nil
}
seen := make(map[int64]struct{}, 2)
chats := make([]tg.ChatClass, 0, 2)
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
view, err := r.deps.Channels.GetChannel(ctx, ownerUserID, id)
if err != nil || view.Channel.ID == 0 {
return
}
chats = append(chats, tgChannelChat(ownerUserID, view.Channel, &view.Self))
}
if msg.From.Type == domain.PeerTypeChannel {
add(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeChannel {
add(msg.Peer.ID)
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeChannel {
add(msg.Forward.From.ID)
}
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeChannel {
add(msg.ReplyTo.Peer.ID)
}
return chats
}
func (r *Router) chatsForMessageUpdates(ctx context.Context, ownerUserID int64, messages []domain.Message) []tg.ChatClass {
seen := make(map[int64]struct{}, len(messages))
chats := make([]tg.ChatClass, 0, len(messages))
for _, msg := range messages {
for _, chat := range r.chatsForMessageUpdate(ctx, ownerUserID, msg) {
channel, ok := chat.(*tg.Channel)
if !ok || channel.ID == 0 {
continue
}
if _, ok := seen[channel.ID]; ok {
continue
}
seen[channel.ID] = struct{}{}
chats = append(chats, chat)
}
}
return chats
}