chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
992
internal/store/memory/channel_members.go
Normal file
992
internal/store/memory/channel_members.go
Normal 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:不保留旧的角色/管理权/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
|
||||
}
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue