owpengram-server/internal/rpc/channels_members.go
Astra 55a6e0bb35 channels: drop stale membership caches on join/leave
After channels.leaveChannel, a client that polls channels.getFullChannel kept
receiving a projection that still showed it as an active member (left=false)
until the per-(viewer,channel) RPC projection cache and the store-level member
cache lapsed on their own or the async read-model NOTIFY landed. The client
therefore kept an open compose box while every send was already rejected with
CHANNEL_PRIVATE - most visible on public forum supergroups, where getFullChannel
keeps succeeding via the preview path instead of tearing the chat down.

Every other membership-mutating path already busts these caches synchronously;
join/leave/invite/request-approval did not. Add:

- store: invalidateChannelMembershipCaches (row + member + dialog caches),
  called post-commit from JoinChannel, LeaveChannel, ImportInvite,
  InviteToChannel.
- rpc: invalidateChannelMembershipProjection (channelFullProjectionCache pair),
  called from the join/leave/invite/hide-requests handlers for every user whose
  membership changed.
2026-09-09 13:28:50 +01:00

986 lines
35 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"
"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()
}
chats := tgChannels(userID, channels)
r.applyUsernamesToPeerObjects(ctx, nil, chats)
return &tg.MessagesChats{Chats: chats}, nil
}
channels, err := r.deps.Channels.ListAdminedPublicChannels(ctx, userID)
if err != nil {
return nil, internalErr()
}
chats := tgChannels(userID, channels)
r.applyUsernamesToPeerObjects(ctx, nil, chats)
return &tg.MessagesChats{Chats: chats}, 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()
}
r.applyPeerReadModels(ctx, userID, users, nil)
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.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
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.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
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 227channels.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)
membershipChanged := channelMemberUserIDs(res.Members)
if len(membershipChanged) == 0 {
membershipChanged = []int64{userID}
}
r.invalidateChannelMembershipProjection(res.Channel.ID, membershipChanged)
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.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
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.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
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)
}
}