owpengram-server/internal/rpc/channels.go
A 6fd690a06e fix: keep participant changes out of channel pts
(cherry picked from commit 07b2497664bd108dec84f6cfe43715540faf2688)
2026-06-09 15:00:25 +08:00

4011 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, 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, 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)
}