owpengram-server/internal/rpc/channels_legacy_chat.go
Astra 798ea70b4e rpc: return PASSWORD_MISSING for channel transfer without 2FA
messages.editChatCreator unconditionally returned PASSWORD_HASH_INVALID for
an account with no cloud password at all. Real Telegram Desktop's transfer-
ownership flow only recognizes the distinct PASSWORD_MISSING error to show
its "enable 2FA first" box; anything else falls through into the real
password-entry flow, which then has nothing to check against and crashes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 22:07:54 +01:00

680 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"errors"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap"
"telesrv/internal/compat/tdesktop"
"telesrv/internal/domain"
"unicode/utf8"
)
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)
}
if req.TTLPeriod < 0 {
return nil, ttlPeriodInvalidErr()
}
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)
memberIDs, missingInvitees, err := r.filterChatInvitePrivacy(ctx, userID, memberIDs)
if err != nil {
return nil, internalErr()
}
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),
)
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...))
}
cache := newViewerPeerCache(r)
canonicalUpdates := r.channelOperationUpdatesWithPeerCache(ctx, userID, responseRes, cache)
var inviteUpdates *tg.Updates
if inviteRes.Event.Pts != 0 {
inviteUpdates = r.channelOperationUpdatesWithPeerCache(ctx, userID, inviteRes, cache)
if inviteUpdates != nil {
canonicalUpdates.Updates = append(canonicalUpdates.Updates, inviteUpdates.Updates...)
}
}
updates := canonicalUpdates
if createChatNeedsLegacyChat(ctx) {
updates = r.tdesktopCreateChatUpdatesWithPeerCache(ctx, userID, responseRes, cache)
if inviteUpdates != nil {
updates.Updates = append(updates.Updates, inviteUpdates.Updates...)
}
}
// The rpc_result reaches only the calling session. Keep the creator's other
// sessions in sync with the same canonical channel state; compatibility-only
// legacy chat projection is needed solely by the synchronous create callback.
r.pushUserUpdates(ctx, userID, canonicalUpdates)
if inviteRes.Event.Pts != 0 {
r.pushChannelExplicitUpdates(ctx, userID, inviteRes.Channel.ID, memberIDs, func(viewerUserID int64) *tg.Updates {
return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, inviteRes, cache)
})
}
return &tg.MessagesInvitedUsers{Updates: updates, MissingInvitees: missingInvitees}, 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 {
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)
}
views, err := r.deps.Channels.GetChannels(ctx, userID, unique)
if err != nil {
return nil, channelInvalidErr(err)
}
byID := make(map[int64]domain.ChannelView, len(views))
for _, view := range views {
if view.Channel.ID != 0 {
byID[view.Channel.ID] = view
}
}
for _, id := range ids {
if id <= 0 {
continue
}
if view, ok := byID[id]; ok {
chats = append(chats, tgChannelChatForView(userID, view))
}
}
}
r.applyUsernamesToPeerObjects(ctx, nil, chats)
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 && r.deps.Communities == 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()
}
if community, ok, err := r.maybeCommunityFromInputPeer(ctx, userID, req.Peer); ok {
if err != nil {
return false, err
}
view, changed, err := r.deps.Communities.EditAbout(ctx, userID, community.Community.ID, req.About)
if err != nil {
return false, communityErr(err)
}
if changed {
r.pushCommunityState(ctx, userID, view)
}
return true, nil
}
if r.deps.Channels == nil {
return false, channelInvalidErr(domain.ErrChannelInvalid)
}
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 && r.deps.Communities == nil {
return nil, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if community, ok, err := r.maybeCommunityFromInputPeer(ctx, userID, req.Peer); ok {
if err != nil {
return nil, err
}
view, changed, err := r.deps.Communities.EditDefaultBannedRights(ctx, userID, community.Community.ID, domainChannelBannedRights(req.BannedRights))
if err != nil {
return nil, communityErr(err)
}
return r.communityMutationUpdates(ctx, userID, view, changed), nil
}
if r.deps.Channels == nil {
return nil, channelInvalidErr(domain.ErrChannelInvalid)
}
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)
}
return r.channelStateMutationUpdates(ctx, userID, channel), nil
}
func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.MessagesEditChatCreatorRequest) (tg.UpdatesClass, error) {
if r.deps.Channels == nil {
return nil, notImplementedErr()
}
if req.UserID == nil {
return nil, peerIDInvalidErr()
}
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
}
if req.Password == nil {
return nil, passwordHashInvalidErr()
}
if r.deps.Account == nil {
return nil, passwordHashInvalidErr()
}
// 转让所有权无条件要求已开启两步验证:先探测账号是否设有密码,
// 让 messages.editChatCreator 的探测请求inputUserEmpty + inputCheckPasswordEmpty
// 拿到 PASSWORD_MISSING 而非 PASSWORD_HASH_INVALID —— 客户端只识别前者来展示
// “请先开启两步验证”提示,否则会误入真实密码校验流程并在没有密码可核对时崩溃。
passwordSettings, err := r.deps.Account.GetPassword(ctx, userID)
if err != nil {
return nil, internalErr()
}
if !passwordSettings.HasPassword {
return nil, passwordMissingErr()
}
if _, ok := req.UserID.(*tg.InputUserEmpty); ok {
return nil, passwordHashInvalidErr()
}
if _, ok := req.Password.(*tg.InputCheckPasswordEmpty); ok {
return nil, passwordHashInvalidErr()
}
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.Bot {
return nil, userIDInvalidErr()
}
if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil {
return nil, passwordErr(err)
}
res, err := r.deps.Channels.TransferOwnership(ctx, userID, domain.TransferChannelOwnershipRequest{
UserID: userID,
ChannelID: channelID,
NewOwnerID: target.ID,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelTransferErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
r.addOnlineChannelMemberships(res.Channel.ID, res.OldOwner.UserID, res.NewOwner.UserID)
cache := newViewerPeerCache(r)
updates := r.channelOwnershipTransferUpdatesWithPeerCache(ctx, userID, userID, res, cache)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelOwnershipTransferUpdatesWithPeerCache(ctx, viewerUserID, userID, res, cache)
})
return updates, nil
}
func (r *Router) onMessagesGetFutureChatCreatorAfterLeave(ctx context.Context, peer tg.InputPeerClass) (tg.UserClass, error) {
if r.deps.Channels == nil {
return nil, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
resolved, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return nil, err
}
if resolved.Type != domain.PeerTypeChannel || resolved.ID == 0 {
return nil, peerIDInvalidErr()
}
member, err := r.deps.Channels.FutureCreatorAfterLeave(ctx, userID, resolved.ID)
if err != nil {
return nil, channelAdminErr(err)
}
if r.deps.Users == nil {
return nil, userIDInvalidErr()
}
user, found, err := r.deps.Users.ByID(ctx, userID, member.UserID)
if err != nil {
return nil, internalErr()
}
if !found || user.Bot {
return nil, tgerr400("USER_NOT_PARTICIPANT")
}
users := r.tgUsersForViewer(userID, []domain.User{user})
if len(users) == 0 {
return nil, userIDInvalidErr()
}
return users[0], nil
}
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()
}
res, err := r.deps.Channels.EditMemberRank(ctx, userID, domain.EditChannelMemberRankRequest{
UserID: userID,
ChannelID: channelID,
MemberID: participant.ID,
Rank: req.Rank,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelAdminErr(err)
}
cache := newViewerPeerCache(r)
updates := r.channelParticipantUpdatesWithPeerCache(ctx, userID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache)
})
return updates, nil
}
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()
}
emoticon, err := chatThemeEmoticonFromInput(req.Theme)
if err != nil {
return nil, err
}
if r.deps.Messages == nil {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.SetChatTheme(ctx, userID, domain.SetPrivateChatThemeRequest{
OwnerUserID: userID,
Peer: peer,
Emoticon: emoticon,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
return nil, peerIDInvalidErr()
}
return nil, internalErr()
}
if res.Changed {
r.invalidateRPCProjectionForPeer(userID, peer)
r.invalidateRPCProjectionForPeer(peer.ID, domain.Peer{Type: domain.PeerTypeUser, ID: userID})
}
if !res.Changed || res.Send.SenderMessage.ID == 0 {
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
}
return tgPrivateMessageUpdates(
res.Send.SenderEvent,
res.Send.SenderMessage,
0,
false,
r.usersForMessageUpdate(ctx, userID, res.Send.SenderMessage),
[]tg.ChatClass{},
), nil
}
func chatThemeEmoticonFromInput(theme tg.InputChatThemeClass) (string, error) {
switch value := theme.(type) {
case *tg.InputChatThemeEmpty:
return "", nil
case *tg.InputChatTheme:
if value.Emoticon == "" {
return "", nil
}
if !tdesktop.IsChatThemeEmoticon(value.Emoticon) {
return "", themeInvalidErr()
}
return value.Emoticon, nil
case *tg.InputChatThemeUniqueGift:
return "", themeInvalidErr()
default:
return "", inputConstructorInvalidErr()
}
}
func (r *Router) onMessagesSetChatWallPaper(ctx context.Context, req *tg.MessagesSetChatWallPaperRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputConstructorInvalidErr()
}
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
}
wallpaper, err := domainWallpaperFromSetChatWallPaper(req)
if err != nil {
return nil, err
}
switch peer.Type {
case domain.PeerTypeUser:
if peer.ID == 0 {
return nil, peerIDInvalidErr()
}
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
case domain.PeerTypeChannel:
if r.deps.Channels == nil || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
res, err := r.deps.Channels.SetWallpaper(ctx, userID, domain.SetChannelWallpaperRequest{
UserID: userID,
ChannelID: peer.ID,
Wallpaper: wallpaper,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, channelAdminErr(err)
}
r.invalidateRPCProjectionForChannel(res.Channel.ID)
if res.Changed && res.Event.Pts != 0 {
r.enqueueChannelWallpaperFanout(ctx, userID, res)
} else if res.Changed {
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelWallpaperUpdatesWithPeerCache(ctx, viewerUserID, res, nil)
})
}
return r.channelWallpaperUpdatesWithPeerCache(ctx, userID, res, nil), nil
default:
return nil, peerIDInvalidErr()
}
}
func (r *Router) channelWallpaperUpdatesWithPeerCache(ctx context.Context, viewerUserID int64, res domain.SetChannelWallpaperResult, cache *viewerPeerCache) *tg.Updates {
update := tgUpdatePeerWallpaper(domain.Peer{Type: domain.PeerTypeChannel, ID: res.Channel.ID}, res.Channel.Wallpaper)
if res.Event.Pts != 0 {
sendRes := domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}
updates := r.channelMessageUpdatesWithPeerCache(ctx, viewerUserID, sendRes, 0, cache)
updates.Updates = append(updates.Updates, update)
return updates
}
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func (r *Router) enqueueChannelWallpaperFanout(ctx context.Context, originUserID int64, res domain.SetChannelWallpaperResult) {
sendRes := domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}
fanoutCache := newViewerPeerCache(r)
ownerIDs := channelMessageFanoutOwnerIDs(sendRes, nil)
r.enqueueChannelFanoutWithPrefetch(ctx, channelFanoutMessageBox, originUserID, res.Channel.ID, res.Event.Pts, res.Recipients,
0,
func(bgCtx context.Context, viewers []int64) {
r.prefetchChannelFanoutUsers(bgCtx, fanoutCache, viewers, ownerIDs)
},
func(bgCtx context.Context, viewerUserID int64) *tg.Updates {
return r.channelWallpaperUpdatesWithPeerCache(bgCtx, viewerUserID, res, fanoutCache)
})
}
func (r *Router) onMessagesSetChatAvailableReactions(ctx context.Context, req *tg.MessagesSetChatAvailableReactionsRequest) (tg.UpdatesClass, error) {
if r.deps.Channels == nil {
return nil, notImplementedErr()
}
if req == nil {
return nil, tgerr400("REACTION_INVALID")
}
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
}
current, err := r.deps.Channels.GetChannelForChangeInfo(ctx, userID, channelID)
if err != nil {
return nil, channelAdminErr(err)
}
_, defaultReactionDocuments := r.availableReactionDocumentMaps(ctx)
policy, err := domainChannelReactionPolicy(req, current.Channel.ReactionPolicy, defaultReactionDocuments)
if err != nil {
return nil, err
}
channel, err := r.deps.Channels.SetAvailableReactions(ctx, userID, channelID, policy)
if err != nil {
return nil, channelAdminErr(err)
}
return r.channelStateMutationUpdates(ctx, userID, channel), nil
}
func legacyBasicGroupAdminRights() tg.ChatAdminRights {
return tg.ChatAdminRights{
ChangeInfo: true,
DeleteMessages: true,
BanUsers: true,
InviteUsers: true,
PinMessages: true,
Other: true,
}
}