chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,992 @@
package memory
import (
"context"
"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, err := s.channelForMemberLocked(viewerUserID, channelID)
if err != nil {
return domain.ChannelParticipantList{}, err
}
if limit <= 0 || limit > domain.MaxChannelParticipantsLimit {
limit = domain.MaxChannelParticipantsLimit
}
viewer := s.members[channelID][viewerUserID]
// 广播频道订阅者列表仅管理员可枚举与隐藏成员同一门控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()
if _, err := s.channelForMemberLocked(viewerUserID, channelID); err != nil {
return domain.ChannelMember{}, err
}
member, ok := s.members[channelID][participantUserID]
if !ok {
return domain.ChannelMember{}, domain.ErrChannelPrivate
}
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
}
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不保留旧的角色/管理权/Taginviter 重置为本人(自加入)。
// 只有 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
}
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
}
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 {
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
}
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 = req.AdminRights
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) 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) 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.channelForMemberLocked(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.channelAndMemberLocked(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) 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 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.ChannelAdminRights{
ChangeInfo: true,
PostMessages: true,
EditMessages: true,
DeleteMessages: true,
PostStories: true,
EditStories: true,
DeleteStories: true,
BanUsers: true,
InviteUsers: true,
PinMessages: true,
AddAdmins: true,
ManageCall: true,
}
}
func cloneChannelMembers(in []domain.ChannelMember) []domain.ChannelMember {
return append([]domain.ChannelMember(nil), in...)
}
func ptrChannelMember(in domain.ChannelMember) *domain.ChannelMember {
out := in
return &out
}