owpengram-server/internal/rpc/channels_legacy_chat.go
Astra 206bde18e0 channels: give kicked/banned/promoted/transferred users a real qts so their client applies it
updateChannelParticipant carries the account's qts per the MTProto spec, but
the server always sent Qts: 0, so real clients silently discarded it as a
stale duplicate -- the banned/kicked user's channel never vanished locally
and no correct "removed by admin" message showed, even though the update was
delivered successfully at the transport layer.

Add a durable per-device qts queue (channel_participant_event_queue) sharing
its qts number space with the existing secret-chat queue (one qts sequence
per device, per spec), and use it to stamp a correct, monotonically
increasing qts on the update for every device of the affected user -- for
channel bans/kicks, admin promotion/demotion, and ownership transfer. A
device offline when it happened can now recover the event via
updates.getDifference instead of missing it permanently.
2026-09-15 15:43:52 +01:00

682 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)
r.deliverChannelParticipantSelfEvent(ctx, res.OldOwner.UserID, userID, res.Channel, res.PreviousOwner, res.OldOwner, res.Date)
r.deliverChannelParticipantSelfEvent(ctx, res.NewOwner.UserID, userID, res.Channel, res.PreviousNewOwner, res.NewOwner, res.Date)
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) bool {
return 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,
}
}