4014 lines
130 KiB
Go
4014 lines
130 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gotd/td/tg"
|
|
"github.com/gotd/td/tgerr"
|
|
"go.uber.org/zap"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
const (
|
|
maxChannelTitleLength = 128
|
|
maxChannelAboutLength = 255
|
|
maxChannelUsernameOrder = 32
|
|
maxChannelReportMessageIDs = 100
|
|
maxChannelSearchPostsLimit = 50
|
|
maxChannelSearchPostsQuery = 256
|
|
maxChannelPaidMessageStars = 10000
|
|
maxChannelBoostsToUnblockRestrictions = 8
|
|
maxChatAvailableReactions = 64
|
|
maxChatInviteListLimit = 100
|
|
maxChatInviteLinkLength = 256
|
|
maxChatInviteSearchLength = 256
|
|
)
|
|
|
|
// registerChannels 注册超级群/频道相关 RPC。messages.createChat 在这里注册,
|
|
// 因为 telesrv 将普通群创建直接实现为 megagroup。
|
|
func (r *Router) registerChannels(d *tg.ServerDispatcher) {
|
|
d.OnMessagesCreateChat(r.onMessagesCreateChat)
|
|
d.OnMessagesMigrateChat(r.onMessagesMigrateChat)
|
|
d.OnMessagesGetChats(r.onMessagesGetChats)
|
|
d.OnMessagesGetFullChat(r.onMessagesGetFullChat)
|
|
d.OnMessagesAddChatUser(r.onMessagesAddChatUser)
|
|
d.OnMessagesDeleteChatUser(r.onMessagesDeleteChatUser)
|
|
d.OnMessagesEditChatTitle(r.onMessagesEditChatTitle)
|
|
d.OnMessagesEditChatPhoto(r.onMessagesEditChatPhoto)
|
|
d.OnMessagesEditChatAdmin(r.onMessagesEditChatAdmin)
|
|
d.OnMessagesEditChatAbout(r.onMessagesEditChatAbout)
|
|
d.OnMessagesEditChatDefaultBannedRights(r.onMessagesEditChatDefaultBannedRights)
|
|
d.OnMessagesEditChatCreator(r.onMessagesEditChatCreator)
|
|
d.OnMessagesEditChatParticipantRank(r.onMessagesEditChatParticipantRank)
|
|
d.OnMessagesSetChatTheme(r.onMessagesSetChatTheme)
|
|
d.OnMessagesToggleNoForwards(r.onMessagesToggleNoForwards)
|
|
d.OnMessagesSetChatAvailableReactions(r.onMessagesSetChatAvailableReactions)
|
|
d.OnChannelsCreateChannel(r.onChannelsCreateChannel)
|
|
d.OnChannelsGetChannels(r.onChannelsGetChannels)
|
|
d.OnChannelsGetFullChannel(r.onChannelsGetFullChannel)
|
|
d.OnChannelsGetParticipants(r.onChannelsGetParticipants)
|
|
d.OnChannelsGetParticipant(r.onChannelsGetParticipant)
|
|
d.OnChannelsGetSendAs(r.onChannelsGetSendAs)
|
|
d.OnChannelsCheckUsername(r.onChannelsCheckUsername)
|
|
d.OnChannelsUpdateUsername(r.onChannelsUpdateUsername)
|
|
d.OnChannelsGetAdminedPublicChannels(r.onChannelsGetAdminedPublicChannels)
|
|
d.OnChannelsExportMessageLink(r.onChannelsExportMessageLink)
|
|
d.OnChannelsToggleSignatures(r.onChannelsToggleSignatures)
|
|
d.OnChannelsTogglePreHistoryHidden(r.onChannelsTogglePreHistoryHidden)
|
|
d.OnChannelsToggleSlowMode(r.onChannelsToggleSlowMode)
|
|
d.OnChannelsSetStickers(r.onChannelsSetStickers)
|
|
d.OnChannelsSetEmojiStickers(r.onChannelsSetEmojiStickers)
|
|
d.OnChannelsReorderUsernames(r.onChannelsReorderUsernames)
|
|
d.OnChannelsToggleUsername(r.onChannelsToggleUsername)
|
|
d.OnChannelsDeactivateAllUsernames(r.onChannelsDeactivateAllUsernames)
|
|
d.OnChannelsUpdateColor(r.onChannelsUpdateColor)
|
|
d.OnChannelsUpdateEmojiStatus(r.onChannelsUpdateEmojiStatus)
|
|
d.OnChannelsReadMessageContents(r.onChannelsReadMessageContents)
|
|
d.OnChannelsReportSpam(r.onChannelsReportSpam)
|
|
d.OnChannelsGetLeftChannels(r.onChannelsGetLeftChannels)
|
|
d.OnChannelsGetInactiveChannels(r.onChannelsGetInactiveChannels)
|
|
d.OnChannelsGetGroupsForDiscussion(r.onChannelsGetGroupsForDiscussion)
|
|
d.OnChannelsSetDiscussionGroup(r.onChannelsSetDiscussionGroup)
|
|
d.OnChannelsEditLocation(r.onChannelsEditLocation)
|
|
d.OnChannelsConvertToGigagroup(r.onChannelsConvertToGigagroup)
|
|
d.OnChannelsDeleteParticipantHistory(r.onChannelsDeleteParticipantHistory)
|
|
d.OnChannelsToggleJoinToSend(r.onChannelsToggleJoinToSend)
|
|
d.OnChannelsToggleJoinRequest(r.onChannelsToggleJoinRequest)
|
|
d.OnChannelsToggleForum(r.onChannelsToggleForum)
|
|
d.OnChannelsToggleAntiSpam(r.onChannelsToggleAntiSpam)
|
|
d.OnChannelsReportAntiSpamFalsePositive(r.onChannelsReportAntiSpamFalsePositive)
|
|
d.OnChannelsToggleParticipantsHidden(r.onChannelsToggleParticipantsHidden)
|
|
d.OnChannelsToggleViewForumAsMessages(r.onChannelsToggleViewForumAsMessages)
|
|
d.OnChannelsGetChannelRecommendations(r.onChannelsGetChannelRecommendations)
|
|
d.OnChannelsSetBoostsToUnblockRestrictions(r.onChannelsSetBoostsToUnblockRestrictions)
|
|
d.OnChannelsRestrictSponsoredMessages(r.onChannelsRestrictSponsoredMessages)
|
|
d.OnChannelsSearchPosts(r.onChannelsSearchPosts)
|
|
d.OnChannelsUpdatePaidMessagesPrice(r.onChannelsUpdatePaidMessagesPrice)
|
|
d.OnChannelsToggleAutotranslation(r.onChannelsToggleAutotranslation)
|
|
d.OnChannelsGetMessageAuthor(r.onChannelsGetMessageAuthor)
|
|
d.OnChannelsCheckSearchPostsFlood(r.onChannelsCheckSearchPostsFlood)
|
|
d.OnChannelsSetMainProfileTab(r.onChannelsSetMainProfileTab)
|
|
d.OnChannelsInviteToChannel(r.onChannelsInviteToChannel)
|
|
d.OnChannelsJoinChannel(r.onChannelsJoinChannel)
|
|
d.OnChannelsLeaveChannel(r.onChannelsLeaveChannel)
|
|
d.OnChannelsEditAdmin(r.onChannelsEditAdmin)
|
|
d.OnChannelsEditBanned(r.onChannelsEditBanned)
|
|
d.OnChannelsEditTitle(r.onChannelsEditTitle)
|
|
d.OnChannelsEditPhoto(r.onChannelsEditPhoto)
|
|
d.OnChannelsDeleteChannel(r.onChannelsDeleteChannel)
|
|
d.OnChannelsGetAdminLog(r.onChannelsGetAdminLog)
|
|
d.OnChannelsReadHistory(r.onChannelsReadHistory)
|
|
d.OnChannelsGetMessages(r.onChannelsGetMessages)
|
|
d.OnChannelsDeleteMessages(r.onChannelsDeleteMessages)
|
|
d.OnChannelsDeleteHistory(r.onChannelsDeleteHistory)
|
|
d.OnMessagesUpdatePinnedMessage(r.onMessagesUpdatePinnedMessage)
|
|
d.OnMessagesUnpinAllMessages(r.onMessagesUnpinAllMessages)
|
|
d.OnMessagesExportChatInvite(r.onMessagesExportChatInvite)
|
|
d.OnMessagesCheckChatInvite(r.onMessagesCheckChatInvite)
|
|
d.OnMessagesImportChatInvite(r.onMessagesImportChatInvite)
|
|
d.OnMessagesGetExportedChatInvites(r.onMessagesGetExportedChatInvites)
|
|
d.OnMessagesGetExportedChatInvite(r.onMessagesGetExportedChatInvite)
|
|
d.OnMessagesEditExportedChatInvite(r.onMessagesEditExportedChatInvite)
|
|
d.OnMessagesDeleteRevokedExportedChatInvites(r.onMessagesDeleteRevokedExportedChatInvites)
|
|
d.OnMessagesDeleteExportedChatInvite(r.onMessagesDeleteExportedChatInvite)
|
|
d.OnMessagesGetAdminsWithInvites(r.onMessagesGetAdminsWithInvites)
|
|
d.OnMessagesGetChatInviteImporters(r.onMessagesGetChatInviteImporters)
|
|
d.OnMessagesHideChatJoinRequest(r.onMessagesHideChatJoinRequest)
|
|
d.OnMessagesHideAllChatJoinRequests(r.onMessagesHideAllChatJoinRequests)
|
|
d.OnUpdatesGetChannelDifference(r.onUpdatesGetChannelDifference)
|
|
}
|
|
|
|
func (r *Router) onMessagesCreateChat(ctx context.Context, req *tg.MessagesCreateChatRequest) (*tg.MessagesInvitedUsers, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if !validChannelTitle(req.Title) || len(req.Users) > domain.MaxChannelInviteUsers {
|
|
return nil, channelInvalidErr(domain.ErrChannelTitleInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
memberIDs, err := r.userIDsFromInputUsers(ctx, userID, req.Users)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
memberIDs = createChatInviteMemberIDs(memberIDs, userID)
|
|
date := int(r.clock.Now().Unix())
|
|
r.log.Debug("messages.createChat resolved users",
|
|
zap.Int("input_users", len(req.Users)),
|
|
zap.Int("member_ids", len(memberIDs)),
|
|
zap.Int64s("member_user_ids", memberIDs),
|
|
)
|
|
if len(memberIDs) == 0 {
|
|
return nil, usersTooFewErr()
|
|
}
|
|
createRes, err := r.deps.Channels.CreateMegagroupFromCreateChat(ctx, userID, domain.CreateChannelRequest{
|
|
CreatorUserID: userID,
|
|
Title: req.Title,
|
|
TTLPeriod: req.TTLPeriod,
|
|
Date: date,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(createRes.Channel.ID, channelMemberUserIDs(createRes.Members)...)
|
|
|
|
responseRes := createRes
|
|
var inviteRes domain.CreateChannelResult
|
|
if len(memberIDs) > 0 {
|
|
inviteRes, err = r.deps.Channels.InviteToChannel(ctx, userID, createRes.Channel.ID, memberIDs, date)
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(inviteRes.Channel.ID, channelMemberUserIDs(inviteRes.Members)...)
|
|
responseRes.Channel = inviteRes.Channel
|
|
responseRes.Members = mergeChannelMembers(createRes.Members, inviteRes.Members)
|
|
responseRes.Recipients = uniqueRecipientIDs(append(append([]int64{}, createRes.Recipients...), inviteRes.Recipients...))
|
|
}
|
|
|
|
updates := r.channelOperationUpdates(ctx, userID, responseRes)
|
|
if tdesktopCreateChatNeedsLegacyChat(ctx) {
|
|
updates = r.tdesktopCreateChatUpdates(ctx, userID, responseRes)
|
|
}
|
|
if inviteRes.Event.Pts != 0 {
|
|
inviteUpdates := r.channelOperationUpdates(ctx, userID, inviteRes)
|
|
if inviteUpdates != nil {
|
|
updates.Updates = append(updates.Updates, inviteUpdates.Updates...)
|
|
}
|
|
}
|
|
if inviteRes.Event.Pts != 0 {
|
|
r.pushChannelExplicitUpdates(ctx, userID, inviteRes.Channel.ID, memberIDs, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, inviteRes)
|
|
})
|
|
}
|
|
return &tg.MessagesInvitedUsers{Updates: updates, MissingInvitees: []tg.MissingInvitee{}}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesMigrateChat(ctx context.Context, chatID int64) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if chatID <= 0 {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
userID, view, err := r.channelChangeInfoView(ctx, &tg.InputChannel{ChannelID: chatID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !view.Channel.Megagroup {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
return r.channelStateUpdates(userID, view.Channel), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetChats(ctx context.Context, ids []int64) (tg.MessagesChatsClass, error) {
|
|
if len(ids) > maxGetMessagesIDs {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
chats := make([]tg.ChatClass, 0, len(ids))
|
|
if r.deps.Channels != nil {
|
|
for _, id := range ids {
|
|
if id <= 0 {
|
|
continue
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, id)
|
|
if err != nil {
|
|
if isChannelNotFound(err) {
|
|
continue
|
|
}
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
chats = append(chats, tgChannelChat(userID, view.Channel, &view.Self))
|
|
}
|
|
}
|
|
return &tg.MessagesChats{Chats: chats}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetFullChat(ctx context.Context, chatID int64) (*tg.MessagesChatFull, error) {
|
|
if chatID <= 0 {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
return r.onChannelsGetFullChannel(ctx, &tg.InputChannel{ChannelID: chatID})
|
|
}
|
|
|
|
func (r *Router) onMessagesAddChatUser(ctx context.Context, req *tg.MessagesAddChatUserRequest) (*tg.MessagesInvitedUsers, error) {
|
|
if req.ChatID <= 0 || req.UserID == nil {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
return r.onChannelsInviteToChannel(ctx, &tg.ChannelsInviteToChannelRequest{
|
|
Channel: &tg.InputChannel{ChannelID: req.ChatID},
|
|
Users: []tg.InputUserClass{req.UserID},
|
|
})
|
|
}
|
|
|
|
func (r *Router) onMessagesDeleteChatUser(ctx context.Context, req *tg.MessagesDeleteChatUserRequest) (tg.UpdatesClass, error) {
|
|
if req.ChatID <= 0 || req.UserID == nil {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
target, found, err := r.userFromInput(ctx, userID, req.UserID)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found || target.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
if target.ID == userID {
|
|
return r.onChannelsLeaveChannel(ctx, &tg.InputChannel{ChannelID: req.ChatID})
|
|
}
|
|
return r.onChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
|
|
Channel: &tg.InputChannel{ChannelID: req.ChatID},
|
|
Participant: &tg.InputPeerUser{UserID: target.ID, AccessHash: target.AccessHash},
|
|
BannedRights: tg.ChatBannedRights{
|
|
ViewMessages: true,
|
|
UntilDate: 0,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatTitle(ctx context.Context, req *tg.MessagesEditChatTitleRequest) (tg.UpdatesClass, error) {
|
|
if req.ChatID <= 0 {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
return r.onChannelsEditTitle(ctx, &tg.ChannelsEditTitleRequest{
|
|
Channel: &tg.InputChannel{ChannelID: req.ChatID},
|
|
Title: req.Title,
|
|
})
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatPhoto(ctx context.Context, req *tg.MessagesEditChatPhotoRequest) (tg.UpdatesClass, error) {
|
|
if req.ChatID <= 0 || req.Photo == nil {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
return r.onChannelsEditPhoto(ctx, &tg.ChannelsEditPhotoRequest{
|
|
Channel: &tg.InputChannel{ChannelID: req.ChatID},
|
|
Photo: req.Photo,
|
|
})
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatAdmin(ctx context.Context, req *tg.MessagesEditChatAdminRequest) (bool, error) {
|
|
if req.ChatID <= 0 || req.UserID == nil {
|
|
return false, peerIDInvalidErr()
|
|
}
|
|
rights := tg.ChatAdminRights{}
|
|
if req.IsAdmin {
|
|
rights = legacyBasicGroupAdminRights()
|
|
}
|
|
_, err := r.onChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{
|
|
Channel: &tg.InputChannel{ChannelID: req.ChatID},
|
|
UserID: req.UserID,
|
|
AdminRights: rights,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatAbout(ctx context.Context, req *tg.MessagesEditChatAboutRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return false, notImplementedErr()
|
|
}
|
|
if utf8.RuneCountInString(req.About) > maxChannelAboutLength {
|
|
return false, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
channel, err := r.deps.Channels.EditAbout(ctx, userID, domain.EditChannelAboutRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
About: req.About,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return false, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatDefaultBannedRights(ctx context.Context, req *tg.MessagesEditChatDefaultBannedRightsRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
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 peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
channel, err := r.deps.Channels.EditDefaultBannedRights(ctx, userID, domain.EditChannelDefaultBannedRightsRequest{
|
|
UserID: userID,
|
|
ChannelID: peer.ID,
|
|
BannedRights: domainChannelBannedRights(req.BannedRights),
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, channel)
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.MessagesEditChatCreatorRequest) (tg.UpdatesClass, error) {
|
|
if req.UserID == nil {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if _, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, found, err := r.userFromInput(ctx, userID, req.UserID); err != nil {
|
|
return nil, internalErr()
|
|
} else if !found {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
return nil, tgerr.New(400, "PASSWORD_HASH_INVALID")
|
|
}
|
|
|
|
func (r *Router) onMessagesEditChatParticipantRank(ctx context.Context, req *tg.MessagesEditChatParticipantRankRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if len(req.Rank) > domain.MaxChannelAdminRankLength {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
participant, ok := r.domainPeerFromInputPeer(userID, req.Participant)
|
|
if !ok || participant.Type != domain.PeerTypeUser || participant.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
member, err := r.deps.Channels.GetParticipant(ctx, userID, channelID, participant.ID)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
return r.onChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{
|
|
Channel: &tg.InputChannel{ChannelID: channelID},
|
|
UserID: &tg.InputUser{UserID: participant.ID},
|
|
AdminRights: tgChatAdminRights(member.AdminRights),
|
|
Rank: req.Rank,
|
|
})
|
|
}
|
|
|
|
func (r *Router) onMessagesSetChatTheme(ctx context.Context, req *tg.MessagesSetChatThemeRequest) (tg.UpdatesClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer); err == nil {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, channelID)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
return r.channelStateUpdates(userID, view.Channel), nil
|
|
} else if _, ok := channelIDFromLegacyInputPeer(userID, req.Peer); ok {
|
|
return nil, err
|
|
}
|
|
peer, ok := r.domainPeerFromInputPeer(userID, req.Peer)
|
|
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesToggleNoForwards(ctx context.Context, req *tg.MessagesToggleNoForwardsRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetNoForwards(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, channel)
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesSetChatAvailableReactions(ctx context.Context, req *tg.MessagesSetChatAvailableReactionsRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
policy, err := domainChannelReactionPolicy(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetAvailableReactions(ctx, userID, channelID, policy)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, channel)
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsCreateChannel(ctx context.Context, req *tg.ChannelsCreateChannelRequest) (tg.UpdatesClass, error) {
|
|
if err := validateChannelsCreateChannelOptions(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if !validChannelTitle(req.Title) || utf8.RuneCountInString(req.About) > maxChannelAboutLength {
|
|
return nil, channelInvalidErr(domain.ErrChannelTitleInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
res, err := r.deps.Channels.CreateChannel(ctx, userID, domain.CreateChannelRequest{
|
|
CreatorUserID: userID,
|
|
Title: req.Title,
|
|
About: req.About,
|
|
Broadcast: req.Broadcast,
|
|
Megagroup: req.Megagroup,
|
|
Forum: req.Forum,
|
|
ForumTabs: req.Forum,
|
|
TTLPeriod: req.TTLPeriod,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func validateChannelsCreateChannelOptions(req *tg.ChannelsCreateChannelRequest) error {
|
|
if req == nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if req.ForImport {
|
|
return chatInvalidErr()
|
|
}
|
|
if req.GeoPoint != nil || req.Address != "" {
|
|
return addressInvalidErr()
|
|
}
|
|
if req.TTLPeriod < 0 {
|
|
return ttlPeriodInvalidErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetChannels(ctx context.Context, ids []tg.InputChannelClass) (tg.MessagesChatsClass, error) {
|
|
if len(ids) > maxGetMessagesIDs {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
chats := make([]tg.ChatClass, 0, len(ids))
|
|
for _, input := range ids {
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok || ref.ID == 0 || r.deps.Channels == nil {
|
|
continue
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, ref.ID)
|
|
if err != nil {
|
|
if isChannelNotFound(err) {
|
|
continue
|
|
}
|
|
return nil, internalErr()
|
|
}
|
|
if !inputChannelAccessHashMatches(ref, view.Channel) {
|
|
continue
|
|
}
|
|
chats = append(chats, tgChannelChat(userID, view.Channel, &view.Self))
|
|
}
|
|
return &tg.MessagesChats{Chats: chats}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputChannelClass) (*tg.MessagesChatFull, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesChatFull{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
_, view, err := r.channelView(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
full := tgChannelFull(view)
|
|
userIDs := []int64{view.Channel.CreatorUserID, view.Self.UserID}
|
|
if canViewChannelJoinRequests(view.Self) {
|
|
userIDs = r.applyPendingJoinRequestsToFullChannel(ctx, full, view.Channel.ID, userIDs)
|
|
}
|
|
r.trackChannelInterest(ctx, userID, view.Channel.ID)
|
|
return &tg.MessagesChatFull{
|
|
FullChat: full,
|
|
Chats: []tg.ChatClass{tgChannelChat(userID, view.Channel, &view.Self)},
|
|
Users: r.tgUsersForIDs(ctx, userID, userIDs),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetSendAs(ctx context.Context, req *tg.ChannelsGetSendAsRequest) (*tg.ChannelsSendAsPeers, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if userID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
peer, ok := r.domainPeerFromInputPeer(userID, req.Peer)
|
|
if !ok || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
chats := []tg.ChatClass(nil)
|
|
peers := []tg.SendAsPeer{{Peer: &tg.PeerUser{UserID: userID}}}
|
|
if peer.Type == domain.PeerTypeChannel {
|
|
if r.deps.Channels == nil {
|
|
return &tg.ChannelsSendAsPeers{}, nil
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
if ref, ok := inputPeerChannelRef(req.Peer); ok {
|
|
if ref.ID != view.Channel.ID || (ref.CheckAccessHash && !inputChannelAccessHashMatches(ref, view.Channel)) {
|
|
return nil, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
}
|
|
chats = []tg.ChatClass{tgChannelChat(userID, view.Channel, &view.Self)}
|
|
if canCurrentChannelSendAs(view) {
|
|
peers = append(peers, tg.SendAsPeer{Peer: &tg.PeerChannel{ChannelID: view.Channel.ID}})
|
|
}
|
|
}
|
|
return &tg.ChannelsSendAsPeers{
|
|
Peers: peers,
|
|
Chats: chats,
|
|
Users: r.tgUsersForIDs(ctx, userID, []int64{userID}),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsCheckUsername(ctx context.Context, req *tg.ChannelsCheckUsernameRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return false, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
okUsername, err := r.deps.Channels.CheckUsername(ctx, userID, channelID, req.Username)
|
|
if err != nil {
|
|
return false, channelUsernameErr(err)
|
|
}
|
|
return okUsername, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsUpdateUsername(ctx context.Context, req *tg.ChannelsUpdateUsernameRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return false, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
channel, err := r.deps.Channels.UpdateUsername(ctx, userID, domain.UpdateChannelUsernameRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Username: req.Username,
|
|
})
|
|
if err != nil {
|
|
return false, channelUsernameErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetAdminedPublicChannels(ctx context.Context, req *tg.ChannelsGetAdminedPublicChannelsRequest) (tg.MessagesChatsClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesChats{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if req.ByLocation {
|
|
return &tg.MessagesChats{}, nil
|
|
}
|
|
channels, err := r.deps.Channels.ListAdminedPublicChannels(ctx, userID)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
return &tg.MessagesChats{Chats: tgChannels(userID, channels)}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsExportMessageLink(ctx context.Context, req *tg.ChannelsExportMessageLinkRequest) (*tg.ExportedMessageLink, error) {
|
|
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
userID, view, err := r.channelView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
history, err := r.deps.Channels.GetMessages(ctx, userID, view.Channel.ID, []int{req.ID})
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
if len(history.Messages) != 1 || history.Messages[0].ID != req.ID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
link := ""
|
|
if view.Channel.Username != "" {
|
|
link = "https://t.me/" + view.Channel.Username + "/" + strconv.Itoa(req.ID)
|
|
} else {
|
|
link = "https://t.me/c/" + strconv.FormatInt(view.Channel.ID, 10) + "/" + strconv.Itoa(req.ID)
|
|
}
|
|
if req.Thread {
|
|
if rootID := channelMessageThreadRootID(history.Messages[0]); rootID > 0 && rootID != req.ID {
|
|
link += "?thread=" + strconv.Itoa(rootID)
|
|
}
|
|
}
|
|
return &tg.ExportedMessageLink{Link: link, HTML: ""}, nil
|
|
}
|
|
|
|
func channelMessageThreadRootID(msg domain.ChannelMessage) int {
|
|
if msg.ReplyTo == nil {
|
|
return 0
|
|
}
|
|
if msg.ReplyTo.TopMessageID > 0 {
|
|
return msg.ReplyTo.TopMessageID
|
|
}
|
|
return msg.ReplyTo.MessageID
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleSignatures(ctx context.Context, req *tg.ChannelsToggleSignaturesRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetSignatures(ctx, userID, channelID, req.SignaturesEnabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, channel)
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsTogglePreHistoryHidden(ctx context.Context, req *tg.ChannelsTogglePreHistoryHiddenRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
viewerUserID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, viewerUserID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetPreHistoryHidden(ctx, viewerUserID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(viewerUserID, channel)
|
|
r.pushChannelStateToMembers(ctx, viewerUserID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleSlowMode(ctx context.Context, req *tg.ChannelsToggleSlowModeRequest) (tg.UpdatesClass, error) {
|
|
if !domain.ValidChannelSlowModeSeconds(req.Seconds) {
|
|
return nil, secondsInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
viewerUserID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, viewerUserID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetSlowMode(ctx, viewerUserID, channelID, req.Seconds)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(viewerUserID, channel)
|
|
r.pushChannelStateToMembers(ctx, viewerUserID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSetStickers(ctx context.Context, req *tg.ChannelsSetStickersRequest) (bool, error) {
|
|
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !view.Channel.Megagroup || view.Channel.Broadcast {
|
|
return false, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
if err := validateEmptyChannelStickerSet(req.Stickerset); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSetEmojiStickers(ctx context.Context, req *tg.ChannelsSetEmojiStickersRequest) (bool, error) {
|
|
_, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !view.Channel.Megagroup || view.Channel.Broadcast {
|
|
return false, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
if err := validateEmptyChannelStickerSet(req.Stickerset); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func validateEmptyChannelStickerSet(stickerset tg.InputStickerSetClass) error {
|
|
if _, ok := stickerset.(*tg.InputStickerSetEmpty); ok {
|
|
return nil
|
|
}
|
|
return stickersetInvalidErr()
|
|
}
|
|
|
|
func (r *Router) onChannelsReorderUsernames(ctx context.Context, req *tg.ChannelsReorderUsernamesRequest) (bool, error) {
|
|
if len(req.Order) > maxChannelUsernameOrder {
|
|
return false, limitInvalidErr()
|
|
}
|
|
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleUsername(ctx context.Context, req *tg.ChannelsToggleUsernameRequest) (bool, error) {
|
|
if req.Username != "" && !validChannelManagementUsername(req.Username) {
|
|
return false, usernameInvalidErr()
|
|
}
|
|
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsDeactivateAllUsernames(ctx context.Context, input tg.InputChannelClass) (bool, error) {
|
|
if _, _, err := r.channelChangeInfoView(ctx, input); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsUpdateColor(ctx context.Context, req *tg.ChannelsUpdateColorRequest) (tg.UpdatesClass, error) {
|
|
viewerUserID, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetColor(ctx, viewerUserID, view.Channel.ID, req.ForProfile, domainPeerColorFromChannelUpdate(req))
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(viewerUserID, channel)
|
|
r.pushChannelStateToMembers(ctx, viewerUserID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsUpdateEmojiStatus(ctx context.Context, req *tg.ChannelsUpdateEmojiStatusRequest) (tg.UpdatesClass, error) {
|
|
viewerUserID, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status, err := domainChannelEmojiStatus(req.EmojiStatus)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetEmojiStatus(ctx, viewerUserID, view.Channel.ID, status)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(viewerUserID, channel)
|
|
r.pushChannelStateToMembers(ctx, viewerUserID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsReadMessageContents(ctx context.Context, req *tg.ChannelsReadMessageContentsRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return false, notImplementedErr()
|
|
}
|
|
if req == nil {
|
|
return false, inputRequestInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
read, err := r.deps.Channels.ReadMessageContents(ctx, userID, domain.ReadChannelMessageContentsRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
IDs: req.ID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrMessageIDInvalid) {
|
|
return false, messageIDInvalidErr()
|
|
}
|
|
return false, channelInvalidErr(err)
|
|
}
|
|
if ids := readChannelMessageContentIDs(read.Messages); len(ids) > 0 {
|
|
r.pushUserUpdates(ctx, userID, &tg.Updates{
|
|
Updates: []tg.UpdateClass{&tg.UpdateChannelReadMessagesContents{
|
|
ChannelID: read.Channel.ID,
|
|
Messages: ids,
|
|
}},
|
|
Users: []tg.UserClass{},
|
|
Chats: []tg.ChatClass{tgChannelChat(userID, read.Channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
})
|
|
}
|
|
if len(read.ClearedUnreadReactionMessageIDs) > 0 {
|
|
r.pushUserUpdates(ctx, userID, r.channelMessagesReactionsUpdates(ctx, userID, domain.ChannelMessageReactionsResult{
|
|
Channel: read.Channel,
|
|
Messages: read.Messages,
|
|
}, read.ClearedUnreadReactionMessageIDs))
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func readChannelMessageContentIDs(messages []domain.ChannelMessage) []int {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]int, 0, len(messages))
|
|
seen := make(map[int]struct{}, len(messages))
|
|
for _, msg := range messages {
|
|
if msg.ID <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[msg.ID]; ok {
|
|
continue
|
|
}
|
|
seen[msg.ID] = struct{}{}
|
|
ids = append(ids, msg.ID)
|
|
}
|
|
sort.Ints(ids)
|
|
return ids
|
|
}
|
|
|
|
func (r *Router) onChannelsReportSpam(ctx context.Context, req *tg.ChannelsReportSpamRequest) (bool, error) {
|
|
if len(req.ID) > maxChannelReportMessageIDs {
|
|
return false, limitInvalidErr()
|
|
}
|
|
for _, id := range req.ID {
|
|
if id <= 0 || id > domain.MaxMessageBoxID {
|
|
return false, messageIDInvalidErr()
|
|
}
|
|
}
|
|
if _, _, err := r.channelView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
if peer, ok := r.domainPeerFromInputPeer(0, req.Participant); !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 {
|
|
return false, peerIDInvalidErr()
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetLeftChannels(ctx context.Context, offset int) (tg.MessagesChatsClass, error) {
|
|
if offset < 0 || offset > domain.MaxLeftChannelsOffset {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesChats{Chats: []tg.ChatClass{}}, nil
|
|
}
|
|
list, err := r.deps.Channels.LeftChannels(ctx, userID, offset, domain.MaxLeftChannelsLimit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
chats := make([]tg.ChatClass, 0, len(list.Channels))
|
|
for _, item := range list.Channels {
|
|
chats = append(chats, tgChannelChat(userID, item.Channel, &item.Self))
|
|
}
|
|
if len(chats) == 0 && list.Count > 0 {
|
|
return &tg.MessagesChatsSlice{Count: list.Count, Chats: chats}, nil
|
|
}
|
|
if offset+len(chats) < list.Count {
|
|
return &tg.MessagesChatsSlice{Count: list.Count, Chats: chats}, nil
|
|
}
|
|
return &tg.MessagesChats{Chats: chats}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetInactiveChannels(ctx context.Context) (*tg.MessagesInactiveChats, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesInactiveChats{Dates: []int{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil
|
|
}
|
|
list, err := r.deps.Channels.InactiveChannels(ctx, userID, domain.MaxInactiveChannelsLimit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
dates := make([]int, 0, len(list.Channels))
|
|
chats := make([]tg.ChatClass, 0, len(list.Channels))
|
|
for i, channel := range list.Channels {
|
|
date := channel.Date
|
|
if i < len(list.Dialogs) && list.Dialogs[i].TopMessageDate > 0 {
|
|
date = list.Dialogs[i].TopMessageDate
|
|
}
|
|
dates = append(dates, date)
|
|
chats = append(chats, tgChannelChat(userID, channel, nil))
|
|
}
|
|
return &tg.MessagesInactiveChats{Dates: dates, Chats: chats, Users: []tg.UserClass{}}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetGroupsForDiscussion(ctx context.Context) (tg.MessagesChatsClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesChats{Chats: []tg.ChatClass{}}, nil
|
|
}
|
|
channels, err := r.deps.Channels.DiscussionGroups(ctx, userID, domain.MaxDiscussionGroupsLimit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
return &tg.MessagesChats{Chats: tgChannels(userID, channels)}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSetDiscussionGroup(ctx context.Context, req *tg.ChannelsSetDiscussionGroupRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return false, notImplementedErr()
|
|
}
|
|
if req == nil {
|
|
return false, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
broadcastID, err := r.optionalChannelIDFromInput(ctx, userID, req.Broadcast)
|
|
if err != nil {
|
|
return false, channelDiscussionErr(err)
|
|
}
|
|
groupID, err := r.optionalChannelIDFromInput(ctx, userID, req.Group)
|
|
if err != nil {
|
|
return false, channelDiscussionErr(err)
|
|
}
|
|
res, err := r.deps.Channels.SetDiscussionGroup(ctx, userID, broadcastID, groupID)
|
|
if err != nil {
|
|
return false, channelDiscussionErr(err)
|
|
}
|
|
for _, channel := range res.Channels {
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsEditLocation(ctx context.Context, req *tg.ChannelsEditLocationRequest) (bool, error) {
|
|
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsConvertToGigagroup(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
|
userID, view, err := r.channelChangeInfoView(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.channelStateUpdates(userID, view.Channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsDeleteParticipantHistory(ctx context.Context, req *tg.ChannelsDeleteParticipantHistoryRequest) (*tg.MessagesAffectedHistory, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
peer, ok := r.domainPeerFromInputPeer(0, req.Participant)
|
|
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.DeleteParticipantHistory(ctx, userID, domain.DeleteChannelParticipantHistoryRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
ParticipantUserID: peer.ID,
|
|
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.Channel.Pts, PtsCount: res.Event.PtsCount, Offset: res.Offset}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleJoinToSend(ctx context.Context, req *tg.ChannelsToggleJoinToSendRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetJoinToSend(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleJoinRequest(ctx context.Context, req *tg.ChannelsToggleJoinRequestRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetJoinRequest(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleForum(ctx context.Context, req *tg.ChannelsToggleForumRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetForum(ctx, userID, channelID, req.Enabled, req.Tabs)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleAntiSpam(ctx context.Context, req *tg.ChannelsToggleAntiSpamRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetAntiSpam(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsReportAntiSpamFalsePositive(ctx context.Context, req *tg.ChannelsReportAntiSpamFalsePositiveRequest) (bool, error) {
|
|
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
|
|
return false, messageIDInvalidErr()
|
|
}
|
|
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleParticipantsHidden(ctx context.Context, req *tg.ChannelsToggleParticipantsHiddenRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetParticipantsHidden(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleViewForumAsMessages(ctx context.Context, req *tg.ChannelsToggleViewForumAsMessagesRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
changed, err := r.deps.Channels.SetViewForumAsMessages(ctx, userID, channelID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
if !changed {
|
|
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
|
|
}
|
|
event := domain.UpdateEvent{
|
|
Type: domain.UpdateEventChannelViewForum,
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
|
Bool: req.Enabled,
|
|
PtsCount: 1,
|
|
Date: int(r.clock.Now().Unix()),
|
|
}
|
|
if r.deps.Updates != nil {
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
sessionID, _ := SessionIDFrom(ctx)
|
|
event, _, err = r.deps.Updates.RecordChannelViewForumAsMessages(ctx, authKeyID, userID, channelID, req.Enabled, sessionID)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
}
|
|
out := tgUpdateForOutboxEvent(event)
|
|
if out == nil {
|
|
out = tgEmptyUpdates(event.Date)
|
|
}
|
|
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, out)
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetChannelRecommendations(ctx context.Context, req *tg.ChannelsGetChannelRecommendationsRequest) (tg.MessagesChatsClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesChats{Chats: []tg.ChatClass{}}, nil
|
|
}
|
|
sourceChannelID := int64(0)
|
|
if req != nil {
|
|
if input, ok := req.GetChannel(); ok {
|
|
source, err := r.publicRecommendationSourceChannel(ctx, userID, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceChannelID = source
|
|
}
|
|
}
|
|
res, err := r.deps.Channels.ChannelRecommendations(ctx, userID, domain.ChannelRecommendationsRequest{
|
|
UserID: userID,
|
|
SourceChannelID: sourceChannelID,
|
|
Limit: domain.DefaultChannelRecommendationsLimit,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
chats := tgChannels(userID, res.Channels)
|
|
if res.Count > len(chats) {
|
|
return &tg.MessagesChatsSlice{Count: res.Count, Chats: chats}, nil
|
|
}
|
|
return &tg.MessagesChats{Chats: chats}, nil
|
|
}
|
|
|
|
func (r *Router) publicRecommendationSourceChannel(ctx context.Context, userID int64, input tg.InputChannelClass) (int64, error) {
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok {
|
|
return 0, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
channel, err := r.deps.Channels.GetJoinableChannel(ctx, userID, ref.ID)
|
|
if err != nil {
|
|
return 0, channelInvalidErr(err)
|
|
}
|
|
if !inputChannelAccessHashMatches(ref, channel) {
|
|
return 0, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
if channel.Deleted || !channel.Broadcast || channel.Megagroup || channel.Username == "" {
|
|
return 0, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
return channel.ID, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSetBoostsToUnblockRestrictions(ctx context.Context, req *tg.ChannelsSetBoostsToUnblockRestrictionsRequest) (tg.UpdatesClass, error) {
|
|
if req == nil {
|
|
return nil, inputRequestInvalidErr()
|
|
}
|
|
if req.Boosts < 0 || req.Boosts > maxChannelBoostsToUnblockRestrictions {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
return r.channelStateCompatUpdate(ctx, req.Channel)
|
|
}
|
|
|
|
func (r *Router) onChannelsRestrictSponsoredMessages(ctx context.Context, req *tg.ChannelsRestrictSponsoredMessagesRequest) (tg.UpdatesClass, error) {
|
|
if req == nil {
|
|
return nil, inputRequestInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetRestrictedSponsored(ctx, userID, view.Channel.ID, req.Restricted)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSearchPosts(ctx context.Context, req *tg.ChannelsSearchPostsRequest) (tg.MessagesMessagesClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if err := validateChannelSearchPostsRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
offsetChannelID, err := r.searchPostsOffsetChannelID(ctx, userID, req.OffsetPeer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return &tg.MessagesMessages{Messages: []tg.MessageClass{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil
|
|
}
|
|
hashtag, query := channelSearchPostsTerms(req)
|
|
history, err := r.deps.Channels.SearchPosts(ctx, userID, domain.ChannelSearchPostsRequest{
|
|
Hashtag: hashtag,
|
|
Query: query,
|
|
OffsetRate: req.OffsetRate,
|
|
OffsetChannelID: offsetChannelID,
|
|
OffsetID: req.OffsetID,
|
|
Limit: req.Limit,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
history = r.enrichChannelHistory(ctx, userID, history)
|
|
return tgChannelSearchPostsMessages(userID, history), nil
|
|
}
|
|
|
|
func validateChannelSearchPostsRequest(req *tg.ChannelsSearchPostsRequest) error {
|
|
if req == nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if req.Limit < 0 || req.Limit > maxChannelSearchPostsLimit {
|
|
return limitInvalidErr()
|
|
}
|
|
if req.OffsetRate < 0 {
|
|
return limitInvalidErr()
|
|
}
|
|
if req.OffsetID < 0 || req.OffsetID > domain.MaxMessageBoxID {
|
|
return messageIDInvalidErr()
|
|
}
|
|
if req.AllowPaidStars < 0 {
|
|
return limitInvalidErr()
|
|
}
|
|
hashtag, hasHashtag, query, hasQuery := channelSearchPostsTermsWithFlags(req)
|
|
if hasHashtag == hasQuery {
|
|
return searchQueryEmptyErr()
|
|
}
|
|
if hasHashtag {
|
|
if strings.TrimSpace(hashtag) == "" {
|
|
return searchQueryEmptyErr()
|
|
}
|
|
if strings.Contains(hashtag, "#") || utf8.RuneCountInString(hashtag) > maxChannelSearchPostsQuery {
|
|
return limitInvalidErr()
|
|
}
|
|
}
|
|
if hasQuery {
|
|
if strings.TrimSpace(query) == "" {
|
|
return searchQueryEmptyErr()
|
|
}
|
|
if utf8.RuneCountInString(query) > maxChannelSearchPostsQuery {
|
|
return limitInvalidErr()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func channelSearchPostsTerms(req *tg.ChannelsSearchPostsRequest) (hashtag, query string) {
|
|
hashtag, _, query, _ = channelSearchPostsTermsWithFlags(req)
|
|
return strings.TrimSpace(hashtag), strings.TrimSpace(query)
|
|
}
|
|
|
|
func channelSearchPostsTermsWithFlags(req *tg.ChannelsSearchPostsRequest) (hashtag string, hasHashtag bool, query string, hasQuery bool) {
|
|
hashtag, hasHashtag = req.GetHashtag()
|
|
if !hasHashtag && req.Hashtag != "" {
|
|
hashtag, hasHashtag = req.Hashtag, true
|
|
}
|
|
query, hasQuery = req.GetQuery()
|
|
if !hasQuery && req.Query != "" {
|
|
query, hasQuery = req.Query, true
|
|
}
|
|
return hashtag, hasHashtag, query, hasQuery
|
|
}
|
|
|
|
func (r *Router) validateSearchPostsOffsetPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) error {
|
|
_, err := r.searchPostsOffsetChannelID(ctx, userID, peer)
|
|
return err
|
|
}
|
|
|
|
func (r *Router) searchPostsOffsetChannelID(ctx context.Context, userID int64, peer tg.InputPeerClass) (int64, error) {
|
|
if peer == nil {
|
|
return 0, nil
|
|
}
|
|
if _, ok := peer.(*tg.InputPeerEmpty); ok {
|
|
return 0, nil
|
|
}
|
|
out, ok := r.domainPeerFromInputPeer(userID, peer)
|
|
if !ok || out.ID == 0 {
|
|
return 0, peerIDInvalidErr()
|
|
}
|
|
if out.Type != domain.PeerTypeChannel {
|
|
return 0, peerIDInvalidErr()
|
|
}
|
|
ref, ok := inputPeerChannelRef(peer)
|
|
if !ok || !ref.CheckAccessHash || r.deps.Channels == nil {
|
|
return out.ID, nil
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, out.ID)
|
|
if err == nil {
|
|
if !inputChannelAccessHashMatches(ref, view.Channel) {
|
|
return 0, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
return out.ID, nil
|
|
}
|
|
if !errors.Is(err, domain.ErrChannelPrivate) {
|
|
return 0, channelInvalidErr(err)
|
|
}
|
|
channel, joinErr := r.deps.Channels.GetJoinableChannel(ctx, userID, ref.ID)
|
|
if joinErr != nil || channel.Username == "" || !inputChannelAccessHashMatches(ref, channel) {
|
|
return 0, channelInvalidErr(err)
|
|
}
|
|
return out.ID, nil
|
|
}
|
|
|
|
func tgChannelSearchPostsMessages(viewerUserID int64, history domain.ChannelHistory) tg.MessagesMessagesClass {
|
|
messages := make([]tg.MessageClass, 0, len(history.Messages))
|
|
for _, msg := range history.Messages {
|
|
if item := tgChannelMessage(viewerUserID, msg); item != nil {
|
|
messages = append(messages, item)
|
|
}
|
|
}
|
|
chats := tgChannels(viewerUserID, history.Channels)
|
|
users := tgUsers(history.Users)
|
|
if history.Count > len(messages) {
|
|
out := &tg.MessagesMessagesSlice{
|
|
Count: history.Count,
|
|
Messages: messages,
|
|
Topics: []tg.ForumTopicClass{},
|
|
Chats: chats,
|
|
Users: users,
|
|
}
|
|
if len(history.Messages) > 0 {
|
|
out.SetNextRate(history.Messages[len(history.Messages)-1].Date)
|
|
}
|
|
out.SetSearchFlood(tg.SearchPostsFlood{QueryIsFree: true, TotalDaily: 100, Remains: 100, StarsAmount: 0})
|
|
return out
|
|
}
|
|
return &tg.MessagesMessages{Messages: messages, Topics: []tg.ForumTopicClass{}, Chats: chats, Users: users}
|
|
}
|
|
|
|
func (r *Router) onChannelsUpdatePaidMessagesPrice(ctx context.Context, req *tg.ChannelsUpdatePaidMessagesPriceRequest) (tg.UpdatesClass, error) {
|
|
if req == nil {
|
|
return nil, inputRequestInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateChannelPaidMessagesPriceRequest(req, view.Channel); err != nil {
|
|
return nil, err
|
|
}
|
|
stars := req.SendPaidMessagesStars
|
|
if stars < 0 {
|
|
stars = 0
|
|
}
|
|
channel, err := r.deps.Channels.SetPaidMessagesPrice(ctx, userID, view.Channel.ID, stars, req.BroadcastMessagesAllowed)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func validateChannelPaidMessagesPriceRequest(req *tg.ChannelsUpdatePaidMessagesPriceRequest, channel domain.Channel) error {
|
|
stars := req.SendPaidMessagesStars
|
|
if stars == -1 && channel.Broadcast && !req.BroadcastMessagesAllowed {
|
|
return nil
|
|
}
|
|
if stars < 0 || stars > maxChannelPaidMessageStars {
|
|
return starsAmountInvalidErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) onChannelsToggleAutotranslation(ctx context.Context, req *tg.ChannelsToggleAutotranslationRequest) (tg.UpdatesClass, error) {
|
|
if req == nil {
|
|
return nil, inputRequestInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, view, err := r.channelChangeInfoView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetAutotranslation(ctx, userID, view.Channel.ID, req.Enabled)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return r.channelStateUpdates(userID, channel), nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetMessageAuthor(ctx context.Context, req *tg.ChannelsGetMessageAuthorRequest) (tg.UserClass, error) {
|
|
userID, view, err := r.channelView(ctx, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
author, err := r.deps.Channels.GetMessageAuthor(ctx, userID, domain.GetChannelMessageAuthorRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
ID: req.ID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrMessageIDInvalid) {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
users := r.tgUsersForIDs(ctx, userID, []int64{author.SenderUserID})
|
|
if len(users) == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
return users[0], nil
|
|
}
|
|
|
|
func (r *Router) onChannelsCheckSearchPostsFlood(ctx context.Context, req *tg.ChannelsCheckSearchPostsFloodRequest) (*tg.SearchPostsFlood, error) {
|
|
if _, _, err := r.currentUserID(ctx); err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if err := validateChannelCheckSearchPostsFloodRequest(req); err != nil {
|
|
return nil, err
|
|
}
|
|
return &tg.SearchPostsFlood{QueryIsFree: true, TotalDaily: 100, Remains: 100, StarsAmount: 0}, nil
|
|
}
|
|
|
|
func validateChannelCheckSearchPostsFloodRequest(req *tg.ChannelsCheckSearchPostsFloodRequest) error {
|
|
if req == nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
query, hasQuery := req.GetQuery()
|
|
if !hasQuery && req.Query != "" {
|
|
query, hasQuery = req.Query, true
|
|
}
|
|
if !hasQuery || strings.TrimSpace(query) == "" {
|
|
return searchQueryEmptyErr()
|
|
}
|
|
if utf8.RuneCountInString(query) > maxChannelSearchPostsQuery {
|
|
return limitInvalidErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) onChannelsSetMainProfileTab(ctx context.Context, req *tg.ChannelsSetMainProfileTabRequest) (bool, error) {
|
|
if _, _, err := r.channelChangeInfoView(ctx, req.Channel); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetParticipants(ctx context.Context, req *tg.ChannelsGetParticipantsRequest) (tg.ChannelsChannelParticipantsClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.ChannelsChannelParticipants{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter := domainChannelParticipantsFilter(req.Filter)
|
|
if utf8.RuneCountInString(filter.Query) > domain.MaxChannelParticipantsQueryLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
list, err := r.deps.Channels.GetParticipants(ctx, userID, channelID, filter, req.Offset, req.Limit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
if req.Hash != 0 && list.Hash == req.Hash {
|
|
return &tg.ChannelsChannelParticipantsNotModified{}, nil
|
|
}
|
|
participants := make([]tg.ChannelParticipantClass, 0, len(list.Participants))
|
|
userIDs := make([]int64, 0, len(list.Participants))
|
|
for _, member := range list.Participants {
|
|
participants = append(participants, tgChannelParticipant(userID, member))
|
|
userIDs = append(userIDs, member.UserID)
|
|
}
|
|
users := r.tgUsers(list.Users)
|
|
if len(users) == 0 {
|
|
users = r.tgUsersForIDs(ctx, userID, userIDs)
|
|
}
|
|
r.log.Debug("channels.getParticipants result",
|
|
zap.Int64("channel_id", channelID),
|
|
zap.String("filter", string(filter.Kind)),
|
|
zap.Int("count", list.Count),
|
|
zap.Int("participants", len(participants)),
|
|
zap.Int("users", len(users)),
|
|
)
|
|
return &tg.ChannelsChannelParticipants{
|
|
Count: list.Count,
|
|
Participants: participants,
|
|
Chats: []tg.ChatClass{},
|
|
Users: users,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetParticipant(ctx context.Context, req *tg.ChannelsGetParticipantRequest) (*tg.ChannelsChannelParticipant, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.ChannelsChannelParticipant{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
peer, ok := r.domainPeerFromInputPeer(userID, req.Participant)
|
|
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
member, err := r.deps.Channels.GetParticipant(ctx, userID, channelID, peer.ID)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
return &tg.ChannelsChannelParticipant{
|
|
Participant: tgChannelParticipant(userID, member),
|
|
Users: r.tgUsersForIDs(ctx, userID, []int64{member.UserID}),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsInviteToChannel(ctx context.Context, req *tg.ChannelsInviteToChannelRequest) (*tg.MessagesInvitedUsers, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if len(req.Users) == 0 || len(req.Users) > domain.MaxChannelInviteUsers {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userIDs, err := r.userIDsFromInputUsers(ctx, userID, req.Users)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := r.deps.Channels.InviteToChannel(ctx, userID, channelID, userIDs, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return &tg.MessagesInvitedUsers{Updates: updates, MissingInvitees: []tg.MissingInvitee{}}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsJoinChannel(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok {
|
|
return nil, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
if ref.CheckAccessHash {
|
|
channel, err := r.deps.Channels.GetJoinableChannel(ctx, userID, ref.ID)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
if !inputChannelAccessHashMatches(ref, channel) {
|
|
return nil, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
}
|
|
res, err := r.deps.Channels.JoinChannel(ctx, userID, ref.ID, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrInviteRequestSent) && res.Channel.ID != 0 {
|
|
r.pushPendingJoinRequestsToAdmins(ctx, res.Channel)
|
|
}
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsLeaveChannel(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := r.deps.Channels.LeaveChannel(ctx, userID, channelID, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
r.removeOnlineChannelMemberships(res.Channel.ID, userID)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsEditTitle(ctx context.Context, req *tg.ChannelsEditTitleRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if !validChannelTitle(req.Title) {
|
|
return nil, channelInvalidErr(domain.ErrChannelTitleInvalid)
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := r.deps.Channels.EditTitle(ctx, userID, domain.EditChannelTitleRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Title: req.Title,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelTitleUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelTitleUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAdminRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
target, found, err := r.userFromInput(ctx, userID, req.UserID)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found || target.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.EditAdmin(ctx, userID, domain.EditChannelAdminRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
MemberID: target.ID,
|
|
AdminRights: domainChannelAdminRights(req.AdminRights),
|
|
Rank: req.Rank,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
if res.Participant.Status == domain.ChannelMemberActive {
|
|
r.addOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
|
} else {
|
|
r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID)
|
|
}
|
|
updates := r.channelParticipantUpdates(ctx, userID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelParticipantUpdates(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsEditBanned(ctx context.Context, req *tg.ChannelsEditBannedRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
participant, ok := r.domainPeerFromInputPeer(userID, req.Participant)
|
|
if !ok || participant.Type != domain.PeerTypeUser || participant.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.EditBanned(ctx, userID, domain.EditChannelBannedRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Participant: participant,
|
|
BannedRights: domainChannelBannedRights(req.BannedRights),
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelParticipantUpdates(ctx, userID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelParticipantUpdates(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPhotoRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
if req.Photo == nil {
|
|
return nil, photoInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
photo, err := r.resolveInputChatPhoto(ctx, userID, req.Photo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := r.deps.Channels.SetPhoto(ctx, userID, channelID, photo)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, channel)
|
|
r.pushChannelStateToMembers(ctx, userID, channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsDeleteChannel(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := r.deps.Channels.DeleteChannel(ctx, userID, domain.DeleteChannelRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelStateUpdates(userID, res.Channel)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelStateUpdates(viewerUserID, res.Channel)
|
|
})
|
|
r.removeOnlineChannelMembershipsForOnlineMembers(res.Channel.ID)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetAdminLog(ctx context.Context, req *tg.ChannelsGetAdminLogRequest) (*tg.ChannelsAdminLogResults, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.ChannelsAdminLogResults{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
adminIDs := []int64(nil)
|
|
if admins, ok := req.GetAdmins(); ok && len(admins) > 0 {
|
|
if len(admins) > domain.MaxChannelAdminLogAdmins {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
adminIDs, err = r.userIDsFromInputUsers(ctx, userID, admins)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
res, err := r.deps.Channels.ListAdminLog(ctx, userID, domain.ChannelAdminLogRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Query: req.Q,
|
|
AdminUserIDs: adminIDs,
|
|
MaxID: req.MaxID,
|
|
MinID: req.MinID,
|
|
Limit: req.Limit,
|
|
Filter: domainChannelAdminLogFilter(req),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
return &tg.ChannelsAdminLogResults{
|
|
Events: tgChannelAdminLogEvents(userID, res.Events),
|
|
Chats: []tg.ChatClass{tgChannelChat(userID, res.Channel, nil)},
|
|
Users: r.channelAdminLogUsers(ctx, userID, res.Events),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsReadHistory(ctx context.Context, req *tg.ChannelsReadHistoryRequest) (bool, error) {
|
|
if r.deps.Channels == nil {
|
|
return true, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
read, err := r.deps.Channels.ReadHistory(ctx, userID, domain.ReadChannelHistoryRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
MaxID: req.MaxID,
|
|
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 true, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsGetMessages(ctx context.Context, req *tg.ChannelsGetMessagesRequest) (tg.MessagesMessagesClass, error) {
|
|
if len(req.ID) > domain.MaxGetMessageIDs {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil || len(req.ID) == 0 {
|
|
return &tg.MessagesMessages{}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids := make([]int, 0, len(req.ID))
|
|
for _, input := range req.ID {
|
|
id, ok := inputMessageBoxID(input)
|
|
if !ok || id <= 0 || id > domain.MaxMessageBoxID {
|
|
continue
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
if len(ids) == 0 {
|
|
return &tg.MessagesMessages{}, nil
|
|
}
|
|
history, err := r.deps.Channels.GetMessages(ctx, userID, channelID, ids)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
history = r.enrichChannelHistory(ctx, userID, history)
|
|
byID := make(map[int]domain.ChannelMessage, len(history.Messages))
|
|
for _, msg := range history.Messages {
|
|
byID[msg.ID] = msg
|
|
}
|
|
messages := make([]tg.MessageClass, 0, len(ids))
|
|
for _, id := range ids {
|
|
if msg, ok := byID[id]; ok {
|
|
messages = append(messages, tgChannelMessage(userID, msg))
|
|
} else {
|
|
messages = append(messages, &tg.MessageEmpty{ID: id})
|
|
}
|
|
}
|
|
return &tg.MessagesMessages{
|
|
Messages: messages,
|
|
Chats: tgChannels(userID, []domain.Channel{history.Channel}),
|
|
Users: r.tgUsers(history.Users),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsDeleteMessages(ctx context.Context, req *tg.ChannelsDeleteMessagesRequest) (*tg.MessagesAffectedMessages, error) {
|
|
if len(req.ID) == 0 {
|
|
return &tg.MessagesAffectedMessages{PtsCount: 0}, nil
|
|
}
|
|
if len(req.ID) > domain.MaxDeleteMessageIDs {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
for _, id := range req.ID {
|
|
if id <= 0 || id > domain.MaxMessageBoxID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := r.deps.Channels.DeleteMessages(ctx, userID, domain.DeleteChannelMessagesRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
IDs: append([]int(nil), req.ID...),
|
|
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.MessagesAffectedMessages{Pts: res.Event.Pts, PtsCount: res.Event.PtsCount}, nil
|
|
}
|
|
return &tg.MessagesAffectedMessages{Pts: res.Channel.Pts, PtsCount: 0}, nil
|
|
}
|
|
|
|
func (r *Router) onChannelsDeleteHistory(ctx context.Context, req *tg.ChannelsDeleteHistoryRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.Updates{Date: int(r.clock.Now().Unix())}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.MaxID < 0 || req.MaxID > domain.MaxMessageBoxID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.DeleteHistory(ctx, userID, domain.DeleteChannelHistoryRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
MaxID: req.MaxID,
|
|
ForEveryone: req.GetForEveryone(),
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelDeleteErr(err)
|
|
}
|
|
if res.Event.Pts == 0 {
|
|
event := r.recordChannelAvailableMessages(ctx, userID, res.Channel.ID, res.AvailableMinID)
|
|
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, event.MaxID)
|
|
r.pushUserUpdates(ctx, userID, updates)
|
|
return updates, nil
|
|
}
|
|
updates := r.channelDeleteMessagesUpdates(userID, res.Channel, res.Event)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelDeleteMessagesUpdates(viewerUserID, res.Channel, res.Event)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesUpdatePinnedMessage(ctx context.Context, req *tg.MessagesUpdatePinnedMessageRequest) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
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 peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
if req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.UpdatePinnedMessage(ctx, userID, domain.UpdateChannelPinnedMessageRequest{
|
|
UserID: userID,
|
|
ChannelID: peer.ID,
|
|
MessageID: req.ID,
|
|
Pinned: !req.Unpin,
|
|
Silent: req.Silent,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
updates := r.channelPinnedUpdates(userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelPinnedUpdates(viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesUnpinAllMessages(ctx context.Context, req *tg.MessagesUnpinAllMessagesRequest) (*tg.MessagesAffectedHistory, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID <= 0 || topMsgID > domain.MaxMessageBoxID) {
|
|
return nil, messageIDInvalidErr()
|
|
}
|
|
if savedPeer, ok := req.GetSavedPeerID(); ok && savedPeer != nil {
|
|
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, savedPeer); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
|
return &tg.MessagesAffectedHistory{Offset: 0}, nil
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
|
|
if err != nil {
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
if topMsgID, ok := req.GetTopMsgID(); ok && topMsgID > 0 {
|
|
return &tg.MessagesAffectedHistory{Pts: view.Channel.Pts, Offset: 0}, nil
|
|
}
|
|
if _, ok := req.GetSavedPeerID(); ok {
|
|
return &tg.MessagesAffectedHistory{Pts: view.Channel.Pts, Offset: 0}, nil
|
|
}
|
|
if view.Channel.PinnedMessageID == 0 {
|
|
return &tg.MessagesAffectedHistory{Pts: view.Channel.Pts, Offset: 0}, nil
|
|
}
|
|
res, err := r.deps.Channels.UpdatePinnedMessage(ctx, userID, domain.UpdateChannelPinnedMessageRequest{
|
|
UserID: userID,
|
|
ChannelID: peer.ID,
|
|
MessageID: view.Channel.PinnedMessageID,
|
|
Pinned: false,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrChannelNotModified) {
|
|
return &tg.MessagesAffectedHistory{Pts: view.Channel.Pts, Offset: 0}, nil
|
|
}
|
|
return nil, channelAdminErr(err)
|
|
}
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelPinnedUpdates(viewerUserID, res)
|
|
})
|
|
return &tg.MessagesAffectedHistory{
|
|
Pts: res.Event.Pts,
|
|
PtsCount: res.Event.PtsCount,
|
|
Offset: 0,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesExportChatInvite(ctx context.Context, req *tg.MessagesExportChatInviteRequest) (tg.ExportedChatInviteClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
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 peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
if req.UsageLimit < 0 || req.ExpireDate < 0 || len(req.Title) > domain.MaxChannelInviteTitleLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.ExportInvite(ctx, userID, domain.ExportChannelInviteRequest{
|
|
UserID: userID,
|
|
ChannelID: peer.ID,
|
|
Title: req.Title,
|
|
RequestNeeded: req.RequestNeeded,
|
|
ExpireDate: req.ExpireDate,
|
|
UsageLimit: req.UsageLimit,
|
|
LegacyRevokePermanent: req.LegacyRevokePermanent,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
return tgExportedChannelInvite(res.Invite), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesCheckChatInvite(ctx context.Context, hash string) (tg.ChatInviteClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
res, err := r.deps.Channels.CheckInvite(ctx, userID, hash, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
if res.Already {
|
|
return &tg.ChatInviteAlready{Chat: tgChannelChat(userID, res.Channel, &res.Self)}, nil
|
|
}
|
|
return &tg.ChatInvite{
|
|
Channel: true,
|
|
Broadcast: res.Channel.Broadcast,
|
|
Megagroup: res.Channel.Megagroup,
|
|
Public: res.Channel.Username != "",
|
|
RequestNeeded: res.Invite.RequestNeeded,
|
|
Title: res.Channel.Title,
|
|
About: res.Channel.About,
|
|
Photo: &tg.PhotoEmpty{},
|
|
ParticipantsCount: res.Channel.ParticipantsCount,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesImportChatInvite(ctx context.Context, hash string) (tg.UpdatesClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return nil, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
res, err := r.deps.Channels.ImportInvite(ctx, userID, domain.ImportChannelInviteRequest{
|
|
UserID: userID,
|
|
Hash: hash,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrInviteRequestSent) && res.Channel.ID != 0 {
|
|
r.pushPendingJoinRequestsToAdmins(ctx, res.Channel)
|
|
}
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetExportedChatInvites(ctx context.Context, req *tg.MessagesGetExportedChatInvitesRequest) (*tg.MessagesExportedChatInvites, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Limit < 0 || req.Limit > maxChatInviteListLimit || len(req.OffsetLink) > maxChatInviteLinkLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
adminID := userID
|
|
if !inputUserIsEmpty(req.AdminID) {
|
|
admins, err := r.userIDsFromInputUsers(ctx, userID, []tg.InputUserClass{req.AdminID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(admins) > 0 {
|
|
adminID = admins[0]
|
|
}
|
|
}
|
|
offsetHash := ""
|
|
if req.OffsetLink != "" {
|
|
offsetHash, err = channelInviteHashFromLink(req.OffsetLink)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
list, err := r.deps.Channels.ListExportedInvites(ctx, userID, domain.ChannelInviteListRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
AdminUserID: adminID,
|
|
Revoked: req.Revoked,
|
|
OffsetDate: req.OffsetDate,
|
|
OffsetHash: offsetHash,
|
|
Limit: req.Limit,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
userIDs := []int64{adminID}
|
|
invites := make([]tg.ExportedChatInviteClass, 0, len(list.Invites))
|
|
for _, invite := range list.Invites {
|
|
invites = append(invites, tgExportedChannelInvite(invite))
|
|
userIDs = append(userIDs, invite.AdminUserID)
|
|
}
|
|
return &tg.MessagesExportedChatInvites{
|
|
Count: list.Count,
|
|
Invites: invites,
|
|
Users: r.tgUsersForIDs(ctx, userID, userIDs),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetExportedChatInvite(ctx context.Context, req *tg.MessagesGetExportedChatInviteRequest) (tg.MessagesExportedChatInviteClass, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash, err := channelInviteHashFromLink(req.Link)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
invite, err := r.deps.Channels.GetExportedInvite(ctx, userID, domain.GetChannelInviteRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
Hash: hash,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
return &tg.MessagesExportedChatInvite{
|
|
Invite: tgExportedChannelInvite(invite),
|
|
Users: r.tgUsersForIDs(ctx, userID, []int64{invite.AdminUserID}),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesEditExportedChatInvite(ctx context.Context, req *tg.MessagesEditExportedChatInviteRequest) (tg.MessagesExportedChatInviteClass, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash, err := channelInviteHashFromLink(req.Link)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.ExpireDate < 0 || req.UsageLimit < 0 || len(req.Title) > domain.MaxChannelInviteTitleLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
expireDate, hasExpireDate := req.GetExpireDate()
|
|
usageLimit, hasUsageLimit := req.GetUsageLimit()
|
|
requestNeeded, hasRequestNeeded := req.GetRequestNeeded()
|
|
title, hasTitle := req.GetTitle()
|
|
edited, err := r.deps.Channels.EditExportedInvite(ctx, userID, domain.EditChannelInviteRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
Hash: hash,
|
|
Revoked: req.Revoked,
|
|
HasExpireDate: hasExpireDate,
|
|
ExpireDate: expireDate,
|
|
HasUsageLimit: hasUsageLimit,
|
|
UsageLimit: usageLimit,
|
|
HasRequestNeeded: hasRequestNeeded,
|
|
RequestNeeded: requestNeeded,
|
|
HasTitle: hasTitle,
|
|
Title: title,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
users := r.tgUsersForIDs(ctx, userID, []int64{edited.Invite.AdminUserID})
|
|
if edited.NewInvite != nil {
|
|
return &tg.MessagesExportedChatInviteReplaced{
|
|
Invite: tgExportedChannelInvite(edited.Invite),
|
|
NewInvite: tgExportedChannelInvite(*edited.NewInvite),
|
|
Users: users,
|
|
}, nil
|
|
}
|
|
return &tg.MessagesExportedChatInvite{Invite: tgExportedChannelInvite(edited.Invite), Users: users}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesDeleteRevokedExportedChatInvites(ctx context.Context, req *tg.MessagesDeleteRevokedExportedChatInvitesRequest) (bool, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
adminID := userID
|
|
if !inputUserIsEmpty(req.AdminID) {
|
|
admins, err := r.userIDsFromInputUsers(ctx, userID, []tg.InputUserClass{req.AdminID})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if len(admins) > 0 {
|
|
adminID = admins[0]
|
|
}
|
|
}
|
|
if err := r.deps.Channels.DeleteRevokedExportedInvites(ctx, userID, domain.DeleteRevokedChannelInvitesRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
AdminUserID: adminID,
|
|
Limit: domain.MaxChannelHideJoinRequests,
|
|
}); err != nil {
|
|
return false, channelInviteErr(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesDeleteExportedChatInvite(ctx context.Context, req *tg.MessagesDeleteExportedChatInviteRequest) (bool, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
hash, err := channelInviteHashFromLink(req.Link)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := r.deps.Channels.DeleteExportedInvite(ctx, userID, domain.DeleteChannelInviteRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
Hash: hash,
|
|
}); err != nil {
|
|
return false, channelInviteErr(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetAdminsWithInvites(ctx context.Context, peer tg.InputPeerClass) (*tg.MessagesChatAdminsWithInvites, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts, err := r.deps.Channels.ListAdminsWithInvites(ctx, userID, view.Channel.ID)
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
admins := make([]tg.ChatAdminWithInvites, 0, len(counts))
|
|
userIDs := make([]int64, 0, len(counts))
|
|
for _, count := range counts {
|
|
admins = append(admins, tg.ChatAdminWithInvites{
|
|
AdminID: count.AdminUserID,
|
|
InvitesCount: count.InvitesCount,
|
|
RevokedInvitesCount: count.RevokedInvitesCount,
|
|
})
|
|
userIDs = append(userIDs, count.AdminUserID)
|
|
}
|
|
return &tg.MessagesChatAdminsWithInvites{
|
|
Admins: admins,
|
|
Users: r.tgUsersForIDs(ctx, userID, userIDs),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetChatInviteImporters(ctx context.Context, req *tg.MessagesGetChatInviteImportersRequest) (*tg.MessagesChatInviteImporters, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
link, hasLink := req.GetLink()
|
|
query, hasQuery := req.GetQ()
|
|
if hasLink && hasQuery && strings.TrimSpace(link) != "" && strings.TrimSpace(query) != "" {
|
|
return nil, tgerr400("SEARCH_WITH_LINK_NOT_SUPPORTED")
|
|
}
|
|
if req.Limit < 0 || req.Limit > maxChatInviteListLimit || len(link) > maxChatInviteLinkLength || len(query) > maxChatInviteSearchLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
hash := ""
|
|
if strings.TrimSpace(link) != "" {
|
|
hash, err = channelInviteHashFromLink(link)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
offsetUserID := int64(0)
|
|
if !inputUserIsEmpty(req.OffsetUser) {
|
|
ids, err := r.userIDsFromInputUsers(ctx, userID, []tg.InputUserClass{req.OffsetUser})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ids) > 0 {
|
|
offsetUserID = ids[0]
|
|
}
|
|
}
|
|
list, err := r.deps.Channels.ListInviteImporters(ctx, userID, domain.ChannelInviteImportersRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
Hash: hash,
|
|
Requested: req.Requested,
|
|
Query: query,
|
|
OffsetDate: req.OffsetDate,
|
|
OffsetUserID: offsetUserID,
|
|
Limit: req.Limit,
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
importers := make([]tg.ChatInviteImporter, 0, len(list.Importers))
|
|
userIDs := make([]int64, 0, len(list.Importers))
|
|
for _, importer := range list.Importers {
|
|
tgImporter := tg.ChatInviteImporter{
|
|
UserID: importer.UserID,
|
|
Date: importer.Date,
|
|
}
|
|
if importer.Requested {
|
|
tgImporter.SetRequested(true)
|
|
}
|
|
if importer.ApprovedBy != 0 {
|
|
tgImporter.SetApprovedBy(importer.ApprovedBy)
|
|
userIDs = append(userIDs, importer.ApprovedBy)
|
|
}
|
|
importers = append(importers, tgImporter)
|
|
userIDs = append(userIDs, importer.UserID)
|
|
}
|
|
return &tg.MessagesChatInviteImporters{
|
|
Count: list.Count,
|
|
Importers: importers,
|
|
Users: r.tgUsersForIDs(ctx, userID, userIDs),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesHideChatJoinRequest(ctx context.Context, req *tg.MessagesHideChatJoinRequestRequest) (tg.UpdatesClass, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targets, err := r.userIDsFromInputUsers(ctx, userID, []tg.InputUserClass{req.UserID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(targets) == 0 {
|
|
return nil, userIDInvalidErr()
|
|
}
|
|
res, err := r.deps.Channels.HideChatJoinRequest(ctx, userID, domain.HideChannelJoinRequestRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
TargetUserID: targets[0],
|
|
Approved: req.Approved,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
r.pushPendingJoinRequestsToAdmins(ctx, res.Channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesHideAllChatJoinRequests(ctx context.Context, req *tg.MessagesHideAllChatJoinRequestsRequest) (tg.UpdatesClass, error) {
|
|
userID, view, err := r.inviteManagementChannelView(ctx, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
link, hasLink := req.GetLink()
|
|
if hasLink && len(link) > maxChatInviteLinkLength {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
hash := ""
|
|
if hasLink && strings.TrimSpace(link) != "" {
|
|
hash, err = channelInviteHashFromLink(link)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
res, err := r.deps.Channels.HideAllChatJoinRequests(ctx, userID, domain.HideChannelJoinRequestsRequest{
|
|
UserID: userID,
|
|
ChannelID: view.Channel.ID,
|
|
Hash: hash,
|
|
Approved: req.Approved,
|
|
Limit: domain.MaxChannelHideJoinRequests,
|
|
Date: int(r.clock.Now().Unix()),
|
|
})
|
|
if err != nil {
|
|
return nil, channelInviteErr(err)
|
|
}
|
|
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
|
|
updates := r.channelOperationUpdates(ctx, userID, res)
|
|
r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel)
|
|
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
})
|
|
r.pushPendingJoinRequestsToAdmins(ctx, res.Channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onUpdatesGetChannelDifference(ctx context.Context, req *tg.UpdatesGetChannelDifferenceRequest) (tg.UpdatesChannelDifferenceClass, error) {
|
|
if r.deps.Channels == nil {
|
|
return &tg.UpdatesChannelDifferenceEmpty{Final: true, Pts: req.Pts, Timeout: 30}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
channelID, err := r.channelIDFromInput(ctx, userID, req.Channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.trackChannelInterest(ctx, userID, channelID)
|
|
diff, err := r.deps.Channels.GetDifference(ctx, userID, domain.ChannelDifferenceRequest{
|
|
UserID: userID,
|
|
ChannelID: channelID,
|
|
Pts: req.Pts,
|
|
Limit: req.Limit,
|
|
Force: req.Force,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrPersistentTimestamp) {
|
|
return nil, persistentTimestampInvalidErr()
|
|
}
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
diff = r.enrichChannelDifference(ctx, userID, diff)
|
|
return tgChannelDifference(userID, diff), nil
|
|
}
|
|
|
|
func (r *Router) channelOperationUpdates(ctx context.Context, viewerUserID int64, res domain.CreateChannelResult) *tg.Updates {
|
|
users := make([]int64, 0, len(res.Members)+1)
|
|
users = append(users, res.Channel.CreatorUserID)
|
|
for _, member := range res.Members {
|
|
users = append(users, member.UserID)
|
|
}
|
|
updates := make([]tg.UpdateClass, 0, 2)
|
|
if res.Event.Pts != 0 {
|
|
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
}
|
|
if res.Channel.ID != 0 {
|
|
updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID})
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, users),
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, channelMemberForUser(res.Members, viewerUserID))},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) tdesktopCreateChatUpdates(ctx context.Context, viewerUserID int64, res domain.CreateChannelResult) *tg.Updates {
|
|
updates := r.channelOperationUpdates(ctx, viewerUserID, res)
|
|
if updates == nil {
|
|
return updates
|
|
}
|
|
self := channelMemberForUser(res.Members, viewerUserID)
|
|
legacy := tgMigratedLegacyChat(viewerUserID, res.Channel, self)
|
|
if legacy == nil {
|
|
return updates
|
|
}
|
|
chats := make([]tg.ChatClass, 0, len(updates.Chats)+1)
|
|
chats = append(chats, legacy)
|
|
chats = append(chats, updates.Chats...)
|
|
updates.Chats = chats
|
|
return updates
|
|
}
|
|
|
|
func tdesktopCreateChatNeedsLegacyChat(ctx context.Context) bool {
|
|
ci, ok := ClientInfoFrom(ctx)
|
|
if !ok {
|
|
_, hasSession := SessionIDFrom(ctx)
|
|
return hasSession
|
|
}
|
|
device := strings.ToLower(ci.DeviceModel)
|
|
return ci.LangPack == "tdesktop" || strings.Contains(device, "desktop")
|
|
}
|
|
|
|
func channelMemberForUser(members []domain.ChannelMember, userID int64) *domain.ChannelMember {
|
|
for i := range members {
|
|
if members[i].UserID == userID {
|
|
return &members[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createChatInviteMemberIDs(ids []int64, selfUserID int64) []int64 {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]int64, 0, len(ids))
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
for _, id := range ids {
|
|
if id == 0 || id == selfUserID {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mergeChannelMembers(a, b []domain.ChannelMember) []domain.ChannelMember {
|
|
if len(a) == 0 {
|
|
return append([]domain.ChannelMember(nil), b...)
|
|
}
|
|
if len(b) == 0 {
|
|
return append([]domain.ChannelMember(nil), a...)
|
|
}
|
|
out := make([]domain.ChannelMember, 0, len(a)+len(b))
|
|
seen := make(map[int64]struct{}, len(a)+len(b))
|
|
appendOne := func(member domain.ChannelMember) {
|
|
if member.UserID == 0 {
|
|
return
|
|
}
|
|
if _, ok := seen[member.UserID]; ok {
|
|
return
|
|
}
|
|
seen[member.UserID] = struct{}{}
|
|
out = append(out, member)
|
|
}
|
|
for _, member := range a {
|
|
appendOne(member)
|
|
}
|
|
for _, member := range b {
|
|
appendOne(member)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Router) channelSelfForViewer(ctx context.Context, userID, channelID int64, members []domain.ChannelMember) *domain.ChannelMember {
|
|
if self := channelMemberForUser(members, userID); self != nil {
|
|
return self
|
|
}
|
|
if r.deps.Channels == nil || userID == 0 || channelID == 0 {
|
|
return nil
|
|
}
|
|
self, err := r.deps.Channels.GetParticipant(ctx, userID, channelID, userID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &self
|
|
}
|
|
|
|
func canViewChannelJoinRequests(member domain.ChannelMember) bool {
|
|
return member.Role == domain.ChannelRoleCreator ||
|
|
(member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo))
|
|
}
|
|
|
|
func (r *Router) applyPendingJoinRequestsToFullChannel(ctx context.Context, full *tg.ChannelFull, channelID int64, userIDs []int64) []int64 {
|
|
if r.deps.Channels == nil || full == nil || channelID == 0 {
|
|
return userIDs
|
|
}
|
|
pending, err := r.deps.Channels.PendingJoinRequests(ctx, channelID, domain.MaxChannelPendingJoinRecentRequesters)
|
|
if err != nil || pending.Count <= 0 {
|
|
return userIDs
|
|
}
|
|
full.SetRequestsPending(pending.Count)
|
|
full.SetRecentRequesters(pending.RecentRequesters)
|
|
return append(userIDs, pending.RecentRequesters...)
|
|
}
|
|
|
|
func (r *Router) pendingJoinRequestsUpdates(ctx context.Context, viewerUserID int64, channel domain.Channel) *tg.Updates {
|
|
if r.deps.Channels == nil || channel.ID == 0 {
|
|
return nil
|
|
}
|
|
pending, err := r.deps.Channels.PendingJoinRequests(ctx, channel.ID, domain.MaxChannelPendingJoinRecentRequesters)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &tg.Updates{
|
|
Updates: []tg.UpdateClass{&tg.UpdatePendingJoinRequests{
|
|
Peer: &tg.PeerChannel{ChannelID: channel.ID},
|
|
RequestsPending: pending.Count,
|
|
RecentRequesters: pending.RecentRequesters,
|
|
}},
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, pending.RecentRequesters),
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) appendPendingJoinRequestsUpdate(ctx context.Context, viewerUserID int64, updates *tg.Updates, channel domain.Channel) {
|
|
if updates == nil {
|
|
return
|
|
}
|
|
pending := r.pendingJoinRequestsUpdates(ctx, viewerUserID, channel)
|
|
if pending == nil {
|
|
return
|
|
}
|
|
updates.Updates = append(updates.Updates, pending.Updates...)
|
|
updates.Users = append(updates.Users, pending.Users...)
|
|
}
|
|
|
|
func (r *Router) pushPendingJoinRequestsToAdmins(ctx context.Context, channel domain.Channel) {
|
|
if r.deps.Channels == nil || r.deps.Sessions == nil || channel.ID == 0 {
|
|
return
|
|
}
|
|
adminIDs, err := r.deps.Channels.InviteAdminMemberIDs(ctx, channel.ID, domain.MaxChannelRealtimeFanout)
|
|
if err != nil || len(adminIDs) == 0 {
|
|
adminIDs = []int64{channel.CreatorUserID}
|
|
}
|
|
seen := make(map[int64]struct{}, len(adminIDs))
|
|
for _, adminID := range adminIDs {
|
|
if adminID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[adminID]; ok {
|
|
continue
|
|
}
|
|
seen[adminID] = struct{}{}
|
|
updates := r.pendingJoinRequestsUpdates(ctx, adminID, channel)
|
|
if updates == nil {
|
|
continue
|
|
}
|
|
r.pushUserUpdates(ctx, adminID, updates)
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelTitleUpdates(ctx context.Context, viewerUserID int64, res domain.EditChannelTitleResult) *tg.Updates {
|
|
updates := []tg.UpdateClass{&tg.UpdateChannel{ChannelID: res.Channel.ID}}
|
|
if res.Event.Pts != 0 {
|
|
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{res.Message.SenderUserID}),
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelParticipantUpdates(ctx context.Context, viewerUserID, actorUserID int64, channel domain.Channel, previous, participant domain.ChannelMember, date int) *tg.Updates {
|
|
update := &tg.UpdateChannelParticipant{
|
|
ChannelID: channel.ID,
|
|
Date: date,
|
|
ActorID: actorUserID,
|
|
UserID: participant.UserID,
|
|
}
|
|
if update.ActorID == 0 {
|
|
update.ActorID = viewerUserID
|
|
}
|
|
if previous.UserID != 0 {
|
|
update.SetPrevParticipant(tgChannelParticipantForUpdate(viewerUserID, previous))
|
|
}
|
|
if participant.UserID != 0 {
|
|
update.SetNewParticipant(tgChannelParticipantForUpdate(viewerUserID, participant))
|
|
}
|
|
return &tg.Updates{
|
|
Updates: []tg.UpdateClass{update, &tg.UpdateChannel{ChannelID: channel.ID}},
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{participant.UserID, participant.InviterUserID, previous.UserID, previous.InviterUserID, update.ActorID}),
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelStateUpdates(viewerUserID int64, channel domain.Channel) *tg.Updates {
|
|
return &tg.Updates{
|
|
Updates: []tg.UpdateClass{&tg.UpdateChannel{ChannelID: channel.ID}},
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelPinnedUpdates(viewerUserID int64, res domain.UpdateChannelPinnedMessageResult) *tg.Updates {
|
|
updates := []tg.UpdateClass(nil)
|
|
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID})
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelMessageUpdates(ctx context.Context, viewerUserID int64, res domain.SendChannelMessageResult, randomID int64) *tg.Updates {
|
|
randomIDs := []int64(nil)
|
|
includeMessageIDs := randomID != 0
|
|
if includeMessageIDs {
|
|
randomIDs = []int64{randomID}
|
|
}
|
|
return r.channelMessagesUpdates(ctx, viewerUserID, []domain.SendChannelMessageResult{res}, randomIDs, includeMessageIDs, nil)
|
|
}
|
|
|
|
func (r *Router) pushChannelDiscussionUpdate(ctx context.Context, originUserID int64, discussion *domain.SendChannelDiscussionResult) {
|
|
if discussion == nil || discussion.Channel.ID == 0 || discussion.Event.Pts == 0 {
|
|
return
|
|
}
|
|
res := domain.SendChannelMessageResult{
|
|
Channel: discussion.Channel,
|
|
Message: discussion.Message,
|
|
Event: discussion.Event,
|
|
Recipients: discussion.Recipients,
|
|
}
|
|
r.pushChannelUpdates(ctx, originUserID, discussion.Channel.ID, discussion.Recipients, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelMessageUpdates(ctx, viewerUserID, res, 0)
|
|
})
|
|
}
|
|
|
|
func (r *Router) channelMessagesUpdates(ctx context.Context, viewerUserID int64, results []domain.SendChannelMessageResult, randomIDs []int64, includeMessageIDs bool, extraUserIDs []int64) *tg.Updates {
|
|
updates := make([]tg.UpdateClass, 0, len(results)*2)
|
|
userIDs := make([]int64, 0, len(results)+len(extraUserIDs))
|
|
userIDs = append(userIDs, extraUserIDs...)
|
|
extraChannelIDs := make([]int64, 0, len(results))
|
|
var channel domain.Channel
|
|
date := 0
|
|
for i, res := range results {
|
|
if res.Channel.ID != 0 {
|
|
channel = res.Channel
|
|
}
|
|
if includeMessageIDs && res.Message.ID != 0 && i < len(randomIDs) && randomIDs[i] != 0 {
|
|
updates = append(updates, &tg.UpdateMessageID{ID: res.Message.ID, RandomID: randomIDs[i]})
|
|
}
|
|
if res.Event.Pts != 0 {
|
|
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
}
|
|
if res.Message.SenderUserID != 0 {
|
|
userIDs = append(userIDs, res.Message.SenderUserID)
|
|
}
|
|
if res.Message.SendAs != nil {
|
|
switch res.Message.SendAs.Type {
|
|
case domain.PeerTypeUser:
|
|
userIDs = append(userIDs, res.Message.SendAs.ID)
|
|
case domain.PeerTypeChannel:
|
|
extraChannelIDs = append(extraChannelIDs, res.Message.SendAs.ID)
|
|
}
|
|
}
|
|
if res.Message.Forward != nil && res.Message.Forward.From.Type == domain.PeerTypeChannel {
|
|
extraChannelIDs = append(extraChannelIDs, res.Message.Forward.From.ID)
|
|
}
|
|
if res.Message.ReplyTo != nil && res.Message.ReplyTo.Peer.Type == domain.PeerTypeChannel {
|
|
extraChannelIDs = append(extraChannelIDs, res.Message.ReplyTo.Peer.ID)
|
|
}
|
|
if date == 0 {
|
|
date = res.Event.Date
|
|
}
|
|
if date == 0 {
|
|
date = res.Message.Date
|
|
}
|
|
}
|
|
chats := []tg.ChatClass(nil)
|
|
if channel.ID != 0 {
|
|
chats = []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)}
|
|
}
|
|
chats = append(chats, r.tgChannelsForIDs(ctx, viewerUserID, extraChannelIDs, channel.ID)...)
|
|
if date == 0 {
|
|
date = int(r.clock.Now().Unix())
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, userIDs),
|
|
Chats: chats,
|
|
Date: date,
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) tgChannelsForIDs(ctx context.Context, viewerUserID int64, ids []int64, skipIDs ...int64) []tg.ChatClass {
|
|
if r.deps.Channels == nil || len(ids) == 0 {
|
|
return nil
|
|
}
|
|
skip := make(map[int64]struct{}, len(skipIDs))
|
|
for _, id := range skipIDs {
|
|
if id != 0 {
|
|
skip[id] = struct{}{}
|
|
}
|
|
}
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
chats := make([]tg.ChatClass, 0, len(ids))
|
|
for _, id := range ids {
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
if _, ok := skip[id]; ok {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
view, err := r.deps.Channels.GetChannel(ctx, viewerUserID, id)
|
|
if err != nil || view.Channel.ID == 0 {
|
|
continue
|
|
}
|
|
chats = append(chats, tgChannelChat(viewerUserID, view.Channel, &view.Self))
|
|
}
|
|
return chats
|
|
}
|
|
|
|
func (r *Router) channelEditMessageUpdates(ctx context.Context, viewerUserID int64, res domain.EditChannelMessageResult) *tg.Updates {
|
|
updates := make([]tg.UpdateClass, 0, 1)
|
|
if res.Event.Pts != 0 {
|
|
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{res.Message.SenderUserID}),
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, res.Channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelDeleteMessagesUpdates(viewerUserID int64, channel domain.Channel, event domain.ChannelUpdateEvent) *tg.Updates {
|
|
updates := make([]tg.UpdateClass, 0, 1)
|
|
if update := tgChannelUpdate(viewerUserID, event); update != nil {
|
|
updates = append(updates, update)
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelAvailableMessagesUpdates(viewerUserID int64, channel domain.Channel, availableMinID int) *tg.Updates {
|
|
updates := make([]tg.UpdateClass, 0, 1)
|
|
if channel.ID != 0 && availableMinID > 0 {
|
|
updates = append(updates, &tg.UpdateChannelAvailableMessages{
|
|
ChannelID: channel.ID,
|
|
AvailableMinID: availableMinID,
|
|
})
|
|
}
|
|
return &tg.Updates{
|
|
Updates: updates,
|
|
Chats: []tg.ChatClass{tgChannelChat(viewerUserID, channel, nil)},
|
|
Date: int(r.clock.Now().Unix()),
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
type channelUpdatesBuilder func(viewerUserID int64) *tg.Updates
|
|
|
|
type channelFanoutScope int
|
|
|
|
const (
|
|
channelFanoutMembers channelFanoutScope = iota
|
|
channelFanoutViewers
|
|
channelFanoutExplicit
|
|
)
|
|
|
|
func (r *Router) pushChannelReadOutboxUpdates(ctx context.Context, channelID int64, updates []domain.ChannelReadOutboxUpdate) {
|
|
if r.deps.Sessions == nil || channelID == 0 || len(updates) == 0 {
|
|
return
|
|
}
|
|
seen := make(map[int64]int, len(updates))
|
|
for _, update := range updates {
|
|
if update.UserID == 0 || update.MaxID <= 0 {
|
|
continue
|
|
}
|
|
if seen[update.UserID] < update.MaxID {
|
|
seen[update.UserID] = update.MaxID
|
|
}
|
|
}
|
|
date := int(r.clock.Now().Unix())
|
|
for userID, maxID := range seen {
|
|
r.pushUserUpdates(ctx, userID, &tg.Updates{
|
|
Updates: []tg.UpdateClass{&tg.UpdateReadChannelOutbox{ChannelID: channelID, MaxID: maxID}},
|
|
Date: date,
|
|
Seq: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (r *Router) recordChannelAvailableMessages(ctx context.Context, userID, channelID int64, availableMinID int) domain.UpdateEvent {
|
|
event := domain.UpdateEvent{
|
|
UserID: userID,
|
|
Type: domain.UpdateEventChannelAvailable,
|
|
Date: int(r.clock.Now().Unix()),
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
|
MaxID: availableMinID,
|
|
PtsCount: 1,
|
|
}
|
|
if r.deps.Updates == nil || userID == 0 || channelID == 0 || availableMinID <= 0 {
|
|
return event
|
|
}
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
sessionID, _ := SessionIDFrom(ctx)
|
|
recorded, _, err := r.deps.Updates.RecordChannelAvailableMessages(ctx, authKeyID, userID, channelID, availableMinID, sessionID)
|
|
if err != nil {
|
|
return event
|
|
}
|
|
return recorded
|
|
}
|
|
|
|
func (r *Router) recordChannelReadInbox(ctx context.Context, userID int64, read domain.ReadChannelHistoryResult) (domain.UpdateEvent, error) {
|
|
if !read.Changed || read.ChannelID == 0 {
|
|
return domain.UpdateEvent{}, nil
|
|
}
|
|
date := int(r.clock.Now().Unix())
|
|
event := domain.UpdateEvent{
|
|
UserID: userID,
|
|
Type: domain.UpdateEventReadHistoryInbox,
|
|
Date: date,
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: read.ChannelID},
|
|
MaxID: read.MaxID,
|
|
StillUnreadCount: read.StillUnreadCount,
|
|
Pts: read.Pts,
|
|
PtsCount: 1,
|
|
}
|
|
recordedEvent := event
|
|
if r.deps.Updates != nil {
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
sessionID, _ := SessionIDFrom(ctx)
|
|
recorded, _, err := r.deps.Updates.RecordReadHistory(ctx, authKeyID, userID, domain.ReadHistoryResult{
|
|
OwnerUserID: userID,
|
|
Peer: event.Peer,
|
|
MaxID: read.MaxID,
|
|
StillUnreadCount: read.StillUnreadCount,
|
|
Changed: read.Changed,
|
|
}, sessionID)
|
|
if err != nil {
|
|
return domain.UpdateEvent{}, internalErr()
|
|
}
|
|
recordedEvent = recorded
|
|
}
|
|
r.pushReadHistoryEvent(ctx, userID, event)
|
|
return recordedEvent, nil
|
|
}
|
|
|
|
func (r *Router) pushChannelUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
|
|
r.pushChannelUpdatesWithScope(ctx, channelFanoutMembers, originUserID, channelID, recipients, build)
|
|
}
|
|
|
|
func (r *Router) pushChannelViewerUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
|
|
r.pushChannelUpdatesWithScope(ctx, channelFanoutViewers, originUserID, channelID, recipients, build)
|
|
}
|
|
|
|
func (r *Router) pushChannelExplicitUpdates(ctx context.Context, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
|
|
r.pushChannelUpdatesWithScope(ctx, channelFanoutExplicit, originUserID, channelID, recipients, build)
|
|
}
|
|
|
|
func (r *Router) pushChannelUpdatesWithScope(ctx context.Context, scope channelFanoutScope, originUserID, channelID int64, recipients []int64, build channelUpdatesBuilder) {
|
|
if r.deps.Sessions == nil || build == nil {
|
|
return
|
|
}
|
|
recipients = r.channelFanoutRecipients(ctx, scope, channelID, recipients)
|
|
seen := make(map[int64]struct{}, len(recipients))
|
|
pushed := false
|
|
for _, userID := range recipients {
|
|
if userID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[userID]; ok {
|
|
continue
|
|
}
|
|
seen[userID] = struct{}{}
|
|
updates := build(userID)
|
|
if updates == nil {
|
|
continue
|
|
}
|
|
r.pushUserUpdates(ctx, userID, updates)
|
|
pushed = true
|
|
}
|
|
if !pushed && originUserID != 0 {
|
|
updates := build(originUserID)
|
|
if updates == nil {
|
|
return
|
|
}
|
|
r.pushUserUpdates(ctx, originUserID, updates)
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelFanoutRecipients(ctx context.Context, scope channelFanoutScope, channelID int64, explicit []int64) []int64 {
|
|
if channelID == 0 || r.deps.Channels == nil || r.deps.Sessions == nil {
|
|
return uniqueRecipientIDs(explicit)
|
|
}
|
|
if scope == channelFanoutExplicit {
|
|
return uniqueRecipientIDs(explicit)
|
|
}
|
|
provider, ok := r.deps.Sessions.(OnlineUserProvider)
|
|
if !ok {
|
|
return uniqueRecipientIDs(explicit)
|
|
}
|
|
var online []int64
|
|
switch scope {
|
|
case channelFanoutMembers:
|
|
online = provider.OnlineChannelMemberUserIDs(channelID, 0)
|
|
case channelFanoutViewers:
|
|
online = provider.OnlineChannelUserIDs(channelID, 0)
|
|
}
|
|
if len(online) == 0 {
|
|
return uniqueRecipientIDs(explicit)
|
|
}
|
|
active, err := r.deps.Channels.FilterActiveMemberIDs(ctx, channelID, online)
|
|
if err != nil {
|
|
return uniqueRecipientIDs(explicit)
|
|
}
|
|
if len(active) == 0 && len(explicit) == 0 {
|
|
return nil
|
|
}
|
|
out := uniqueRecipientIDs(active)
|
|
seen := make(map[int64]struct{}, len(out)+len(explicit))
|
|
for _, userID := range active {
|
|
if userID == 0 {
|
|
continue
|
|
}
|
|
seen[userID] = struct{}{}
|
|
}
|
|
// Keep operation-specific recipients as a fallback: leave/kick/delete flows
|
|
// may need to notify a user who is no longer an active member after commit.
|
|
for _, userID := range explicit {
|
|
if userID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[userID]; ok {
|
|
continue
|
|
}
|
|
seen[userID] = struct{}{}
|
|
out = append(out, userID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func uniqueRecipientIDs(ids []int64) []int64 {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]int64, 0, len(ids))
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
for _, userID := range ids {
|
|
if userID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[userID]; ok {
|
|
continue
|
|
}
|
|
seen[userID] = struct{}{}
|
|
out = append(out, userID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Router) pushChannelStateToMembers(ctx context.Context, originUserID int64, channel domain.Channel) {
|
|
if r.deps.Channels == nil || channel.ID == 0 {
|
|
return
|
|
}
|
|
r.pushChannelUpdates(ctx, originUserID, channel.ID, []int64{originUserID}, func(viewerUserID int64) *tg.Updates {
|
|
return r.channelStateUpdates(viewerUserID, channel)
|
|
})
|
|
}
|
|
|
|
func (r *Router) tgUsersForIDs(ctx context.Context, currentUserID int64, ids []int64) []tg.UserClass {
|
|
if r.deps.Users == nil || len(ids) == 0 {
|
|
return nil
|
|
}
|
|
unique := make([]int64, 0, len(ids))
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
for _, id := range ids {
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
unique = append(unique, id)
|
|
}
|
|
users, err := r.deps.Users.ByIDs(ctx, currentUserID, unique)
|
|
if err != nil {
|
|
// 批量解析失败(通常是 DB 故障)不静默丢弃:批量语义下无部分结果,记日志便于排查。
|
|
// update 仍以空 users 列表推送,客户端用本地缓存或后续 getUser 补齐,不致命。
|
|
// 不降级逐个查询以免 DB 抖动时把一次失败放大成 N 次查询。
|
|
r.log.Warn("batch resolve users for channel update failed",
|
|
zap.Int("count", len(unique)), zap.Error(err))
|
|
return nil
|
|
}
|
|
byID := make(map[int64]domain.User, len(users))
|
|
for _, u := range users {
|
|
if u.ID != 0 {
|
|
byID[u.ID] = u
|
|
}
|
|
}
|
|
out := make([]tg.UserClass, 0, len(byID))
|
|
for _, id := range unique {
|
|
u, ok := byID[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if id == currentUserID {
|
|
out = append(out, r.tgSelfUser(u))
|
|
continue
|
|
}
|
|
out = append(out, r.tgUser(u))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tgExportedChannelInvite(invite domain.ChannelInvite) tg.ExportedChatInviteClass {
|
|
out := &tg.ChatInviteExported{
|
|
Revoked: invite.Revoked,
|
|
Permanent: invite.Permanent,
|
|
RequestNeeded: invite.RequestNeeded,
|
|
Link: "https://t.me/+" + invite.Hash,
|
|
AdminID: invite.AdminUserID,
|
|
Date: invite.Date,
|
|
}
|
|
if invite.Title != "" {
|
|
out.SetTitle(invite.Title)
|
|
}
|
|
if invite.ExpireDate > 0 {
|
|
out.SetExpireDate(invite.ExpireDate)
|
|
}
|
|
if invite.UsageLimit > 0 {
|
|
out.SetUsageLimit(invite.UsageLimit)
|
|
}
|
|
if invite.UsageCount > 0 {
|
|
out.SetUsage(invite.UsageCount)
|
|
}
|
|
if invite.RequestedCount > 0 {
|
|
out.SetRequested(invite.RequestedCount)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func chatInviteExportedStub(adminUserID int64, link, title string, revoked, requestNeeded bool, expireDate, usageLimit, usageCount, date int) tg.ExportedChatInviteClass {
|
|
out := &tg.ChatInviteExported{
|
|
Revoked: revoked,
|
|
RequestNeeded: requestNeeded,
|
|
Link: strings.TrimSpace(link),
|
|
AdminID: adminUserID,
|
|
Date: date,
|
|
}
|
|
if title != "" {
|
|
out.SetTitle(title)
|
|
}
|
|
if expireDate > 0 {
|
|
out.SetExpireDate(expireDate)
|
|
}
|
|
if usageLimit > 0 {
|
|
out.SetUsageLimit(usageLimit)
|
|
}
|
|
if usageCount > 0 {
|
|
out.SetUsage(usageCount)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Router) inviteManagementChannelView(ctx context.Context, peer tg.InputPeerClass) (int64, domain.ChannelView, error) {
|
|
ref, ok := inviteManagementChannelRef(peer)
|
|
if !ok {
|
|
return 0, domain.ChannelView{}, peerIDInvalidErr()
|
|
}
|
|
input := &tg.InputChannel{ChannelID: ref.ID}
|
|
if ref.CheckAccessHash {
|
|
input.AccessHash = ref.AccessHash
|
|
}
|
|
userID, view, err := r.channelView(ctx, input)
|
|
if err != nil {
|
|
return 0, domain.ChannelView{}, err
|
|
}
|
|
if view.Self.Role != domain.ChannelRoleCreator && view.Self.Role != domain.ChannelRoleAdmin {
|
|
return 0, domain.ChannelView{}, tgerr400("CHAT_ADMIN_REQUIRED")
|
|
}
|
|
return userID, view, nil
|
|
}
|
|
|
|
func inviteManagementChannelRef(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
|
|
case *tg.InputPeerChat:
|
|
return channelInputRef{ID: p.ChatID}, p.ChatID > 0
|
|
default:
|
|
return channelInputRef{}, false
|
|
}
|
|
}
|
|
|
|
func validateChatInviteLink(link string) error {
|
|
link = strings.TrimSpace(link)
|
|
if link == "" {
|
|
return tgerr400("INVITE_HASH_EMPTY")
|
|
}
|
|
if len(link) > maxChatInviteLinkLength {
|
|
return limitInvalidErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func channelInviteHashFromLink(link string) (string, error) {
|
|
if err := validateChatInviteLink(link); err != nil {
|
|
return "", err
|
|
}
|
|
link = strings.TrimSpace(link)
|
|
link = strings.TrimPrefix(link, "tg://join?invite=")
|
|
if strings.Contains(link, "://") {
|
|
if idx := strings.LastIndex(link, "/+"); idx >= 0 {
|
|
link = link[idx+2:]
|
|
} else if idx := strings.LastIndex(link, "/joinchat/"); idx >= 0 {
|
|
link = link[idx+10:]
|
|
} else if idx := strings.LastIndex(link, "/"); idx >= 0 {
|
|
link = link[idx+1:]
|
|
}
|
|
}
|
|
link = strings.TrimPrefix(link, "+")
|
|
link = strings.TrimSpace(link)
|
|
if link == "" {
|
|
return "", tgerr400("INVITE_HASH_EMPTY")
|
|
}
|
|
if len(link) > maxChatInviteLinkLength {
|
|
return "", limitInvalidErr()
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
func inputUserIsEmpty(input tg.InputUserClass) bool {
|
|
switch input.(type) {
|
|
case nil, *tg.InputUserEmpty:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (r *Router) userIDsFromInputUsers(ctx context.Context, currentUserID int64, inputs []tg.InputUserClass) ([]int64, error) {
|
|
out := make([]int64, 0, len(inputs))
|
|
seen := make(map[int64]struct{}, len(inputs))
|
|
for _, input := range inputs {
|
|
u, found, err := r.userFromInput(ctx, currentUserID, input)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found || u.ID == 0 {
|
|
return nil, peerIDInvalidErr()
|
|
}
|
|
if _, ok := seen[u.ID]; ok {
|
|
continue
|
|
}
|
|
seen[u.ID] = struct{}{}
|
|
out = append(out, u.ID)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func domainChannelAdminLogFilter(req *tg.ChannelsGetAdminLogRequest) domain.ChannelAdminLogFilter {
|
|
filter, ok := req.GetEventsFilter()
|
|
if !ok {
|
|
return domain.ChannelAdminLogFilter{}
|
|
}
|
|
return domain.ChannelAdminLogFilter{
|
|
Join: filter.GetJoin(),
|
|
Leave: filter.GetLeave(),
|
|
Invite: filter.GetInvite(),
|
|
Ban: filter.GetBan(),
|
|
Unban: filter.GetUnban(),
|
|
Kick: filter.GetKick(),
|
|
Unkick: filter.GetUnkick(),
|
|
Promote: filter.GetPromote(),
|
|
Demote: filter.GetDemote(),
|
|
Info: filter.GetInfo(),
|
|
Settings: filter.GetSettings(),
|
|
Pinned: filter.GetPinned(),
|
|
Edit: filter.GetEdit(),
|
|
Delete: filter.GetDelete(),
|
|
Send: filter.GetSend(),
|
|
Invites: filter.GetInvites(),
|
|
Forums: filter.GetForums(),
|
|
SubExtend: filter.GetSubExtend(),
|
|
EditRank: filter.GetEditRank(),
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelAdminLogUsers(ctx context.Context, currentUserID int64, events []domain.ChannelAdminLogEvent) []tg.UserClass {
|
|
if r.deps.Users == nil || len(events) == 0 {
|
|
return nil
|
|
}
|
|
ids := make(map[int64]struct{}, len(events))
|
|
add := func(id int64) {
|
|
if id != 0 {
|
|
ids[id] = struct{}{}
|
|
}
|
|
}
|
|
addMember := func(member *domain.ChannelMember) {
|
|
if member != nil {
|
|
add(member.UserID)
|
|
add(member.InviterUserID)
|
|
}
|
|
}
|
|
addMessage := func(msg *domain.ChannelMessage) {
|
|
if msg != nil {
|
|
add(msg.SenderUserID)
|
|
if msg.From.Type == domain.PeerTypeUser {
|
|
add(msg.From.ID)
|
|
}
|
|
}
|
|
}
|
|
for _, event := range events {
|
|
add(event.UserID)
|
|
addMember(event.PrevParticipant)
|
|
addMember(event.NewParticipant)
|
|
addMember(event.Participant)
|
|
addMessage(event.Message)
|
|
addMessage(event.PrevMessage)
|
|
addMessage(event.NewMessage)
|
|
}
|
|
out := make([]tg.UserClass, 0, len(ids))
|
|
userIDs := make([]int64, 0, len(ids))
|
|
for id := range ids {
|
|
userIDs = append(userIDs, id)
|
|
}
|
|
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
|
|
for _, id := range userIDs {
|
|
u, found, err := r.deps.Users.ByID(ctx, currentUserID, id)
|
|
if err == nil && found {
|
|
out = append(out, r.tgUser(u))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type channelInputRef struct {
|
|
ID int64
|
|
AccessHash int64
|
|
CheckAccessHash bool
|
|
}
|
|
|
|
func inputChannelRef(input tg.InputChannelClass) (channelInputRef, bool) {
|
|
switch channel := input.(type) {
|
|
case *tg.InputChannel:
|
|
return channelInputRef{
|
|
ID: channel.ChannelID,
|
|
AccessHash: channel.AccessHash,
|
|
CheckAccessHash: channel.AccessHash != 0,
|
|
}, channel.ChannelID > 0
|
|
case *tg.InputChannelFromMessage:
|
|
return channelInputRef{ID: channel.ChannelID}, channel.ChannelID > 0
|
|
default:
|
|
return channelInputRef{}, false
|
|
}
|
|
}
|
|
|
|
func inputChannelID(input tg.InputChannelClass) (int64, bool) {
|
|
ref, ok := inputChannelRef(input)
|
|
return ref.ID, ok
|
|
}
|
|
|
|
func inputChannelAccessHashMatches(ref channelInputRef, channel domain.Channel) bool {
|
|
return !ref.CheckAccessHash || ref.AccessHash == channel.AccessHash
|
|
}
|
|
|
|
func (r *Router) optionalChannelIDFromInput(ctx context.Context, userID int64, input tg.InputChannelClass) (int64, error) {
|
|
switch input.(type) {
|
|
case nil, *tg.InputChannelEmpty:
|
|
return 0, nil
|
|
default:
|
|
return r.channelIDFromInput(ctx, userID, input)
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelIDFromInput(ctx context.Context, userID int64, input tg.InputChannelClass) (int64, error) {
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok {
|
|
return 0, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
if !ref.CheckAccessHash || r.deps.Channels == nil {
|
|
return ref.ID, 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 validChannelTitle(title string) bool {
|
|
n := utf8.RuneCountInString(title)
|
|
return n > 0 && n <= maxChannelTitleLength
|
|
}
|
|
|
|
func (r *Router) channelView(ctx context.Context, input tg.InputChannelClass) (int64, domain.ChannelView, error) {
|
|
if r.deps.Channels == nil {
|
|
return 0, domain.ChannelView{}, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return 0, domain.ChannelView{}, internalErr()
|
|
}
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok {
|
|
return 0, domain.ChannelView{}, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
view, err := r.deps.Channels.GetChannel(ctx, userID, ref.ID)
|
|
if err != nil {
|
|
return 0, domain.ChannelView{}, channelInvalidErr(err)
|
|
}
|
|
if !inputChannelAccessHashMatches(ref, view.Channel) {
|
|
return 0, domain.ChannelView{}, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
return userID, view, nil
|
|
}
|
|
|
|
func (r *Router) channelChangeInfoView(ctx context.Context, input tg.InputChannelClass) (int64, domain.ChannelView, error) {
|
|
if r.deps.Channels == nil {
|
|
return 0, domain.ChannelView{}, notImplementedErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return 0, domain.ChannelView{}, internalErr()
|
|
}
|
|
ref, ok := inputChannelRef(input)
|
|
if !ok {
|
|
return 0, domain.ChannelView{}, channelInvalidErr(domain.ErrChannelInvalid)
|
|
}
|
|
view, err := r.deps.Channels.GetChannelForChangeInfo(ctx, userID, ref.ID)
|
|
if err != nil {
|
|
return 0, domain.ChannelView{}, channelAdminErr(err)
|
|
}
|
|
if !inputChannelAccessHashMatches(ref, view.Channel) {
|
|
return 0, domain.ChannelView{}, channelInvalidErr(domain.ErrChannelPrivate)
|
|
}
|
|
return userID, view, nil
|
|
}
|
|
|
|
func (r *Router) channelStateCompatUpdate(ctx context.Context, input tg.InputChannelClass) (tg.UpdatesClass, error) {
|
|
userID, view, err := r.channelChangeInfoView(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates := r.channelStateUpdates(userID, view.Channel)
|
|
r.pushChannelStateToMembers(ctx, userID, view.Channel)
|
|
return updates, nil
|
|
}
|
|
|
|
func tgEmptyUpdates(date int) *tg.Updates {
|
|
return &tg.Updates{
|
|
Updates: []tg.UpdateClass{},
|
|
Users: []tg.UserClass{},
|
|
Chats: []tg.ChatClass{},
|
|
Date: date,
|
|
Seq: 0,
|
|
}
|
|
}
|
|
|
|
func validChannelSlowModeSeconds(seconds int) bool {
|
|
return domain.ValidChannelSlowModeSeconds(seconds)
|
|
}
|
|
|
|
func validChannelManagementUsername(username string) bool {
|
|
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
|
|
if len(username) < 5 || len(username) > 32 {
|
|
return false
|
|
}
|
|
for i := 0; i < len(username); i++ {
|
|
c := username[i]
|
|
switch {
|
|
case i == 0 && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')):
|
|
case i > 0 && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'):
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func domainChannelReactionPolicy(req *tg.MessagesSetChatAvailableReactionsRequest) (domain.ChannelReactionPolicy, error) {
|
|
if req == nil || req.AvailableReactions == nil {
|
|
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
|
}
|
|
if req.ReactionsLimit < 0 || req.ReactionsLimit > domain.MaxChannelReactionItems {
|
|
return domain.ChannelReactionPolicy{}, limitInvalidErr()
|
|
}
|
|
policy := domain.ChannelReactionPolicy{
|
|
Limit: req.ReactionsLimit,
|
|
PaidEnabled: req.PaidEnabled,
|
|
}
|
|
switch reactions := req.AvailableReactions.(type) {
|
|
case *tg.ChatReactionsNone:
|
|
policy.Type = domain.ChannelReactionPolicyNone
|
|
case *tg.ChatReactionsAll:
|
|
policy.Type = domain.ChannelReactionPolicyAll
|
|
policy.AllowCustom = reactions.AllowCustom
|
|
case *tg.ChatReactionsSome:
|
|
if len(reactions.Reactions) > domain.MaxChannelReactionItems {
|
|
return domain.ChannelReactionPolicy{}, limitInvalidErr()
|
|
}
|
|
policy.Type = domain.ChannelReactionPolicySome
|
|
for _, reaction := range reactions.Reactions {
|
|
switch value := reaction.(type) {
|
|
case *tg.ReactionEmoji:
|
|
if strings.TrimSpace(value.Emoticon) == "" || utf8.RuneCountInString(value.Emoticon) > domain.MaxChannelReactionEmoticonLength {
|
|
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
|
}
|
|
policy.Emoticons = append(policy.Emoticons, value.Emoticon)
|
|
case *tg.ReactionCustomEmoji:
|
|
if value.DocumentID <= 0 {
|
|
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
|
}
|
|
policy.CustomEmojiIDs = append(policy.CustomEmojiIDs, value.DocumentID)
|
|
default:
|
|
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
|
}
|
|
}
|
|
default:
|
|
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func domainPeerColorFromChannelUpdate(req *tg.ChannelsUpdateColorRequest) domain.ChannelPeerColor {
|
|
if req == nil {
|
|
return domain.ChannelPeerColor{}
|
|
}
|
|
color, hasColor := req.GetColor()
|
|
backgroundEmojiID, hasBackground := req.GetBackgroundEmojiID()
|
|
out := domain.ChannelPeerColor{HasColor: hasColor, Color: color}
|
|
if hasBackground {
|
|
out.BackgroundEmojiID = backgroundEmojiID
|
|
}
|
|
return out
|
|
}
|
|
|
|
func domainChannelEmojiStatus(status tg.EmojiStatusClass) (domain.ChannelEmojiStatus, error) {
|
|
switch s := status.(type) {
|
|
case *tg.EmojiStatusEmpty:
|
|
return domain.ChannelEmojiStatus{}, nil
|
|
case *tg.EmojiStatus:
|
|
if s.DocumentID <= 0 {
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
}
|
|
until, _ := s.GetUntil()
|
|
if until < 0 {
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
}
|
|
return domain.ChannelEmojiStatus{DocumentID: s.DocumentID, Until: until}, nil
|
|
case *tg.EmojiStatusCollectible:
|
|
if s.DocumentID <= 0 {
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
}
|
|
until, _ := s.GetUntil()
|
|
if until < 0 {
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
}
|
|
return domain.ChannelEmojiStatus{DocumentID: s.DocumentID, Until: until}, nil
|
|
case *tg.InputEmojiStatusCollectible:
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
default:
|
|
return domain.ChannelEmojiStatus{}, tgerr400("EMOJI_STATUS_INVALID")
|
|
}
|
|
}
|
|
|
|
func domainChannelParticipantsFilter(filter tg.ChannelParticipantsFilterClass) domain.ChannelParticipantsFilter {
|
|
switch f := filter.(type) {
|
|
case *tg.ChannelParticipantsAdmins:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsAdmins}
|
|
case *tg.ChannelParticipantsKicked:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsKicked, Query: f.Q}
|
|
case *tg.ChannelParticipantsBanned:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsBanned, Query: f.Q}
|
|
case *tg.ChannelParticipantsSearch:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsSearch, Query: f.Q}
|
|
case *tg.ChannelParticipantsBots:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsBots}
|
|
case *tg.ChannelParticipantsContacts:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsContacts, Query: f.Q}
|
|
case *tg.ChannelParticipantsMentions:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsMentions, Query: f.Q}
|
|
default:
|
|
return domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsRecent}
|
|
}
|
|
}
|
|
|
|
func legacyBasicGroupAdminRights() tg.ChatAdminRights {
|
|
return tg.ChatAdminRights{
|
|
ChangeInfo: true,
|
|
DeleteMessages: true,
|
|
BanUsers: true,
|
|
InviteUsers: true,
|
|
PinMessages: true,
|
|
Other: true,
|
|
}
|
|
}
|
|
|
|
func channelIDFromLegacyInputPeer(userID int64, peer tg.InputPeerClass) (int64, bool) {
|
|
switch p := peer.(type) {
|
|
case *tg.InputPeerChannel:
|
|
return p.ChannelID, p.ChannelID > 0
|
|
case *tg.InputPeerChat:
|
|
return p.ChatID, p.ChatID > 0
|
|
case *tg.InputPeerChannelFromMessage:
|
|
return p.ChannelID, p.ChannelID > 0
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func (r *Router) channelIDFromLegacyInputPeerChecked(ctx context.Context, userID int64, peer tg.InputPeerClass) (int64, error) {
|
|
channelID, ok := channelIDFromLegacyInputPeer(userID, peer)
|
|
if !ok {
|
|
return 0, peerIDInvalidErr()
|
|
}
|
|
if err := r.validateInputPeerChannelAccess(ctx, userID, peer, channelID); err != nil {
|
|
return 0, err
|
|
}
|
|
return channelID, nil
|
|
}
|
|
|
|
func isChannelNotFound(err error) bool {
|
|
return errors.Is(err, domain.ErrChannelInvalid) ||
|
|
errors.Is(err, domain.ErrChannelPrivate) ||
|
|
errors.Is(err, domain.ErrChannelUserBanned)
|
|
}
|
|
|
|
func channelInvalidErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrChannelTitleInvalid):
|
|
return tgerr400("CHAT_TITLE_EMPTY")
|
|
case errors.Is(err, domain.ErrChannelInvalid):
|
|
return tgerr400("CHANNEL_INVALID")
|
|
case errors.Is(err, domain.ErrChannelPrivate):
|
|
return tgerr400("CHANNEL_PRIVATE")
|
|
case errors.Is(err, domain.ErrChannelUserBanned):
|
|
return tgerr400("USER_BANNED_IN_CHANNEL")
|
|
case errors.Is(err, domain.ErrChannelWriteForbidden):
|
|
return tgerr400("CHAT_WRITE_FORBIDDEN")
|
|
case errors.Is(err, domain.ErrChannelAdminRequired):
|
|
return tgerr400("CHAT_ADMIN_REQUIRED")
|
|
case errors.Is(err, domain.ErrUserAlreadyParticipant):
|
|
return tgerr400("USER_ALREADY_PARTICIPANT")
|
|
case errors.Is(err, domain.ErrReplyMessageIDInvalid):
|
|
return replyMessageIDInvalidErr()
|
|
default:
|
|
if seconds, ok := domain.SlowModeWaitSeconds(err); ok {
|
|
return tgerr.New(420, fmt.Sprintf("SLOWMODE_WAIT_%d", seconds))
|
|
}
|
|
return internalErr()
|
|
}
|
|
}
|
|
|
|
func channelDeleteErr(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.ErrChannelAdminRequired):
|
|
return tgerr400("CHAT_ADMIN_REQUIRED")
|
|
default:
|
|
return channelInvalidErr(err)
|
|
}
|
|
}
|
|
|
|
func channelAdminErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrChannelNotModified):
|
|
return tgerr400("CHAT_NOT_MODIFIED")
|
|
case errors.Is(err, domain.ErrChatPublicRequired):
|
|
return tgerr400("CHAT_PUBLIC_REQUIRED")
|
|
case errors.Is(err, domain.ErrChatDiscussionUnallowed):
|
|
return tgerr400("CHAT_DISCUSSION_UNALLOWED")
|
|
case errors.Is(err, domain.ErrChannelRightForbidden):
|
|
return tgerr.New(403, "RIGHT_FORBIDDEN")
|
|
case errors.Is(err, domain.ErrChannelUserCreator):
|
|
return tgerr400("USER_CREATOR")
|
|
case errors.Is(err, domain.ErrMessageIDInvalid):
|
|
return messageIDInvalidErr()
|
|
default:
|
|
return channelInvalidErr(err)
|
|
}
|
|
}
|
|
|
|
func channelDiscussionErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrLinkNotModified):
|
|
return tgerr400("LINK_NOT_MODIFIED")
|
|
case errors.Is(err, domain.ErrBroadcastIDInvalid):
|
|
return tgerr400("BROADCAST_ID_INVALID")
|
|
case errors.Is(err, domain.ErrMegagroupIDInvalid):
|
|
return tgerr400("MEGAGROUP_ID_INVALID")
|
|
case errors.Is(err, domain.ErrMegagroupPrehistoryHidden):
|
|
return tgerr400("MEGAGROUP_PREHISTORY_HIDDEN")
|
|
default:
|
|
return channelAdminErr(err)
|
|
}
|
|
}
|
|
|
|
func channelInviteErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrInviteHashEmpty):
|
|
return tgerr400("INVITE_HASH_EMPTY")
|
|
case errors.Is(err, domain.ErrInviteHashInvalid):
|
|
return tgerr400("INVITE_HASH_INVALID")
|
|
case errors.Is(err, domain.ErrInviteHashExpired):
|
|
return tgerr.New(406, "INVITE_HASH_EXPIRED")
|
|
case errors.Is(err, domain.ErrInvitePermanent):
|
|
return tgerr400("CHAT_INVITE_PERMANENT")
|
|
case errors.Is(err, domain.ErrInviteRevokedMissing):
|
|
return tgerr400("INVITE_REVOKED_MISSING")
|
|
case errors.Is(err, domain.ErrInviteRequestSent):
|
|
return tgerr400("INVITE_REQUEST_SENT")
|
|
case errors.Is(err, domain.ErrHideRequesterMissing):
|
|
return tgerr400("HIDE_REQUESTER_MISSING")
|
|
case errors.Is(err, domain.ErrUsersTooMuch):
|
|
return tgerr400("USERS_TOO_MUCH")
|
|
case errors.Is(err, domain.ErrUserAlreadyParticipant):
|
|
return tgerr400("USER_ALREADY_PARTICIPANT")
|
|
case errors.Is(err, domain.ErrUserKicked):
|
|
return tgerr400("USER_KICKED")
|
|
default:
|
|
return channelInvalidErr(err)
|
|
}
|
|
}
|
|
|
|
func channelUsernameErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrUsernameInvalid):
|
|
return usernameInvalidErr()
|
|
case errors.Is(err, domain.ErrUsernameOccupied):
|
|
return usernameOccupiedErr()
|
|
case errors.Is(err, domain.ErrChannelNotModified):
|
|
return usernameNotModifiedErr()
|
|
default:
|
|
return channelAdminErr(err)
|
|
}
|
|
}
|
|
|
|
func tgerr400(message string) error {
|
|
return tgerr.New(400, message)
|
|
}
|