564 lines
18 KiB
Go
564 lines
18 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"strings"
|
|
"telesrv/internal/compat/tdesktop"
|
|
"telesrv/internal/domain"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
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,
|
|
}
|
|
echoCache := newViewerPeerCache(r)
|
|
updates := r.channelMessageUpdatesWithPeerCache(ctx, userID, sendRes, req.RandomID, echoCache)
|
|
if !res.Duplicate {
|
|
r.enqueueChannelMessageFanout(ctx, userID, sendRes, nil)
|
|
}
|
|
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,
|
|
}
|
|
echoCache := newViewerPeerCache(r)
|
|
updates := r.channelMessageUpdatesWithPeerCache(ctx, userID, sendRes, 0, echoCache)
|
|
r.enqueueChannelMessageFanout(ctx, userID, sendRes, nil)
|
|
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.enqueueChannelFanout(ctx, channelFanoutMessageBox, userID, res.Channel.ID, res.Event.Pts, res.Recipients, func(_ context.Context, viewerUserID int64) *tg.Updates {
|
|
return &tg.Updates{
|
|
Updates: []tg.UpdateClass{tgChannelUpdate(viewerUserID, res.Event)},
|
|
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)},
|
|
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) 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 || utf8.RuneCountInString(req.Q) > maxMessageSearchQLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
// 官方客户端会请求超过单页上限的数量(telegram-tt 首屏 loadTopics 发 limit=500),对齐官方与
|
|
// store 行为=钳到服务端单页上限,而非报 LIMIT_INVALID;客户端据响应里的 count 用 offset 翻页取其余。
|
|
effectiveLimit := req.Limit
|
|
if effectiveLimit > domain.MaxChannelForumTopicsLimit {
|
|
effectiveLimit = domain.MaxChannelForumTopicsLimit
|
|
}
|
|
view, err := r.forumTopicPeerView(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !view.Channel.Forum {
|
|
return nil, channelForumMissingErr()
|
|
}
|
|
includeGeneral := effectiveLimit > 0 && forumTopicQueryMatchesGeneral(req.Q)
|
|
list := domain.ChannelForumTopicList{}
|
|
if r.deps.Channels != nil && effectiveLimit > 0 {
|
|
limit := effectiveLimit
|
|
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) == 0 {
|
|
return nil, topicsEmptyErr()
|
|
}
|
|
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
|
|
requestedIDs := make([]int, 0, len(req.Topics))
|
|
ids := make([]int, 0, len(req.Topics))
|
|
seen := make(map[int]struct{}, len(req.Topics))
|
|
for _, topicID := range req.Topics {
|
|
if topicID <= 0 || topicID > domain.MaxMessageBoxID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
if _, ok := seen[topicID]; ok {
|
|
continue
|
|
}
|
|
seen[topicID] = struct{}{}
|
|
requestedIDs = append(requestedIDs, topicID)
|
|
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.forumTopicsByIDResponse(ctx, userID, view, list, includeGeneral, requestedIDs), nil
|
|
}
|
|
|
|
// forumTopicsByIDResponse keeps the request's unique ID order and returns one
|
|
// constructor for every requested topic. Telegram clients use
|
|
// forumTopicDeleted as a positive deletion/missing confirmation; silently
|
|
// omitting an ID leaves their local thread state stale and causes repeat reads.
|
|
func (r *Router) forumTopicsByIDResponse(ctx context.Context, userID int64, view domain.ChannelView, list domain.ChannelForumTopicList, includeGeneral bool, requestedIDs []int) *tg.MessagesForumTopics {
|
|
response := r.forumTopicsResponse(ctx, userID, view, list, includeGeneral)
|
|
live := make(map[int]tg.ForumTopicClass, len(response.Topics))
|
|
for _, topic := range response.Topics {
|
|
switch topic := topic.(type) {
|
|
case *tg.ForumTopic:
|
|
live[topic.ID] = topic
|
|
case *tg.ForumTopicDeleted:
|
|
live[topic.ID] = topic
|
|
}
|
|
}
|
|
response.Topics = make([]tg.ForumTopicClass, 0, len(requestedIDs))
|
|
for _, topicID := range requestedIDs {
|
|
if topic, ok := live[topicID]; ok {
|
|
response.Topics = append(response.Topics, topic)
|
|
} else {
|
|
response.Topics = append(response.Topics, &tg.ForumTopicDeleted{ID: topicID})
|
|
}
|
|
}
|
|
response.Count = len(response.Topics)
|
|
return response
|
|
}
|
|
|
|
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++
|
|
general := domain.ChannelForumTopic{TopMessageID: view.Channel.TopMessageID}
|
|
if r.deps.Channels != nil {
|
|
if g, err := r.deps.Channels.GeneralForumTopic(ctx, userID, view.Channel.ID); err == nil {
|
|
general = g
|
|
}
|
|
}
|
|
topics = append(topics, tgForumGeneralTopic(userID, view, general))
|
|
userIDs = append(userIDs, view.Channel.CreatorUserID)
|
|
if general.TopMessageID > 0 && r.deps.Channels != nil {
|
|
if history, err := r.deps.Channels.GetMessages(ctx, userID, view.Channel.ID, []int{general.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 r.applyStoryMaxIDsToForumTopics(ctx, userID, &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, topic domain.ChannelForumTopic) *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: topic.TopMessageID,
|
|
ReadInboxMaxID: topic.ReadInboxMaxID,
|
|
ReadOutboxMaxID: topic.ReadOutboxMaxID,
|
|
UnreadCount: topic.UnreadCount,
|
|
UnreadMentionsCount: topic.UnreadMentionsCount,
|
|
UnreadReactionsCount: topic.UnreadReactionsCount,
|
|
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{tgChannelChatMin(viewerUserID, channel)},
|
|
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{tgChannelChatMin(viewerUserID, channel)},
|
|
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)
|
|
}
|