package memory import ( "context" "errors" "sort" "strconv" "strings" "telesrv/internal/domain" ) func (s *ChannelStore) GetParticipants(_ context.Context, viewerUserID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) { s.mu.RLock() defer s.mu.RUnlock() channel, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID) if err != nil { return domain.ChannelParticipantList{}, err } if limit <= 0 || limit > domain.MaxChannelParticipantsLimit { limit = domain.MaxChannelParticipantsLimit } // 广播频道订阅者列表仅管理员可枚举(与隐藏成员同一门控):admins filter 仍放行(徽章数据源)。 if channel.MembersListAdminOnly() && !isChannelAdmin(viewer) { switch filter.Kind { case domain.ChannelParticipantsAdmins: case domain.ChannelParticipantsBots: return domain.ChannelParticipantList{Channel: channel, Count: 0}, nil default: return domain.ChannelParticipantList{Channel: channel, Count: channel.ParticipantsCount}, nil } } if (filter.Kind == domain.ChannelParticipantsBanned || filter.Kind == domain.ChannelParticipantsKicked) && !isChannelAdmin(viewer) { return domain.ChannelParticipantList{Channel: channel}, nil } query := strings.ToLower(strings.TrimSpace(filter.Query)) items := make([]domain.ChannelMember, 0, len(s.members[channelID])) for _, member := range s.members[channelID] { if !channelParticipantMatchesFilter(member, filter.Kind, query) { continue } if shouldHideAnonymousAdminFromParticipantList(viewer, member) { continue } items = append(items, member) } sort.Slice(items, func(i, j int) bool { if items[i].Role != items[j].Role { return channelRoleOrder(items[i].Role) < channelRoleOrder(items[j].Role) } return items[i].UserID < items[j].UserID }) count := len(items) if offset < 0 { offset = 0 } if offset > domain.MaxChannelParticipantsOffset { offset = domain.MaxChannelParticipantsOffset } if offset >= len(items) { items = nil } else { end := offset + limit if end > len(items) { end = len(items) } items = items[offset:end] } return domain.ChannelParticipantList{ Channel: channel, Participants: cloneChannelMembers(items), Count: count, }, nil } func (s *ChannelStore) GetParticipant(_ context.Context, viewerUserID, channelID, participantUserID int64) (domain.ChannelMember, error) { s.mu.RLock() defer s.mu.RUnlock() _, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID) if err != nil { return domain.ChannelMember{}, err } if viewerUserID == participantUserID && viewer.Guest { return domain.ChannelMember{}, domain.ErrUserNotParticipant } member, ok := s.members[channelID][participantUserID] if !ok || (participantUserID == viewerUserID && member.Status == domain.ChannelMemberLeft) { return domain.ChannelMember{}, domain.ErrUserNotParticipant } return member, nil } func (s *ChannelStore) FutureCreatorAfterLeave(_ context.Context, channelID, userID int64) (domain.ChannelMember, error) { s.mu.RLock() defer s.mu.RUnlock() channel, member, err := s.channelAndMemberLocked(userID, channelID) if err != nil { return domain.ChannelMember{}, err } if channel.CreatorUserID != userID || member.Role != domain.ChannelRoleCreator { return domain.ChannelMember{}, domain.ErrChannelAdminRequired } return s.futureCreatorAfterLeaveLocked(channelID, userID) } func (s *ChannelStore) JoinChannel(_ context.Context, channelID, userID int64, date int) (domain.CreateChannelResult, error) { s.mu.Lock() defer s.mu.Unlock() channel, ok := s.channels[channelID] if !ok || channel.Deleted { return domain.CreateChannelResult{}, domain.ErrChannelInvalid } if channel.Monoforum { return domain.CreateChannelResult{}, domain.ErrChannelMonoforumUnsupported } preJoinTopID := channel.TopMessageID if existing, ok := s.members[channelID][userID]; ok { if existing.Status == domain.ChannelMemberActive { return domain.CreateChannelResult{}, domain.ErrUserAlreadyParticipant } if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages { return domain.CreateChannelResult{}, domain.ErrChannelUserBanned } } if channel.JoinRequest { if err := s.recordPublicJoinRequestLocked(channel, userID, date); err != nil { return domain.CreateChannelResult{}, err } return domain.CreateChannelResult{Channel: channel}, domain.ErrInviteRequestSent } member := domain.ChannelMember{ ChannelID: channelID, UserID: userID, // 自加入:inviter 即本人(对齐官方 channelParticipantSelf.inviter_id == user_id), // 客户端据此生成本地「您加入了此频道」服务消息把广播频道补进会话列表。见 postgres 同处注释。 InviterUserID: userID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive, JoinedAt: date, } if existing, ok := s.members[channelID][userID]; ok { member = existing member.Status = domain.ChannelMemberActive member.LeftAt = 0 // 重进是全新 participant:不保留旧的角色/管理权/Tag;inviter 重置为本人(自加入)。 // 只有 channel 当前 owner 仍是该账号时,才保留 creator 身份。 if existing.Role != domain.ChannelRoleCreator || channel.CreatorUserID != userID { member.Role = domain.ChannelRoleMember member.AdminRights = domain.ChannelAdminRights{} member.Rank = "" member.InviterUserID = userID member.JoinedAt = date } if minID := channelInitialAvailableMinID(channel); minID > member.AvailableMinID { member.AvailableMinID = minID member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, minID) } member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, preJoinTopID) if minPts := channelInitialAvailableMinPts(channel); minPts > member.AvailableMinPts { member.AvailableMinPts = minPts } channel.ParticipantsCount++ } else { member.AvailableMinID = channelInitialAvailableMinID(channel) member.AvailableMinPts = channelInitialAvailableMinPts(channel) member.ReadInboxMaxID = maxInt(member.AvailableMinID, preJoinTopID) channel.ParticipantsCount++ } if s.members[channelID] == nil { s.members[channelID] = make(map[int64]domain.ChannelMember) } s.members[channelID][userID] = member s.channels[channelID] = channel s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: channelID, UserID: userID, Date: date, Type: domain.ChannelAdminLogParticipantJoin, }) var msg domain.ChannelMessage var event domain.ChannelUpdateEvent if channel.Megagroup { msg, event = s.appendChannelServiceMessageLocked(channelID, userID, date, domain.ChannelMessageAction{ Type: domain.ChannelActionChatJoined, UserIDs: []int64{userID}, }) channel.TopMessageID = msg.ID channel.Pts = event.Pts s.channels[channelID] = channel } member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, channel.TopMessageID) if msg.ID != 0 && msg.SenderUserID == userID { member.ReadOutboxMaxID = maxInt(member.ReadOutboxMaxID, msg.ID) } s.members[channelID][userID] = member s.upsertChannelDialogLocked(userID, channel, msg, true) return domain.CreateChannelResult{ Channel: channel, Members: []domain.ChannelMember{member}, Message: cloneChannelMessage(msg), Event: cloneChannelEvent(event), Recipients: s.activeMemberIDsLocked(channelID, 0, 0), }, nil } func (s *ChannelStore) LeaveChannel(_ context.Context, channelID, userID int64, date int) (domain.CreateChannelResult, error) { s.mu.Lock() defer s.mu.Unlock() channel, member, err := s.channelAndMemberLocked(userID, channelID) if err != nil { return domain.CreateChannelResult{}, err } members := []domain.ChannelMember{} adminsDelta := 0 if isChannelAdmin(member) { adminsDelta-- } if channel.CreatorUserID == userID || member.Role == domain.ChannelRoleCreator { if channel.CreatorUserID != userID || member.Role != domain.ChannelRoleCreator { return domain.CreateChannelResult{}, domain.ErrChannelUserCreator } future, err := s.futureCreatorAfterLeaveLocked(channelID, userID) if err != nil { return domain.CreateChannelResult{}, domain.ErrChannelUserCreator } if !isChannelAdmin(future) { adminsDelta++ } future.Role = domain.ChannelRoleCreator future.AdminRights = creatorChannelAdminRights() future.Rank = "" future.Status = domain.ChannelMemberActive future.LeftAt = 0 s.members[channelID][future.UserID] = future channel.CreatorUserID = future.UserID members = append(members, future) member.Role = domain.ChannelRoleMember member.AdminRights = domain.ChannelAdminRights{} member.Rank = "" } member.Status = domain.ChannelMemberLeft member.LeftAt = date s.members[channelID][userID] = member members = append([]domain.ChannelMember{member}, members...) s.clearChannelMentionsForUserLocked(channelID, userID) s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: channelID, UserID: userID, Date: date, Type: domain.ChannelAdminLogParticipantLeave, }) if channel.ParticipantsCount > 0 { channel.ParticipantsCount-- } channel.AdminsCount += adminsDelta if channel.AdminsCount < 0 { channel.AdminsCount = 0 } var msg domain.ChannelMessage var event domain.ChannelUpdateEvent if channel.Megagroup { msg, event = s.appendChannelServiceMessageLocked(channelID, userID, date, domain.ChannelMessageAction{ Type: domain.ChannelActionChatDelete, UserIDs: []int64{userID}, }) channel.TopMessageID = msg.ID channel.Pts = event.Pts } s.channels[channelID] = channel return domain.CreateChannelResult{ Channel: channel, Members: cloneChannelMembers(members), Message: cloneChannelMessage(msg), Event: cloneChannelEvent(event), Recipients: append(s.activeMemberIDsLocked(channelID, 0, 0), userID), }, nil } func (s *ChannelStore) EditChannelAdmin(_ context.Context, req domain.EditChannelAdminRequest) (domain.EditChannelAdminResult, error) { if req.UserID == 0 || req.ChannelID == 0 || req.MemberID == 0 { return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.EditChannelAdminResult{}, err } actor := s.members[req.ChannelID][req.UserID] if !canAddChannelAdmins(actor) { return domain.EditChannelAdminResult{}, domain.ErrChannelAdminRequired } previous, ok := s.members[req.ChannelID][req.MemberID] if !ok { previous = domain.ChannelMember{ ChannelID: req.ChannelID, UserID: req.MemberID, InviterUserID: req.UserID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive, JoinedAt: req.Date, AvailableMinID: channelInitialAvailableMinID(channel), AvailableMinPts: channelInitialAvailableMinPts(channel), ReadInboxMaxID: channel.TopMessageID, } } if previous.Role == domain.ChannelRoleCreator { if req.MemberID != req.UserID || channel.CreatorUserID != req.UserID || actor.Role != domain.ChannelRoleCreator { return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator } member := previous member.Status = domain.ChannelMemberActive member.LeftAt = 0 member.AdminRights = domain.CreatorProjectionAdminRights(req.AdminRights) if req.HasRank() { member.Rank = req.Rank } s.members[req.ChannelID][req.MemberID] = member s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: domain.ChannelAdminLogParticipantPromote, PrevParticipant: ptrChannelMember(previous), NewParticipant: ptrChannelMember(member), }) s.refreshChannelCountsLocked(req.ChannelID) channel = s.channels[req.ChannelID] event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date) recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0) recipients = append(recipients, req.MemberID) return domain.EditChannelAdminResult{ Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date, }, nil } if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) { return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden } member := previous member.InviterUserID = req.UserID member.Status = domain.ChannelMemberActive member.LeftAt = 0 if previous.Status != domain.ChannelMemberActive { if minPts := channelInitialAvailableMinPts(channel); minPts > member.AvailableMinPts { member.AvailableMinPts = minPts } } member.AdminRights = domain.NormalizeFullMegagroupAdminRights(channel, req.AdminRights) if req.HasRank() { member.Rank = req.Rank } if zeroChannelAdminRights(req.AdminRights) { member.Role = domain.ChannelRoleMember member.Rank = "" } else { member.Role = domain.ChannelRoleAdmin } s.members[req.ChannelID][req.MemberID] = member logType := domain.ChannelAdminLogParticipantPromote if member.Role != domain.ChannelRoleAdmin { logType = domain.ChannelAdminLogParticipantDemote } s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: logType, PrevParticipant: ptrChannelMember(previous), NewParticipant: ptrChannelMember(member), }) s.refreshChannelCountsLocked(req.ChannelID) channel = s.channels[req.ChannelID] event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date) if msg, ok := s.findMessageLocked(req.ChannelID, channel.TopMessageID); ok { s.upsertChannelDialogLocked(member.UserID, channel, msg, false) } recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0) recipients = append(recipients, req.MemberID) return domain.EditChannelAdminResult{ Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date, }, nil } func (s *ChannelStore) TransferChannelOwnership(_ context.Context, req domain.TransferChannelOwnershipRequest) (domain.TransferChannelOwnershipResult, error) { if req.UserID == 0 || req.ChannelID == 0 || req.NewOwnerID == 0 || req.NewOwnerID == req.UserID { return domain.TransferChannelOwnershipResult{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.TransferChannelOwnershipResult{}, err } previousOwner := s.members[req.ChannelID][req.UserID] if channel.CreatorUserID != req.UserID || previousOwner.Role != domain.ChannelRoleCreator { return domain.TransferChannelOwnershipResult{}, domain.ErrChannelAdminRequired } previousNewOwner, ok := s.members[req.ChannelID][req.NewOwnerID] if !ok || previousNewOwner.Status != domain.ChannelMemberActive || previousNewOwner.BannedRights.ViewMessages { return domain.TransferChannelOwnershipResult{}, domain.ErrUserNotParticipant } if previousNewOwner.Role == domain.ChannelRoleCreator { return domain.TransferChannelOwnershipResult{}, domain.ErrChannelNotModified } oldOwner := previousOwner oldOwner.Role = domain.ChannelRoleAdmin oldOwner.AdminRights = creatorChannelAdminRights() oldOwner.Rank = "" oldOwner.Status = domain.ChannelMemberActive oldOwner.LeftAt = 0 if oldOwner.InviterUserID == 0 { oldOwner.InviterUserID = req.UserID } newOwner := previousNewOwner newOwner.Role = domain.ChannelRoleCreator newOwner.AdminRights = creatorChannelAdminRights() newOwner.Rank = "" newOwner.Status = domain.ChannelMemberActive newOwner.LeftAt = 0 if newOwner.JoinedAt == 0 { newOwner.JoinedAt = req.Date } channel.CreatorUserID = req.NewOwnerID s.channels[req.ChannelID] = channel s.members[req.ChannelID][req.UserID] = oldOwner s.members[req.ChannelID][req.NewOwnerID] = newOwner s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: domain.ChannelAdminLogParticipantPromote, PrevParticipant: ptrChannelMember(previousOwner), NewParticipant: ptrChannelMember(oldOwner), }) s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: domain.ChannelAdminLogParticipantPromote, PrevParticipant: ptrChannelMember(previousNewOwner), NewParticipant: ptrChannelMember(newOwner), }) s.refreshChannelCountsLocked(req.ChannelID) channel = s.channels[req.ChannelID] if msg, ok := s.findMessageLocked(req.ChannelID, channel.TopMessageID); ok { s.upsertChannelDialogLocked(oldOwner.UserID, channel, msg, false) s.upsertChannelDialogLocked(newOwner.UserID, channel, msg, false) } events := []domain.ChannelUpdateEvent{ transientChannelParticipantEvent(channel.ID, req.UserID, previousOwner, oldOwner, req.Date), transientChannelParticipantEvent(channel.ID, req.UserID, previousNewOwner, newOwner, req.Date), } recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0) recipients = append(recipients, req.UserID, req.NewOwnerID) return domain.TransferChannelOwnershipResult{ Channel: channel, PreviousOwner: previousOwner, OldOwner: oldOwner, PreviousNewOwner: previousNewOwner, NewOwner: newOwner, Events: events, Recipients: recipients, Date: req.Date, }, nil } func (s *ChannelStore) EditChannelMemberRank(_ context.Context, req domain.EditChannelMemberRankRequest) (domain.EditChannelAdminResult, error) { if req.UserID == 0 || req.ChannelID == 0 || req.MemberID == 0 { return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.EditChannelAdminResult{}, err } actor := s.members[req.ChannelID][req.UserID] previous, ok := s.members[req.ChannelID][req.MemberID] if !ok || previous.Status != domain.ChannelMemberActive { return domain.EditChannelAdminResult{}, domain.ErrUserNotParticipant } if err := checkEditMemberRank(channel, actor, previous); err != nil { return domain.EditChannelAdminResult{}, err } member := previous member.Rank = req.Rank s.members[req.ChannelID][req.MemberID] = member s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: domain.ChannelAdminLogParticipantEditRank, PrevString: previous.Rank, NewString: member.Rank, Participant: ptrChannelMember(member), }) event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date) recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0) recipients = append(recipients, req.MemberID) return domain.EditChannelAdminResult{ Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date, }, nil } func (s *ChannelStore) EditChannelBanned(_ context.Context, req domain.EditChannelBannedRequest) (domain.EditChannelBannedResult, error) { if req.UserID == 0 || req.ChannelID == 0 || req.Participant.Type != domain.PeerTypeUser || req.Participant.ID == 0 { return domain.EditChannelBannedResult{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.EditChannelBannedResult{}, err } actor := s.members[req.ChannelID][req.UserID] if !canBanChannelUsers(actor) { return domain.EditChannelBannedResult{}, domain.ErrChannelAdminRequired } previous, ok := s.members[req.ChannelID][req.Participant.ID] if !ok { previous = domain.ChannelMember{ChannelID: req.ChannelID, UserID: req.Participant.ID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberLeft} } if previous.Role == domain.ChannelRoleCreator { return domain.EditChannelBannedResult{}, domain.ErrChannelUserCreator } member := previous member.Role = domain.ChannelRoleMember member.BannedRights = req.BannedRights switch { case req.BannedRights.ViewMessages: member.InviterUserID = req.UserID member.Status = domain.ChannelMemberKicked member.LeftAt = req.Date case zeroChannelBannedRights(req.BannedRights): if previous.Status == domain.ChannelMemberActive { member.Status = domain.ChannelMemberActive } else { member.Status = domain.ChannelMemberLeft } member.LeftAt = 0 default: member.InviterUserID = req.UserID if previous.Status == domain.ChannelMemberActive { member.Status = domain.ChannelMemberActive } else { member.Status = domain.ChannelMemberBanned } } if member.JoinedAt == 0 && member.Status == domain.ChannelMemberActive { member.JoinedAt = req.Date } s.members[req.ChannelID][req.Participant.ID] = member s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, UserID: req.UserID, Date: req.Date, Type: adminLogBanType(previous, member), PrevParticipant: ptrChannelMember(previous), NewParticipant: ptrChannelMember(member), }) s.refreshChannelCountsLocked(req.ChannelID) channel = s.channels[req.ChannelID] event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date) if member.Status == domain.ChannelMemberActive { if msg, ok := s.findMessageLocked(req.ChannelID, channel.TopMessageID); ok { s.upsertChannelDialogLocked(member.UserID, channel, msg, false) } } if member.Status == domain.ChannelMemberKicked && previous.Status == domain.ChannelMemberActive { s.clearChannelMentionsForUserLocked(req.ChannelID, req.Participant.ID) } var serviceMsg domain.ChannelMessage var serviceEvent domain.ChannelUpdateEvent if channel.Megagroup && previous.Status == domain.ChannelMemberActive && member.Status == domain.ChannelMemberKicked { serviceMsg, serviceEvent = s.appendChannelServiceMessageLocked(req.ChannelID, req.UserID, req.Date, domain.ChannelMessageAction{ Type: domain.ChannelActionChatDelete, UserIDs: []int64{req.Participant.ID}, }) channel.TopMessageID = serviceMsg.ID channel.Pts = serviceEvent.Pts s.channels[req.ChannelID] = channel } recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0) recipients = append(recipients, req.Participant.ID) return domain.EditChannelBannedResult{ Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date, Message: cloneChannelMessage(serviceMsg), ServiceEvent: cloneChannelEvent(serviceEvent), }, nil } func (s *ChannelStore) EditChannelDefaultBannedRights(_ context.Context, req domain.EditChannelDefaultBannedRightsRequest) (domain.Channel, error) { if req.UserID == 0 || req.ChannelID == 0 { return domain.Channel{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.Channel{}, err } actor := s.members[req.ChannelID][req.UserID] if !canBanChannelUsers(actor) { return domain.Channel{}, domain.ErrChannelAdminRequired } if channel.DefaultBannedRights == req.BannedRights { return domain.Channel{}, domain.ErrChannelNotModified } channel.DefaultBannedRights = req.BannedRights s.channels[req.ChannelID] = channel return channel, nil } func (s *ChannelStore) ListAdminedPublicChannels(_ context.Context, userID int64) ([]domain.Channel, error) { if userID == 0 { return nil, nil } s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.Channel, 0) for channelID, members := range s.members { member := members[userID] if member.Status != domain.ChannelMemberActive || !isChannelAdmin(member) { continue } channel, ok := s.channels[channelID] if !ok || channel.Deleted || channel.Username == "" { continue } out = append(out, channel) } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) if len(out) > domain.MaxAdminedPublicChannels { out = out[:domain.MaxAdminedPublicChannels] } return append([]domain.Channel(nil), out...), nil } func (s *ChannelStore) ListCommunityLinkableChannels(_ context.Context, userID int64) ([]domain.Channel, error) { if userID == 0 { return nil, nil } s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.Channel, 0) for channelID, members := range s.members { member := members[userID] if member.Status != domain.ChannelMemberActive || !isChannelAdmin(member) { continue } channel, ok := s.channels[channelID] if !ok || channel.Deleted || channel.Monoforum || channel.LinkedCommunityID != 0 { continue } out = append(out, channel) } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) if len(out) > domain.MaxCommunityPeers { out = out[:domain.MaxCommunityPeers] } return append([]domain.Channel(nil), out...), nil } func (s *ChannelStore) ListStoryPostableChannels(_ context.Context, userID int64) ([]domain.Channel, error) { if userID == 0 { return nil, nil } s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.Channel, 0) for channelID, members := range s.members { member := members[userID] if member.Status != domain.ChannelMemberActive { continue } if member.Role != domain.ChannelRoleCreator && !(member.Role == domain.ChannelRoleAdmin && member.AdminRights.PostStories) { continue } channel, ok := s.channels[channelID] if !ok || channel.Deleted || (!channel.Broadcast && !channel.Megagroup) { continue } out = append(out, channel) } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) if len(out) > domain.MaxStorySendAsChannels { out = out[:domain.MaxStorySendAsChannels] } return append([]domain.Channel(nil), out...), nil } func (s *ChannelStore) ListSendAsChannels(_ context.Context, userID int64) ([]domain.Channel, error) { if userID == 0 { return nil, nil } s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.Channel, 0) for channelID, members := range s.members { member := members[userID] if member.Status != domain.ChannelMemberActive { continue } if member.Role != domain.ChannelRoleCreator && !(member.Role == domain.ChannelRoleAdmin && member.AdminRights.PostMessages) { continue } channel, ok := s.channels[channelID] if !ok || channel.Deleted || !channel.Broadcast { continue } out = append(out, channel) } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) if len(out) > domain.MaxSendAsChannels { out = out[:domain.MaxSendAsChannels] } return append([]domain.Channel(nil), out...), nil } func (s *ChannelStore) SetParticipantsHidden(_ context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) { if userID == 0 || channelID == 0 { return domain.Channel{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(userID, channelID) if err != nil { return domain.Channel{}, err } member := s.members[channelID][userID] if !channel.Megagroup || !canBanChannelUsers(member) { return domain.Channel{}, domain.ErrChannelAdminRequired } channel.ParticipantsHidden = enabled s.channels[channelID] = channel return channel, nil } func (s *ChannelStore) SetJoinToSend(_ context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) { if userID == 0 || channelID == 0 { return domain.Channel{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(userID, channelID) if err != nil { return domain.Channel{}, err } member := s.members[channelID][userID] if !channel.Megagroup || !canExportChannelInvite(member) { return domain.Channel{}, domain.ErrChannelAdminRequired } channel.JoinToSend = enabled s.channels[channelID] = channel return channel, nil } func (s *ChannelStore) SetJoinRequest(_ context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) { if userID == 0 || channelID == 0 { return domain.Channel{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(userID, channelID) if err != nil { return domain.Channel{}, err } member := s.members[channelID][userID] if !channel.Megagroup || !canExportChannelInvite(member) { return domain.Channel{}, domain.ErrChannelAdminRequired } if enabled && strings.TrimSpace(channel.Username) == "" { return domain.Channel{}, domain.ErrChatPublicRequired } channel.JoinRequest = enabled s.channels[channelID] = channel return channel, nil } func (s *ChannelStore) HideChatJoinRequest(_ context.Context, req domain.HideChannelJoinRequestRequest) (domain.CreateChannelResult, error) { if req.UserID == 0 || req.ChannelID == 0 || req.TargetUserID == 0 { return domain.CreateChannelResult{}, domain.ErrChannelInvalid } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID) if err != nil { return domain.CreateChannelResult{}, err } member := s.members[req.ChannelID][req.UserID] if !canExportChannelInvite(member) { return domain.CreateChannelResult{}, domain.ErrChannelAdminRequired } importer, ok := s.importers[req.ChannelID][req.TargetUserID] if !ok || !importer.Requested { return domain.CreateChannelResult{}, domain.ErrHideRequesterMissing } invite := domain.ChannelInvite{ChannelID: req.ChannelID, AdminUserID: req.UserID} if importer.InviteID != 0 { var err error invite, err = s.inviteByIDLocked(req.ChannelID, importer.InviteID) if err != nil { return domain.CreateChannelResult{}, err } } if !req.Approved { s.deletePendingInviteImporterLocked(invite, req.TargetUserID) return domain.CreateChannelResult{Channel: channel, Recipients: s.activeMemberIDsLocked(req.ChannelID, req.TargetUserID, 0)}, nil } return s.approveInviteImporterLocked(channel, invite, req.TargetUserID, req.UserID, req.Date) } func (s *ChannelStore) recordPublicJoinRequestLocked(channel domain.Channel, userID int64, date int) error { if existing, ok := s.members[channel.ID][userID]; ok { if existing.Status == domain.ChannelMemberActive { return domain.ErrUserAlreadyParticipant } if existing.Status == domain.ChannelMemberKicked || existing.Status == domain.ChannelMemberBanned || existing.BannedRights.ViewMessages { return domain.ErrInviteHashInvalid } } if s.importers[channel.ID] == nil { s.importers[channel.ID] = make(map[int64]domain.ChannelInviteImporter) } if existing, ok := s.importers[channel.ID][userID]; ok && existing.Requested { return domain.ErrInviteRequestSent } s.importers[channel.ID][userID] = domain.ChannelInviteImporter{ ChannelID: channel.ID, UserID: userID, Date: date, Requested: true, } return nil } func (s *ChannelStore) ListActiveChannelMemberIDs(_ context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) { s.mu.RLock() defer s.mu.RUnlock() if _, _, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID); err != nil { return nil, err } return s.activeMemberIDsLocked(channelID, 0, limit), nil } func (s *ChannelStore) FilterActiveChannelMemberIDs(_ context.Context, channelID int64, userIDs []int64) ([]int64, error) { s.mu.RLock() defer s.mu.RUnlock() if channelID == 0 || len(userIDs) == 0 { return nil, nil } members := s.members[channelID] if len(members) == 0 { return nil, nil } out := make([]int64, 0, len(userIDs)) seen := make(map[int64]struct{}, len(userIDs)) for _, userID := range userIDs { if userID == 0 { continue } if _, ok := seen[userID]; ok { continue } seen[userID] = struct{}{} member, ok := members[userID] if !ok || member.Status != domain.ChannelMemberActive { continue } out = append(out, userID) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out, nil } func (s *ChannelStore) ListActiveChannelMembers(_ context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error) { s.mu.RLock() defer s.mu.RUnlock() channel, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID) if err != nil { return domain.Channel{}, domain.ChannelMember{}, nil, err } if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout { limit = domain.MaxSynchronousChannelDialogFanout } members := s.members[channelID] out := make([]domain.ChannelMember, 0, minInt(limit, len(members))) for _, member := range members { if member.Status != domain.ChannelMemberActive { continue } out = append(out, member) if len(out) >= limit { break } } sort.Slice(out, func(i, j int) bool { return out[i].UserID < out[j].UserID }) return channel, viewer, cloneChannelMembers(out), nil } func (s *ChannelStore) channelForMemberLocked(userID, channelID int64) (domain.Channel, error) { channel, _, err := s.channelAndMemberLocked(userID, channelID) return channel, err } func (s *ChannelStore) channelAndMemberLocked(userID, channelID int64) (domain.Channel, domain.ChannelMember, error) { channel, ok := s.channels[channelID] if !ok || channel.Deleted { return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelInvalid } member, ok := s.members[channelID][userID] if !ok || member.Status == domain.ChannelMemberLeft { return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelPrivate } if member.Status == domain.ChannelMemberBanned || member.Status == domain.ChannelMemberKicked || member.BannedRights.ViewMessages { return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelUserBanned } return channel, member, nil } func (s *ChannelStore) channelAndMemberOrLinkedGuestLocked(userID, channelID int64) (domain.Channel, domain.ChannelMember, error) { channel, member, err := s.channelAndMemberLocked(userID, channelID) if !errors.Is(err, domain.ErrChannelPrivate) { return channel, member, err } target, ok := s.channels[channelID] if !ok || target.Deleted { return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelInvalid } guest, allowed, guestErr := s.linkedDiscussionGuestLocked(userID, target) if guestErr != nil { return domain.Channel{}, domain.ChannelMember{}, guestErr } if !allowed { return domain.Channel{}, domain.ChannelMember{}, err } return target, guest, nil } // linkedDiscussionGuestLocked authorizes a discussion-group view through the // viewer's active membership in its bidirectionally linked broadcast. It never // materializes a channel_members row. Explicit target bans always win. func (s *ChannelStore) linkedDiscussionGuestLocked(userID int64, target domain.Channel) (domain.ChannelMember, bool, error) { if target.Broadcast || !target.Megagroup || target.LinkedChatID == 0 { return domain.ChannelMember{}, false, nil } if existing, ok := s.members[target.ID][userID]; ok { if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages { return domain.ChannelMember{}, false, domain.ErrChannelUserBanned } if existing.Status == domain.ChannelMemberActive { return existing, false, nil } } source, ok := s.channels[target.LinkedChatID] if !ok || source.Deleted || !source.Broadcast || source.LinkedChatID != target.ID { return domain.ChannelMember{}, false, nil } sourceMember, ok := s.members[source.ID][userID] if !ok || sourceMember.Status != domain.ChannelMemberActive || sourceMember.BannedRights.ViewMessages { return domain.ChannelMember{}, false, nil } return domain.ChannelMember{ ChannelID: target.ID, UserID: userID, Status: domain.ChannelMemberLeft, Role: domain.ChannelRoleMember, Guest: true, }, true, nil } func (s *ChannelStore) activeMemberIDsLocked(channelID, excludeUserID int64, limit int) []int64 { members := s.members[channelID] if limit <= 0 || limit > domain.MaxChannelRealtimeFanout { limit = domain.MaxChannelRealtimeFanout } capacity := limit if len(members) < capacity { capacity = len(members) } out := make([]int64, 0, capacity) for userID, member := range members { if userID == excludeUserID || member.Status != domain.ChannelMemberActive { continue } out = append(out, userID) if len(out) >= limit { break } } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } func (s *ChannelStore) futureCreatorAfterLeaveLocked(channelID, userID int64) (domain.ChannelMember, error) { members := s.members[channelID] var selected domain.ChannelMember found := false for _, member := range members { if member.UserID == userID || member.Status != domain.ChannelMemberActive || member.Role == domain.ChannelRoleCreator || member.BannedRights.ViewMessages { continue } if !found || futureCreatorCandidateLess(member, selected) { selected = member found = true } } if !found { return domain.ChannelMember{}, domain.ErrUserNotParticipant } return selected, nil } func futureCreatorCandidateLess(a, b domain.ChannelMember) bool { if channelRoleOrder(a.Role) != channelRoleOrder(b.Role) { return channelRoleOrder(a.Role) < channelRoleOrder(b.Role) } return a.UserID < b.UserID } func publicPreviewMember(channel domain.Channel, userID int64, existing domain.ChannelMember, found bool) domain.ChannelMember { member := domain.ChannelMember{ ChannelID: channel.ID, UserID: userID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberLeft, AvailableMinID: channelInitialAvailableMinID(channel), AvailableMinPts: channelInitialAvailableMinPts(channel), ReadInboxMaxID: channel.TopMessageID, ReadOutboxMaxID: channel.TopMessageID, } if found { member.InviterUserID = existing.InviterUserID member.JoinedAt = existing.JoinedAt member.LeftAt = existing.LeftAt member.AvailableMinID = maxInt(member.AvailableMinID, existing.AvailableMinID) member.AvailableMinPts = maxInt(member.AvailableMinPts, existing.AvailableMinPts) member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, existing.ReadInboxMaxID) member.ReadOutboxMaxID = maxInt(member.ReadOutboxMaxID, existing.ReadOutboxMaxID) } return member } func syntheticMonoforumAdminMember(mono domain.Channel, parentMember domain.ChannelMember) domain.ChannelMember { member := parentMember member.ChannelID = mono.ID member.Status = domain.ChannelMemberActive if mono.CreatorUserID == parentMember.UserID { member.Role = domain.ChannelRoleCreator } else { member.Role = domain.ChannelRoleAdmin } member.AvailableMinID = 0 member.AvailableMinPts = 0 member.ReadInboxMaxID = mono.TopMessageID member.ReadOutboxMaxID = mono.TopMessageID member.UnreadMark = false member.SlowmodeLastSendDate = 0 return member } func syntheticMonoforumUserMember(mono domain.Channel, userID int64) domain.ChannelMember { return domain.ChannelMember{ ChannelID: mono.ID, UserID: userID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive, } } func publicChannelSearchRank(channel domain.Channel, queryLower string) (int, bool) { if !publicSearchableChannel(channel) { return 0, false } username := strings.ToLower(strings.TrimSpace(channel.Username)) title := strings.ToLower(strings.TrimSpace(channel.Title)) switch { case username == queryLower: return 0, true case strings.HasPrefix(username, queryLower): return 1, true case strings.Contains(username, queryLower): return 2, true case strings.HasPrefix(title, queryLower): return 3, true case strings.Contains(title, queryLower): return 4, true default: return 0, false } } func canPostToBroadcast(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PostMessages) } func isChannelAdmin(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin } func shouldHideAnonymousAdminFromParticipantList(viewer, member domain.ChannelMember) bool { if isChannelAdmin(viewer) { return false } if member.Role != domain.ChannelRoleCreator && member.Role != domain.ChannelRoleAdmin { return false } return member.AdminRights.Anonymous } func channelParticipantMatchesFilter(member domain.ChannelMember, kind domain.ChannelParticipantsFilterKind, query string) bool { if query != "" && !strings.Contains(strconv.FormatInt(member.UserID, 10), query) { return false } switch kind { case "", domain.ChannelParticipantsRecent, domain.ChannelParticipantsContacts, domain.ChannelParticipantsMentions, domain.ChannelParticipantsSearch: return member.Status == domain.ChannelMemberActive case domain.ChannelParticipantsAdmins: // Layer 225 起 admins filter 同时是消息徽章数据源:客户端把整个返回 // (含 rank)灌进 badge 缓存,因此带成员 Tag 的普通成员也必须返回。 return member.Status == domain.ChannelMemberActive && (isChannelAdmin(member) || member.Rank != "") case domain.ChannelParticipantsKicked: return member.Status == domain.ChannelMemberKicked || member.BannedRights.ViewMessages case domain.ChannelParticipantsBanned: return member.Status != domain.ChannelMemberKicked && !zeroChannelBannedRights(member.BannedRights) case domain.ChannelParticipantsBots: return false default: return member.Status == domain.ChannelMemberActive } } func canManageDiscussionBroadcast(member domain.ChannelMember) bool { return canChangeChannelInfo(member) } func canManageDiscussionGroup(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PinMessages) } func canAddChannelAdmins(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.AddAdmins) } func canBanChannelUsers(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.BanUsers) } func zeroChannelAdminRights(rights domain.ChannelAdminRights) bool { return rights == domain.ChannelAdminRights{} } func zeroChannelBannedRights(rights domain.ChannelBannedRights) bool { return rights == domain.ChannelBannedRights{} } func creatorChannelAdminRights() domain.ChannelAdminRights { return domain.CreatorChannelAdminRights() } func cloneChannelMembers(in []domain.ChannelMember) []domain.ChannelMember { return append([]domain.ChannelMember(nil), in...) } func ptrChannelMember(in domain.ChannelMember) *domain.ChannelMember { out := in return &out }