package rpc import ( "context" "errors" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" "go.uber.org/zap" "sort" "strings" "telesrv/internal/domain" "unicode/utf8" ) 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 } if req.ForCommunityPeer { channels, err := r.deps.Channels.ListCommunityLinkableChannels(ctx, userID) if err != nil { return nil, internalErr() } return &tg.MessagesChats{Chats: tgChannels(userID, channels)}, 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) 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 { // deleteParticipantHistory fan-out 异步化(设计 Phase 0),与已异步的 messages.deleteMessages // 频道分支对齐。builder 纯 viewer 无关(仅 delete update + ChatMin,无 Users),无需预热。 r.enqueueChannelFanout(ctx, channelFanoutMembers, userID, res.Channel.ID, res.Event.Pts, res.Recipients, func(_ context.Context, 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) { return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) { return r.deps.Channels.SetJoinToSend(ctx, userID, channelID, req.Enabled) }) } func (r *Router) onChannelsToggleJoinRequest(ctx context.Context, req *tg.ChannelsToggleJoinRequestRequest) (tg.UpdatesClass, error) { return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) { return r.deps.Channels.SetJoinRequest(ctx, userID, channelID, req.Enabled) }) } func (r *Router) onChannelsToggleParticipantsHidden(ctx context.Context, req *tg.ChannelsToggleParticipantsHiddenRequest) (tg.UpdatesClass, error) { return r.applyChannelAdminStateMutation(ctx, req.Channel, func(ctx context.Context, userID, channelID int64) (domain.Channel, error) { return r.deps.Channels.SetParticipantsHidden(ctx, userID, channelID, req.Enabled) }) } 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) onChannelsGetParticipants(ctx context.Context, req *tg.ChannelsGetParticipantsRequest) (tg.ChannelsChannelParticipantsClass, error) { if r.deps.Channels == nil && r.deps.Communities == nil { return &tg.ChannelsChannelParticipants{}, nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } ref, ok := inputChannelRef(req.Channel) if !ok { return nil, channelInvalidErr(domain.ErrChannelInvalid) } filter := domainChannelParticipantsFilter(req.Filter) if utf8.RuneCountInString(filter.Query) > domain.MaxChannelParticipantsQueryLength { return nil, limitInvalidErr() } if community, isCommunity, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); isCommunity { if err != nil { return nil, err } list, err := r.deps.Communities.Participants(ctx, userID, community.Community.ID, filter, req.Offset, req.Limit) if err != nil { return nil, communityErr(err) } if req.Hash != 0 && list.Hash == req.Hash { return &tg.ChannelsChannelParticipantsNotModified{}, nil } participants := make([]tg.ChannelParticipantClass, 0, len(list.Participants)) for _, member := range list.Participants { participants = append(participants, tgCommunityMember(userID, member)) } return &tg.ChannelsChannelParticipants{Count: list.Count, Participants: participants, Chats: []tg.ChatClass{tgCommunityChat(community)}, Users: tgUsers(list.Users)}, nil } if r.deps.Channels == nil { return nil, channelInvalidErr(domain.ErrChannelInvalid) } list, err := r.deps.Channels.GetParticipants(ctx, userID, ref.ID, filter, req.Offset, req.Limit) if err != nil { return nil, channelInvalidErr(err) } if !inputChannelAccessHashMatches(ref, list.Channel) { return nil, channelInvalidErr(domain.ErrChannelPrivate) } 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)) seenUserIDs := make(map[int64]struct{}, len(list.Participants)*2) addUserID := func(id int64) { if id == 0 { return } if _, ok := seenUserIDs[id]; ok { return } seenUserIDs[id] = struct{}{} userIDs = append(userIDs, id) } for _, member := range list.Participants { participant := tgChannelParticipant(userID, member) participants = append(participants, participant) for _, id := range channelParticipantUserRefs(participant) { addUserID(id) } } users := r.tgUsers(list.Users) if len(users) == 0 { users = r.tgUsersForIDs(ctx, userID, userIDs) } else { present := make(map[int64]struct{}, len(users)) for _, item := range users { if u, ok := item.(*tg.User); ok { present[u.ID] = struct{}{} } } missing := make([]int64, 0, len(userIDs)) for _, id := range userIDs { if _, ok := present[id]; !ok { missing = append(missing, id) } } if len(missing) > 0 { users = append(users, r.tgUsersForIDs(ctx, userID, missing)...) } } r.applyPeerReadModels(ctx, userID, users, nil) r.log.Debug("channels.getParticipants result", zap.Int64("channel_id", ref.ID), 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, channelParticipantErr(err) } participant := tgChannelParticipant(userID, member) users := r.tgUsersForIDs(ctx, userID, channelParticipantUserRefs(participant)) r.applyPeerReadModels(ctx, userID, users, nil) return &tg.ChannelsChannelParticipant{ Participant: participant, Users: users, }, nil } func channelParticipantErr(err error) error { if errors.Is(err, domain.ErrUserNotParticipant) { return tgerr400("USER_NOT_PARTICIPANT") } return channelInvalidErr(err) } func channelParticipantUserRefs(participant tg.ChannelParticipantClass) []int64 { ids := make([]int64, 0, 3) add := func(id int64) { if id == 0 { return } for _, existing := range ids { if existing == id { return } } ids = append(ids, id) } addPeer := func(peer tg.PeerClass) { if p, ok := peer.(*tg.PeerUser); ok { add(p.UserID) } } switch p := participant.(type) { case *tg.ChannelParticipantCreator: add(p.UserID) case *tg.ChannelParticipantAdmin: add(p.UserID) add(p.PromotedBy) if inviterID, ok := p.GetInviterID(); ok { add(inviterID) } case *tg.ChannelParticipantSelf: add(p.UserID) add(p.InviterID) case *tg.ChannelParticipant: add(p.UserID) case *tg.ChannelParticipantLeft: addPeer(p.Peer) case *tg.ChannelParticipantBanned: addPeer(p.Peer) add(p.KickedBy) } return ids } 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 } // Authorize before evaluating target privacy, otherwise a non-admin could // probe whether a target permits invites. view, err := r.deps.Channels.ResolveChannel(ctx, userID, channelID) if err != nil { return nil, channelInviteErr(err) } if !view.Self.CanInviteUsers(view.Channel) { return nil, channelInviteErr(domain.ErrChannelAdminRequired) } userIDs, missingInvitees, err := r.filterChatInvitePrivacy(ctx, userID, userIDs) if err != nil { return nil, internalErr() } date := int(r.clock.Now().Unix()) if len(userIDs) == 0 { return &tg.MessagesInvitedUsers{ Updates: emptyInvitedUsersUpdates(date), MissingInvitees: missingInvitees, }, nil } res, err := r.deps.Channels.InviteToChannel(ctx, userID, channelID, userIDs, date) if err != nil { return nil, channelInviteErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) cache := newViewerPeerCache(r) updates := r.channelOperationUpdatesWithPeerCache(ctx, userID, res, cache) r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates { return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, res, cache) }) return &tg.MessagesInvitedUsers{Updates: updates, MissingInvitees: missingInvitees}, nil } func (r *Router) onChannelsJoinChannel(ctx context.Context, input tg.InputChannelClass) (tg.MessagesChatInviteJoinResultClass, 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.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) 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) }) // Layer 227:channels.joinChannel 返回 messages.ChatInviteJoinResult; // 正常加入即 chatInviteJoinResultOk 包裹本次操作的 updates。 return &tg.MessagesChatInviteJoinResultOk{Updates: 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, channelAdminErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) r.removeOnlineChannelMemberships(res.Channel.ID, userID) r.recordChannelStateForUser(ctx, userID, res.Channel.ID, true) 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 } // recordChannelStateForUser 给该账号写一条 durable channel 状态事件: // 离线设备经 difference 收到 updateChannel 后重拉 channel,发现 left/ // forbidden 并移除会话;excludeCurrent 时当前 session 由 RPC 响应承担。 func (r *Router) recordChannelStateForUser(ctx context.Context, userID, channelID int64, excludeCurrent bool) { if r.deps.Updates == nil || userID == 0 || channelID == 0 { return } authKeyID := [8]byte{} excludeSessionID := int64(0) if excludeCurrent { authKeyID, _ = AuthKeyIDFrom(ctx) excludeSessionID, _ = SessionIDFrom(ctx) } excludeAuthKeyID := [8]byte{} if excludeCurrent { excludeAuthKeyID = rawAuthKeyIDForOrigin(ctx) } event, _, err := r.deps.Updates.RecordChannelState(ctx, authKeyID, userID, channelID, excludeAuthKeyID, excludeSessionID) if err != nil { return } if excludeCurrent { r.bookkeepAuxPtsForCurrentSession(ctx, event) } } func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAdminRequest) (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() } target, found, err := r.userFromInput(ctx, userID, req.UserID) if err != nil { return nil, internalErr() } if !found || target.ID == 0 { return nil, peerIDInvalidErr() } if community, ok, err := r.maybeCommunityFromInput(ctx, userID, req.Channel); ok { if err != nil { return nil, err } view, changed, err := r.deps.Communities.EditAdmin(ctx, userID, domain.CommunityEditAdminRequest{ CommunityID: community.Community.ID, UserID: target.ID, Rights: domainChannelAdminRights(req.AdminRights), Rank: req.Rank, Date: int(r.clock.Now().Unix()), }) if err != nil { return nil, communityErr(err) } updates := r.communityMutationUpdates(ctx, userID, view, changed) if changed && target.ID != userID { r.refreshAndPushCommunityState(ctx, target.ID, community.Community.ID, community.Community) } return updates, nil } if r.deps.Channels == nil { return nil, channelInvalidErr(domain.ErrChannelInvalid) } channelID, err := r.channelIDFromInput(ctx, userID, req.Channel) if err != nil { return nil, err } res, err := r.deps.Channels.EditAdmin(ctx, userID, domain.EditChannelAdminRequest{ UserID: userID, ChannelID: channelID, MemberID: target.ID, AdminRights: domainChannelAdminRights(req.AdminRights), RankSet: req.Flags.Has(0), Rank: req.Rank, Date: int(r.clock.Now().Unix()), }) if err != nil { return nil, channelAdminErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) if res.Participant.Status == domain.ChannelMemberActive { r.addOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID) } else { r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID) } 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) 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) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) // 与 onChannelsEditAdmin 对称维护在线成员路由索引:踢出/封禁后立即摘除, // 否则被踢在线成员会留着 stale byMemberChannel 条目直到断线(占用实时 // fan-out cap 名额,且群通话推送等未过 PG 复核的路径会继续投递)。 if res.Participant.Status == domain.ChannelMemberActive { r.addOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID) } else { r.removeOnlineChannelMemberships(res.Channel.ID, res.Participant.UserID) } if res.Participant.Status == domain.ChannelMemberKicked && res.Previous.Status == domain.ChannelMemberActive { r.recordChannelStateForUser(ctx, res.Participant.UserID, res.Channel.ID, false) } cache := newViewerPeerCache(r) build := func(viewerUserID int64) *tg.Updates { updates := r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, userID, res.Channel, res.Previous, res.Participant, res.Date, cache) if updates != nil && res.ServiceEvent.Pts != 0 { // megagroup 踢人服务消息占 channel pts,必须先于 participant // update 应用,让成员面板/人数与消息流一起收敛。 if update := tgChannelUpdate(viewerUserID, res.ServiceEvent); update != nil { updates.Updates = append([]tg.UpdateClass{update}, updates.Updates...) } } return updates } updates := build(userID) r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, build) 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) } events := tgChannelAdminLogEvents(userID, res.Events) chats := []tg.ChatClass{tgChannelChatMin(userID, res.Channel)} users := r.channelAdminLogUsers(ctx, userID, res.Events) r.applyPeerReadModels(ctx, userID, users, chats) return &tg.ChannelsAdminLogResults{ Events: events, Chats: chats, Users: users, }, 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) 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.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) 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.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) 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 canViewChannelJoinRequests(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo)) } 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{tgChannelChatMin(viewerUserID, channel)}, 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) channelParticipantUpdates(ctx context.Context, viewerUserID, actorUserID int64, channel domain.Channel, previous, participant domain.ChannelMember, date int) *tg.Updates { return r.channelParticipantUpdatesWithPeerCache(ctx, viewerUserID, actorUserID, channel, previous, participant, date, newViewerPeerCache(r)) } func (r *Router) channelParticipantUpdatesWithPeerCache(ctx context.Context, viewerUserID, actorUserID int64, channel domain.Channel, previous, participant domain.ChannelMember, date int, cache *viewerPeerCache) *tg.Updates { if cache == nil { cache = newViewerPeerCache(r) } 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)) } // 当事成员必须收到完整 channel 投影:被踢/被封禁状态只有通过非 min // 对象的 left/banned_rights 才会被客户端应用,min 形态会让被踢者 // 永远不知道自己已离开会话。其它接收者用 min 保护各自本地权限。 var chat tg.ChatClass if viewerUserID != 0 && viewerUserID == participant.UserID { self := participant chat = tgChannelChat(viewerUserID, channel, &self) } else { chat = tgChannelChatMin(viewerUserID, channel) } return &tg.Updates{ Updates: []tg.UpdateClass{update, &tg.UpdateChannel{ChannelID: channel.ID}}, Users: tgUsersForViewer(viewerUserID, cache.usersForIDs(ctx, viewerUserID, []int64{participant.UserID, participant.InviterUserID, previous.UserID, previous.InviterUserID, update.ActorID})), Chats: []tg.ChatClass{chat}, Date: int(r.clock.Now().Unix()), Seq: 0, } } func (r *Router) channelOwnershipTransferUpdatesWithPeerCache(ctx context.Context, viewerUserID, actorUserID int64, res domain.TransferChannelOwnershipResult, cache *viewerPeerCache) *tg.Updates { if cache == nil { cache = newViewerPeerCache(r) } date := res.Date if date == 0 { date = int(r.clock.Now().Unix()) } updates := make([]tg.UpdateClass, 0, len(res.Events)+1) userIDs := []int64{actorUserID, res.PreviousOwner.UserID, res.OldOwner.UserID, res.OldOwner.InviterUserID, res.PreviousNewOwner.UserID, res.NewOwner.UserID, res.NewOwner.InviterUserID} for _, event := range res.Events { update := tgChannelUpdate(viewerUserID, event) if update == nil { continue } updates = append(updates, update) userIDs = append(userIDs, event.SenderUserID, event.Previous.UserID, event.Previous.InviterUserID, event.Participant.UserID, event.Participant.InviterUserID) } updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID}) var self *domain.ChannelMember switch viewerUserID { case res.OldOwner.UserID: member := res.OldOwner self = &member case res.NewOwner.UserID: member := res.NewOwner self = &member } chat := tgChannelChatMin(viewerUserID, res.Channel) if self != nil { chat = tgChannelChat(viewerUserID, res.Channel, self) } return &tg.Updates{ Updates: updates, Users: tgUsersForViewer(viewerUserID, cache.usersForIDs(ctx, viewerUserID, uniqueRecipientIDs(userIDs))), Chats: []tg.ChatClass{chat}, Date: date, Seq: 0, } } 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) } 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] }) return r.tgUsersForIDs(ctx, currentUserID, userIDs) } 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 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.ErrUserNotParticipant): return tgerr400("USER_NOT_PARTICIPANT") case errors.Is(err, domain.ErrMegagroupIDInvalid): return tgerr400("MEGAGROUP_ID_INVALID") case errors.Is(err, domain.ErrMessageIDInvalid): return messageIDInvalidErr() default: return channelInvalidErr(err) } } func channelTransferErr(err error) error { switch { case errors.Is(err, domain.ErrChannelAdminRequired): return tgerr400("CHAT_CREATOR_REQUIRED") case errors.Is(err, domain.ErrUserNotParticipant): return tgerr400("PARTICIPANT_MISSING") default: return channelAdminErr(err) } }