package rpc import ( "context" "errors" "github.com/gotd/td/tg" "telesrv/internal/domain" ) func (r *Router) registerChatlists(d *tg.ServerDispatcher) { d.OnChatlistsExportChatlistInvite(r.onChatlistsExportChatlistInvite) d.OnChatlistsDeleteExportedInvite(r.onChatlistsDeleteExportedInvite) d.OnChatlistsEditExportedInvite(r.onChatlistsEditExportedInvite) d.OnChatlistsGetExportedInvites(r.onChatlistsGetExportedInvites) d.OnChatlistsCheckChatlistInvite(r.onChatlistsCheckChatlistInvite) d.OnChatlistsJoinChatlistInvite(r.onChatlistsJoinChatlistInvite) d.OnChatlistsGetChatlistUpdates(r.onChatlistsGetChatlistUpdates) d.OnChatlistsJoinChatlistUpdates(r.onChatlistsJoinChatlistUpdates) d.OnChatlistsHideChatlistUpdates(r.onChatlistsHideChatlistUpdates) d.OnChatlistsGetLeaveChatlistSuggestions(r.onChatlistsGetLeaveChatlistSuggestions) d.OnChatlistsLeaveChatlist(r.onChatlistsLeaveChatlist) } func (r *Router) onChatlistsExportChatlistInvite(ctx context.Context, req *tg.ChatlistsExportChatlistInviteRequest) (*tg.ChatlistsExportedChatlistInvite, error) { if req == nil { return nil, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers) if err != nil { return nil, err } folder, invite, err := r.deps.Chatlists.ExportInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Title, peers, int(r.clock.Now().Unix())) if err != nil { return nil, chatlistErr(err) } if err := r.recordChatlistFilterUpdate(ctx, userID, folder.ID, &folder); err != nil { return nil, err } return &tg.ChatlistsExportedChatlistInvite{ Filter: tgDialogFilter(folder), Invite: *r.tgExportedChatlistInvite(invite), }, nil } func (r *Router) onChatlistsDeleteExportedInvite(ctx context.Context, req *tg.ChatlistsDeleteExportedInviteRequest) (bool, error) { if req == nil { return false, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return false, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } folder, changed, err := r.deps.Chatlists.DeleteInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Slug) if err != nil { return false, chatlistErr(err) } if changed { if err := r.recordChatlistFilterUpdate(ctx, userID, folder.ID, &folder); err != nil { return false, err } } return true, nil } func (r *Router) onChatlistsEditExportedInvite(ctx context.Context, req *tg.ChatlistsEditExportedInviteRequest) (*tg.ExportedChatlistInvite, error) { if req == nil { return nil, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } var title *string if v, ok := req.GetTitle(); ok { title = &v } var peersPtr *[]domain.DialogFolderPeer if v, ok := req.GetPeers(); ok { peers, err := r.dialogFolderPeersFromInput(ctx, userID, v) if err != nil { return nil, err } peersPtr = &peers } invite, err := r.deps.Chatlists.EditInvite(ctx, userID, chatlistFilterID(req.Chatlist), req.Slug, title, peersPtr, req.Flags.Has(0)) if err != nil { return nil, chatlistErr(err) } return r.tgExportedChatlistInvite(invite), nil } func (r *Router) onChatlistsGetExportedInvites(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (*tg.ChatlistsExportedInvites, error) { if r.deps.Chatlists == nil { return &tg.ChatlistsExportedInvites{}, nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } invites, err := r.deps.Chatlists.ListInvites(ctx, userID, chatlistFilterID(chatlist)) if err != nil { return nil, chatlistErr(err) } out := &tg.ChatlistsExportedInvites{ Invites: make([]tg.ExportedChatlistInvite, 0, len(invites)), } allPeers := make([]domain.DialogFolderPeer, 0) for _, invite := range invites { out.Invites = append(out.Invites, *r.tgExportedChatlistInvite(invite)) allPeers = append(allPeers, invite.Peers...) } out.Users, out.Chats = r.tgChatlistPeerEnvelope(ctx, userID, allPeers) return out, nil } func (r *Router) onChatlistsCheckChatlistInvite(ctx context.Context, slug string) (tg.ChatlistsChatlistInviteClass, error) { if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } preview, err := r.deps.Chatlists.CheckInvite(ctx, userID, slug) if err != nil { return nil, chatlistErr(err) } if preview.LocalFolder != nil { filterID := preview.LocalFolder.ID if preview.Membership != nil { filterID = preview.Membership.LocalFilterID } users, chats, channels := r.tgChatlistPeerEnvelopeWithChannels(ctx, userID, append(preview.Missing, preview.Already...)) if preview.Membership != nil { r.pushChatlistJoinedChannelNudges(ctx, userID, channels) } return &tg.ChatlistsChatlistInviteAlready{ FilterID: filterID, MissingPeers: tgChatlistPeers(preview.Missing), AlreadyPeers: tgChatlistPeers(preview.Already), Chats: chats, Users: users, }, nil } users, chats := r.tgChatlistPeerEnvelope(ctx, userID, preview.Missing) out := &tg.ChatlistsChatlistInvite{ TitleNoanimate: preview.OwnerFolder.TitleNoanimate, Title: tg.TextWithEntities{ Text: preview.OwnerFolder.Title, Entities: tgMessageEntities(preview.OwnerFolder.TitleEntities), }, Peers: tgChatlistPeers(preview.Missing), Chats: chats, Users: users, } if preview.OwnerFolder.HasEmoticon { out.SetEmoticon(preview.OwnerFolder.Emoticon) } return out, nil } func (r *Router) onChatlistsJoinChatlistInvite(ctx context.Context, req *tg.ChatlistsJoinChatlistInviteRequest) (tg.UpdatesClass, error) { if req == nil { return nil, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers) if err != nil { return nil, err } result, err := r.deps.Chatlists.JoinInvite(ctx, userID, req.Slug, peers, int(r.clock.Now().Unix())) if err != nil { return nil, chatlistErr(err) } return r.chatlistOperationUpdates(ctx, userID, result.Folder.ID, &result.Folder, result.ChannelResults, false) } func (r *Router) onChatlistsGetChatlistUpdates(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (*tg.ChatlistsChatlistUpdates, error) { if r.deps.Chatlists == nil { return &tg.ChatlistsChatlistUpdates{}, nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } updates, err := r.deps.Chatlists.GetUpdates(ctx, userID, chatlistFilterID(chatlist)) if err != nil { return nil, chatlistErr(err) } users, chats := r.tgChatlistPeerEnvelope(ctx, userID, updates.Missing) return &tg.ChatlistsChatlistUpdates{ MissingPeers: tgChatlistPeers(updates.Missing), Chats: chats, Users: users, }, nil } func (r *Router) onChatlistsJoinChatlistUpdates(ctx context.Context, req *tg.ChatlistsJoinChatlistUpdatesRequest) (tg.UpdatesClass, error) { if req == nil { return nil, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers) if err != nil { return nil, err } result, err := r.deps.Chatlists.JoinUpdates(ctx, userID, chatlistFilterID(req.Chatlist), peers, int(r.clock.Now().Unix())) if err != nil { return nil, chatlistErr(err) } return r.chatlistOperationUpdates(ctx, userID, result.Folder.ID, &result.Folder, result.ChannelResults, false) } func (r *Router) onChatlistsHideChatlistUpdates(ctx context.Context, chatlist tg.InputChatlistDialogFilter) (bool, error) { if r.deps.Chatlists == nil { return false, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } if err := r.deps.Chatlists.HideUpdates(ctx, userID, chatlistFilterID(chatlist)); err != nil { return false, chatlistErr(err) } return true, nil } func (r *Router) onChatlistsGetLeaveChatlistSuggestions(ctx context.Context, chatlist tg.InputChatlistDialogFilter) ([]tg.PeerClass, error) { if r.deps.Chatlists == nil { return []tg.PeerClass{}, nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } peers, err := r.deps.Chatlists.LeaveSuggestions(ctx, userID, chatlistFilterID(chatlist)) if err != nil { return nil, chatlistErr(err) } return tgChatlistPeers(peers), nil } func (r *Router) onChatlistsLeaveChatlist(ctx context.Context, req *tg.ChatlistsLeaveChatlistRequest) (tg.UpdatesClass, error) { if req == nil { return nil, inputRequestInvalidErr() } if r.deps.Chatlists == nil { return nil, notImplementedErr() } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } peers, err := r.dialogFolderPeersFromInput(ctx, userID, req.Peers) if err != nil { return nil, err } result, err := r.deps.Chatlists.Leave(ctx, userID, chatlistFilterID(req.Chatlist), peers, int(r.clock.Now().Unix())) if err != nil { return nil, chatlistErr(err) } return r.chatlistOperationUpdates(ctx, userID, result.FilterID, nil, result.ChannelResults, true) } func (r *Router) tgExportedChatlistInvite(invite domain.ChatlistInvite) *tg.ExportedChatlistInvite { out := &tg.ExportedChatlistInvite{ Title: invite.Title, URL: r.publicLink("addlist/" + invite.Slug), Peers: tgChatlistPeers(invite.Peers), } if invite.Revoked { out.Flags.Set(0) } return out } func (r *Router) tgChatlistPeerEnvelope(ctx context.Context, userID int64, peers []domain.DialogFolderPeer) ([]tg.UserClass, []tg.ChatClass) { users, chats, _ := r.tgChatlistPeerEnvelopeWithChannels(ctx, userID, peers) return users, chats } func (r *Router) tgChatlistPeerEnvelopeWithChannels(ctx context.Context, userID int64, peers []domain.DialogFolderPeer) ([]tg.UserClass, []tg.ChatClass, []domain.Channel) { userIDs := make([]int64, 0) channelIDs := make([]int64, 0) seenUsers := make(map[int64]struct{}) seenChannels := make(map[int64]struct{}) for _, item := range peers { switch item.Peer.Type { case domain.PeerTypeUser: if item.Peer.ID == 0 { continue } if _, ok := seenUsers[item.Peer.ID]; ok { continue } seenUsers[item.Peer.ID] = struct{}{} userIDs = append(userIDs, item.Peer.ID) case domain.PeerTypeChannel: if item.Peer.ID == 0 { continue } if _, ok := seenChannels[item.Peer.ID]; ok { continue } seenChannels[item.Peer.ID] = struct{}{} channelIDs = append(channelIDs, item.Peer.ID) } } chats := make([]tg.ChatClass, 0, len(channelIDs)) channels := make([]domain.Channel, 0, len(channelIDs)) if r.deps.Channels != nil && len(channelIDs) > 0 { if views, err := r.deps.Channels.GetChannels(ctx, userID, channelIDs); err == nil { for _, view := range views { if view.Channel.ID == 0 { continue } channels = append(channels, view.Channel) chats = append(chats, tgChannelChatForView(userID, view)) } } } return r.tgUsersForIDs(ctx, userID, userIDs), chats, channels } func tgChatlistPeers(peers []domain.DialogFolderPeer) []tg.PeerClass { out := make([]tg.PeerClass, 0, len(peers)) seen := make(map[domain.Peer]struct{}, len(peers)) for _, item := range peers { if _, ok := seen[item.Peer]; ok { continue } seen[item.Peer] = struct{}{} if peer := tgPeer(item.Peer); peer != nil { out = append(out, peer) } } return out } func chatlistFilterID(chatlist tg.InputChatlistDialogFilter) int { return chatlist.FilterID } func (r *Router) recordChatlistFilterUpdate(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder) error { event := domain.UpdateEvent{ Type: domain.UpdateEventDialogFilter, FilterID: filterID, DialogFilter: folder, Date: int(r.clock.Now().Unix()), } var err error if r.deps.Updates != nil { authKeyID, _ := AuthKeyIDFrom(ctx) sessionID, _ := SessionIDFrom(ctx) event, _, err = r.deps.Updates.RecordDialogFilter(ctx, authKeyID, userID, filterID, folder, rawAuthKeyIDForOrigin(ctx), sessionID) if err != nil { return internalErr() } } r.bookkeepAuxPtsForCurrentSession(ctx, event) r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, tgUpdateForOutboxEvent(event)) return nil } func (r *Router) chatlistFilterUpdates(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder) (tg.UpdatesClass, error) { event := domain.UpdateEvent{ Type: domain.UpdateEventDialogFilter, FilterID: filterID, DialogFilter: folder, Date: int(r.clock.Now().Unix()), } var err error if r.deps.Updates != nil { authKeyID, _ := AuthKeyIDFrom(ctx) sessionID, _ := SessionIDFrom(ctx) event, _, err = r.deps.Updates.RecordDialogFilter(ctx, authKeyID, userID, filterID, folder, rawAuthKeyIDForOrigin(ctx), sessionID) if err != nil { return nil, internalErr() } } r.bookkeepAuxPtsForCurrentSession(ctx, event) out := tgUpdateForOutboxEvent(event) if out == nil { out = &tg.Updates{Date: event.Date} } if folder != nil { out.Users, out.Chats = r.tgChatlistPeerEnvelope(ctx, userID, append(folder.PinnedPeers, folder.IncludePeers...)) } r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, out) return out, nil } func (r *Router) chatlistOperationUpdates(ctx context.Context, userID int64, filterID int, folder *domain.DialogFolder, channelResults []domain.CreateChannelResult, leaving bool) (tg.UpdatesClass, error) { base, err := r.chatlistFilterUpdates(ctx, userID, filterID, folder) if err != nil { return nil, err } out, ok := base.(*tg.Updates) if !ok { out = &tg.Updates{Date: int(r.clock.Now().Unix())} } for _, res := range channelResults { if res.Channel.ID == 0 { continue } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) if leaving { r.removeOnlineChannelMemberships(res.Channel.ID, userID) r.recordChannelStateForUser(ctx, userID, res.Channel.ID, true) } else { r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) } channelUpdates := r.channelOperationUpdates(ctx, userID, res) out = mergeUpdates(out, channelUpdates) if !leaving { if nudge := chatlistJoinedChannelNudge(res.Channel); nudge != nil { out.Updates = append(out.Updates, nudge) } } r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates { return r.channelOperationUpdates(ctx, viewerUserID, res) }) } return out, nil } func chatlistJoinedChannelNudge(channel domain.Channel) tg.UpdateClass { if channel.ID == 0 || channel.Pts <= 0 { return nil } update := &tg.UpdateChannelTooLong{ChannelID: channel.ID} update.SetPts(channel.Pts) return update } func (r *Router) pushChatlistJoinedChannelNudges(ctx context.Context, userID int64, channels []domain.Channel) { if userID == 0 || len(channels) == 0 { return } updates := make([]tg.UpdateClass, 0, len(channels)) for _, channel := range channels { if nudge := chatlistJoinedChannelNudge(channel); nudge != nil { updates = append(updates, nudge) } } if len(updates) == 0 { return } out := &tg.Updates{ Updates: updates, Users: []tg.UserClass{}, Chats: []tg.ChatClass{}, Date: int(r.clock.Now().Unix()), Seq: 0, } if _, ok := SessionIDFrom(ctx); ok { r.pushCurrentSessionMessage(ctx, "push chatlist joined channel nudges", out) return } r.pushUserUpdates(ctx, userID, out) } func chatlistErr(err error) error { switch { case err == nil: return nil case errors.Is(err, domain.ErrInviteRequestSent), errors.Is(err, domain.ErrUserAlreadyParticipant), errors.Is(err, domain.ErrChannelUserBanned), errors.Is(err, domain.ErrUserKicked), errors.Is(err, domain.ErrBotGroupsBlocked): return channelInviteErr(err) case errors.Is(err, domain.ErrChannelUserCreator), errors.Is(err, domain.ErrUserNotParticipant), errors.Is(err, domain.ErrChannelAdminRequired): return channelAdminErr(err) case errors.Is(err, domain.ErrChannelInvalid), errors.Is(err, domain.ErrChannelPrivate): return channelInvalidErr(err) case errors.Is(err, domain.ErrChatlistInviteInvalid): return inviteSlugEmptyErr() case errors.Is(err, domain.ErrChatlistInviteExpired): return inviteSlugExpiredErr() case errors.Is(err, domain.ErrChatlistInvitesTooMuch): return invitesTooMuchErr() case errors.Is(err, domain.ErrChatlistsTooMuch): return chatlistsTooMuchErr() case errors.Is(err, domain.ErrChatlistPeersEmpty): return peersListEmptyErr() case errors.Is(err, domain.ErrChatlistPeersTooMuch): return limitInvalidErr() case errors.Is(err, domain.ErrChatlistNotShareable): return filterNotSupportedErr() case errors.Is(err, domain.ErrChatlistInvalid): return filterIDInvalidErr() default: return internalErr() } }