package memory import ( "context" "encoding/base64" "errors" "hash/fnv" "sort" "strconv" "strings" "sync" "telesrv/internal/domain" ) type CommunityStore struct { mu sync.RWMutex users *UserStore channels *ChannelStore bots *BotStore dialogs *DialogStore nextID int64 nextHash int64 communities map[int64]domain.Community members map[int64]map[int64]domain.CommunityMember links map[int64]map[domain.Peer]domain.CommunityPeerLink requests map[int64]map[domain.Peer]domain.CommunityPeerLinkRequest states map[int64]map[int64]domain.CommunityUserState } func NewCommunityStore(users *UserStore, channels *ChannelStore, bots *BotStore, dialogs *DialogStore) *CommunityStore { return &CommunityStore{ users: users, channels: channels, bots: bots, dialogs: dialogs, nextID: 3000000000, nextHash: 990000000000, communities: map[int64]domain.Community{}, members: map[int64]map[int64]domain.CommunityMember{}, links: map[int64]map[domain.Peer]domain.CommunityPeerLink{}, requests: map[int64]map[domain.Peer]domain.CommunityPeerLinkRequest{}, states: map[int64]map[int64]domain.CommunityUserState{}, } } func cloneCommunity(c domain.Community) domain.Community { c.PhotoStripped = append([]byte(nil), c.PhotoStripped...) return c } func cloneCommunityView(v domain.CommunityView) domain.CommunityView { v.Community = cloneCommunity(v.Community) v.Links = append([]domain.CommunityPeerLink(nil), v.Links...) v.ServiceMessages = append([]domain.SendChannelMessageResult(nil), v.ServiceMessages...) return v } func (s *CommunityStore) communityLocked(id int64) (domain.Community, error) { c, ok := s.communities[id] if !ok || c.Deleted { return domain.Community{}, domain.ErrCommunityInvalid } return c, nil } func (s *CommunityStore) derivedMemberLocked(c domain.Community, userID int64) (domain.CommunityMember, bool) { if m, ok := s.members[c.ID][userID]; ok { return m, true } if s.channels != nil { s.channels.mu.RLock() for p := range s.links[c.ID] { if p.Type != domain.PeerTypeChannel { continue } if m, ok := s.channels.members[p.ID][userID]; ok && m.Status == domain.ChannelMemberActive { s.channels.mu.RUnlock() return domain.CommunityMember{CommunityID: c.ID, UserID: userID, Role: domain.CommunityRoleMember, Status: domain.CommunityMemberActive, Date: c.Date}, true } } s.channels.mu.RUnlock() } if s.dialogs != nil { s.dialogs.mu.RLock() for p := range s.links[c.ID] { if p.Type != domain.PeerTypeUser { continue } for _, d := range s.dialogs.m[userID].Dialogs { if d.Peer == p && d.TopMessage > 0 { s.dialogs.mu.RUnlock() return domain.CommunityMember{CommunityID: c.ID, UserID: userID, Role: domain.CommunityRoleMember, Status: domain.CommunityMemberActive, Date: c.Date}, true } } } s.dialogs.mu.RUnlock() } return domain.CommunityMember{}, false } func (s *CommunityStore) viewLocked(userID, id int64) (domain.CommunityView, error) { c, e := s.communityLocked(id) if e != nil { return domain.CommunityView{}, e } m, ok := s.derivedMemberLocked(c, userID) if !ok || !m.Active() { return domain.CommunityView{Community: cloneCommunity(c), Self: m, Forbidden: true}, domain.ErrCommunityPrivate } v := domain.CommunityView{Community: cloneCommunity(c), Self: m, State: s.states[id][userID]} v.State.CommunityID = id v.State.UserID = userID for _, l := range s.links[id] { joined := false inherentlyViewable := l.Peer.Type == domain.PeerTypeUser if l.Peer.Type == domain.PeerTypeChannel && s.channels != nil { s.channels.mu.RLock() cm, ok := s.channels.members[l.Peer.ID][userID] joined = ok && cm.Status == domain.ChannelMemberActive if channel, ok := s.channels.channels[l.Peer.ID]; ok { inherentlyViewable = s.channels.publicPreviewableChannelLocked(channel) } s.channels.mu.RUnlock() } else if l.Peer.Type == domain.PeerTypeUser && s.dialogs != nil { s.dialogs.mu.RLock() for _, d := range s.dialogs.m[userID].Dialogs { if d.Peer == l.Peer && d.TopMessage > 0 { joined = true break } } s.dialogs.mu.RUnlock() } if l.Visibility == domain.CommunityPeerHidden && !joined && !m.CanManageLinkedPeers() { continue } // Community administration reveals hidden links but never grants access // to a linked private channel's history. TDesktop uses this bit to decide // between opening History directly and showing the join prompt. l.CanViewHistory = joined || inherentlyViewable v.Links = append(v.Links, l) } if s.channels != nil { s.channels.mu.RLock() for _, l := range v.Links { if l.Peer.Type == domain.PeerTypeChannel { if ch, ok := s.channels.channels[l.Peer.ID]; ok { v.Channels = append(v.Channels, cloneChannel(ch)) } } } s.channels.mu.RUnlock() } if s.users != nil { s.users.mu.RLock() for _, l := range v.Links { if l.Peer.Type == domain.PeerTypeUser { if u, ok := s.users.byID[l.Peer.ID]; ok { v.Users = append(v.Users, u) } } } s.users.mu.RUnlock() } for _, cm := range s.members[id] { if cm.Status == domain.CommunityMemberKicked { v.KickedCount++ } else if cm.Role == domain.CommunityRoleCreator || cm.Role == domain.CommunityRoleAdmin { v.AdminsCount++ } } v.PendingRequests = len(s.requests[id]) sort.Slice(v.Links, func(i, j int) bool { if v.Links[i].Date != v.Links[j].Date { return v.Links[i].Date < v.Links[j].Date } if v.Links[i].Peer.Type != v.Links[j].Peer.Type { return v.Links[i].Peer.Type < v.Links[j].Peer.Type } return v.Links[i].Peer.ID < v.Links[j].Peer.ID }) return v, nil } func (s *CommunityStore) GetCommunity(_ context.Context, userID, id int64) (domain.CommunityView, error) { s.mu.RLock() defer s.mu.RUnlock() return s.viewLocked(userID, id) } func (s *CommunityStore) GetCommunities(ctx context.Context, userID int64, ids []int64) ([]domain.CommunityView, error) { s.mu.RLock() defer s.mu.RUnlock() out := []domain.CommunityView{} seen := map[int64]struct{}{} for _, id := range ids { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} v, e := s.viewLocked(userID, id) if errors.Is(e, domain.ErrCommunityInvalid) || errors.Is(e, domain.ErrCommunityPrivate) { continue } if e != nil { return nil, e } out = append(out, v) } return out, nil } func (s *CommunityStore) ListJoinedCommunities(ctx context.Context, userID int64) ([]domain.CommunityView, error) { s.mu.RLock() ids := make([]int64, 0, len(s.communities)) for id := range s.communities { ids = append(ids, id) } s.mu.RUnlock() sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) return s.GetCommunities(ctx, userID, ids) } func (s *CommunityStore) validatePeerLocked(actor int64, p domain.Peer) error { for _, byPeer := range s.links { if _, ok := byPeer[p]; ok { return domain.ErrCommunityPeerLinked } } switch p.Type { case domain.PeerTypeChannel: if s.channels == nil { return domain.ErrCommunityPeerInvalid } s.channels.mu.RLock() defer s.channels.mu.RUnlock() ch, ok := s.channels.channels[p.ID] if !ok || ch.Deleted || ch.Monoforum || ch.LinkedCommunityID != 0 { return domain.ErrCommunityPeerInvalid } m, ok := s.channels.members[p.ID][actor] if !ok || m.Status != domain.ChannelMemberActive || (m.Role != domain.ChannelRoleCreator && m.Role != domain.ChannelRoleAdmin) { return domain.ErrCommunityAdminRequired } case domain.PeerTypeUser: if s.users == nil || s.bots == nil { return domain.ErrCommunityPeerInvalid } s.users.mu.RLock() u, ok := s.users.byID[p.ID] s.users.mu.RUnlock() if !ok || !u.Bot || u.Deleted || u.LinkedCommunityID != 0 { return domain.ErrCommunityPeerInvalid } s.bots.mu.RLock() profile, ok := s.bots.byID[p.ID] s.bots.mu.RUnlock() if !ok || profile.OwnerUserID != actor { return domain.ErrCommunityAdminRequired } default: return domain.ErrCommunityPeerInvalid } return nil } func (s *CommunityStore) setPeerLinkLocked(p domain.Peer, id int64) error { switch p.Type { case domain.PeerTypeChannel: s.channels.mu.Lock() ch, ok := s.channels.channels[p.ID] if ok { ch.LinkedCommunityID = id s.channels.channels[p.ID] = ch } s.channels.mu.Unlock() if !ok { return domain.ErrCommunityPeerInvalid } case domain.PeerTypeUser: s.users.mu.Lock() u, ok := s.users.byID[p.ID] if ok { u.LinkedCommunityID = id s.users.byID[p.ID] = u } s.users.mu.Unlock() if !ok { return domain.ErrCommunityPeerInvalid } default: return domain.ErrCommunityPeerInvalid } return nil } func (s *CommunityStore) insertLinkLocked(id, actor int64, p domain.Peer, v domain.CommunityPeerVisibility, date int) (domain.CommunityPeerLink, error) { if e := s.validatePeerLocked(actor, p); e != nil { return domain.CommunityPeerLink{}, e } channels, bots := 0, 0 for peer := range s.links[id] { if peer.Type == domain.PeerTypeChannel { channels++ } else { bots++ } } if (p.Type == domain.PeerTypeChannel && channels >= domain.MaxCommunityPeers) || (p.Type == domain.PeerTypeUser && bots >= domain.MaxCommunityBotPeers) { return domain.CommunityPeerLink{}, domain.ErrCommunityPeersTooMuch } l := domain.CommunityPeerLink{CommunityID: id, Peer: p, Visibility: v, CanViewHistory: true, CreatedBy: actor, Date: date} if s.links[id] == nil { s.links[id] = map[domain.Peer]domain.CommunityPeerLink{} } s.links[id][p] = l if e := s.setPeerLinkLocked(p, id); e != nil { delete(s.links[id], p) return domain.CommunityPeerLink{}, e } return l, nil } func (s *CommunityStore) unlinkLocked(id int64, peer domain.Peer) { delete(s.links[id], peer) _ = s.setPeerLinkLocked(peer, 0) } func (s *CommunityStore) CreateCommunity(_ context.Context, req domain.CreateCommunityRequest) (domain.CommunityView, error) { s.mu.Lock() defer s.mu.Unlock() if e := s.validatePeerLocked(req.CreatorUserID, req.InitialPeer); e != nil { return domain.CommunityView{}, e } id := s.nextID s.nextID++ if s.channels != nil { s.channels.mu.Lock() if id < s.channels.nextID { id = s.channels.nextID } s.channels.nextID = id + 1 s.channels.mu.Unlock() } hash := s.nextHash s.nextHash++ c := domain.Community{ID: id, AccessHash: hash, CreatorUserID: req.CreatorUserID, Title: req.Title, About: req.About, Date: req.Date} s.communities[id] = c s.members[id] = map[int64]domain.CommunityMember{req.CreatorUserID: {CommunityID: id, UserID: req.CreatorUserID, Role: domain.CommunityRoleCreator, Status: domain.CommunityMemberActive, AdminRights: domain.CreatorChannelAdminRights(), Date: req.Date}} s.links[id] = map[domain.Peer]domain.CommunityPeerLink{} s.requests[id] = map[domain.Peer]domain.CommunityPeerLinkRequest{} s.states[id] = map[int64]domain.CommunityUserState{} l, e := s.insertLinkLocked(id, req.CreatorUserID, req.InitialPeer, req.Visibility, req.Date) if e != nil { delete(s.communities, id) return domain.CommunityView{}, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(req.InitialPeer, req.CreatorUserID, req.Date, c.ID) if e != nil { s.unlinkLocked(id, req.InitialPeer) delete(s.communities, id) return domain.CommunityView{}, e } view := domain.CommunityView{Community: c, Self: s.members[id][req.CreatorUserID], Links: []domain.CommunityPeerLink{l}, AdminsCount: 1} if serviceMessage != nil { view.ServiceMessages = append(view.ServiceMessages, *serviceMessage) } return view, nil } func (s *CommunityStore) actorLocked(id, user int64) (domain.Community, domain.CommunityMember, error) { c, e := s.communityLocked(id) if e != nil { return domain.Community{}, domain.CommunityMember{}, e } m, ok := s.derivedMemberLocked(c, user) if !ok || !m.Active() { return domain.Community{}, domain.CommunityMember{}, domain.ErrCommunityPrivate } return c, m, nil } func (s *CommunityStore) ToggleCommunityPeerLink(_ context.Context, req domain.CommunityTogglePeerLinkRequest) (domain.CommunityTogglePeerLinkResult, error) { s.mu.Lock() defer s.mu.Unlock() c, m, e := s.actorLocked(req.CommunityID, req.ActorUserID) if e != nil { return domain.CommunityTogglePeerLinkResult{}, e } if req.Deleted { if !m.CanManageLinkedPeers() { return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityAdminRequired } if _, ok := s.links[c.ID][req.Peer]; !ok { return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityPeerInvalid } if e := s.setPeerLinkLocked(req.Peer, 0); e != nil { return domain.CommunityTogglePeerLinkResult{}, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(req.Peer, req.ActorUserID, req.Date, 0) if e != nil { _ = s.setPeerLinkLocked(req.Peer, c.ID) return domain.CommunityTogglePeerLinkResult{}, e } delete(s.links[c.ID], req.Peer) return domain.CommunityTogglePeerLinkResult{Community: c, Peer: req.Peer, ServiceMessage: serviceMessage, Removed: true}, nil } if m.CanManageLinkedPeers() { l, e := s.insertLinkLocked(c.ID, req.ActorUserID, req.Peer, req.Visibility, req.Date) if e != nil { return domain.CommunityTogglePeerLinkResult{}, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(req.Peer, req.ActorUserID, req.Date, c.ID) if e != nil { s.unlinkLocked(c.ID, req.Peer) return domain.CommunityTogglePeerLinkResult{}, e } return domain.CommunityTogglePeerLinkResult{Community: c, Peer: req.Peer, Link: &l, ServiceMessage: serviceMessage}, nil } if c.DefaultBannedRights.ManageLinkedPeers { return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityAdminRequired } if e := s.validatePeerLocked(req.ActorUserID, req.Peer); e != nil { return domain.CommunityTogglePeerLinkResult{}, e } s.requests[c.ID][req.Peer] = domain.CommunityPeerLinkRequest{CommunityID: c.ID, Peer: req.Peer, RequestedBy: req.ActorUserID, Visibility: req.Visibility, Date: req.Date} return domain.CommunityTogglePeerLinkResult{Community: c, Peer: req.Peer, RequestCreated: true}, nil } func (s *CommunityStore) appendCommunityServiceMessageLocked(peer domain.Peer, actorUserID int64, date int, communityID int64) (*domain.SendChannelMessageResult, error) { if peer.Type != domain.PeerTypeChannel || s.channels == nil { return nil, nil } s.channels.mu.Lock() defer s.channels.mu.Unlock() channel, ok := s.channels.channels[peer.ID] if !ok || channel.Deleted { return nil, domain.ErrChannelInvalid } message, event := s.channels.appendChannelServiceMessageLocked(peer.ID, actorUserID, date, domain.ChannelMessageAction{ Type: domain.ChannelActionChangeCommunity, CommunityID: communityID, }) channel = s.channels.channels[peer.ID] channel.TopMessageID = message.ID channel.Pts = event.Pts s.channels.channels[peer.ID] = channel return &domain.SendChannelMessageResult{ Channel: channel, Message: message, Event: event, Recipients: s.channels.activeMemberIDsLocked(peer.ID, 0, 0), }, nil } func (s *CommunityStore) SetCommunityCollapsed(_ context.Context, user, id int64, collapsed bool) (domain.CommunityView, bool, error) { s.mu.Lock() c, _, e := s.actorLocked(id, user) if e != nil { s.mu.Unlock() return domain.CommunityView{}, false, e } state := s.states[id][user] changed := state.Collapsed != collapsed state.CommunityID, state.UserID, state.Collapsed = id, user, collapsed if !collapsed { state.Pinned = false state.PinnedOrder = 0 } s.states[id][user] = state v, e := s.viewLocked(user, c.ID) s.mu.Unlock() return v, changed, e } func encodeMemoryCommunityOffset(n int) string { return base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(n))) } func decodeMemoryCommunityOffset(raw string) (int, error) { if raw == "" { return 0, nil } b, e := base64.RawURLEncoding.DecodeString(raw) if e != nil { return 0, domain.ErrCommunityInvalid } n, e := strconv.Atoi(string(b)) if e != nil || n < 0 { return 0, domain.ErrCommunityInvalid } return n, nil } func (s *CommunityStore) ListCommunityPeerLinkRequests(_ context.Context, user, id int64, offset string, limit int) (domain.CommunityPeerLinkRequestPage, error) { s.mu.RLock() defer s.mu.RUnlock() _, m, e := s.actorLocked(id, user) if e != nil { return domain.CommunityPeerLinkRequestPage{}, e } if !m.CanManageLinkedPeers() { return domain.CommunityPeerLinkRequestPage{}, domain.ErrCommunityAdminRequired } start, e := decodeMemoryCommunityOffset(offset) if e != nil { return domain.CommunityPeerLinkRequestPage{}, e } items := make([]domain.CommunityPeerLinkRequest, 0, len(s.requests[id])) for _, r := range s.requests[id] { items = append(items, r) } sort.Slice(items, func(i, j int) bool { if items[i].Date != items[j].Date { return items[i].Date > items[j].Date } if items[i].Peer.Type != items[j].Peer.Type { return items[i].Peer.Type > items[j].Peer.Type } return items[i].Peer.ID > items[j].Peer.ID }) page := domain.CommunityPeerLinkRequestPage{TotalCount: len(items)} if start > len(items) { start = len(items) } end := start + limit if end > len(items) { end = len(items) } page.Requests = append(page.Requests, items[start:end]...) if end < len(items) { page.NextOffset = encodeMemoryCommunityOffset(end) } if s.channels != nil { s.channels.mu.RLock() for _, r := range page.Requests { if r.Peer.Type == domain.PeerTypeChannel { if ch, ok := s.channels.channels[r.Peer.ID]; ok { page.Channels = append(page.Channels, cloneChannel(ch)) } } } s.channels.mu.RUnlock() } if s.users != nil { s.users.mu.RLock() seen := map[int64]struct{}{} for _, r := range page.Requests { ids := []int64{r.RequestedBy} if r.Peer.Type == domain.PeerTypeUser { ids = append(ids, r.Peer.ID) } for _, uid := range ids { if _, ok := seen[uid]; ok { continue } seen[uid] = struct{}{} if u, ok := s.users.byID[uid]; ok { page.Users = append(page.Users, u) } } } s.users.mu.RUnlock() } return page, nil } func (s *CommunityStore) decideLocked(actor, id int64, p domain.Peer, reject bool, date int) (domain.CommunityTogglePeerLinkResult, error) { c, m, e := s.actorLocked(id, actor) if e != nil { return domain.CommunityTogglePeerLinkResult{}, e } if !m.CanManageLinkedPeers() { return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityAdminRequired } r, ok := s.requests[id][p] if !ok { return domain.CommunityTogglePeerLinkResult{}, domain.ErrCommunityRequestMissing } delete(s.requests[id], p) if reject { return domain.CommunityTogglePeerLinkResult{Community: c, Peer: p, RequestedBy: r.RequestedBy}, nil } l, e := s.insertLinkLocked(id, r.RequestedBy, p, r.Visibility, date) if e != nil { s.requests[id][p] = r return domain.CommunityTogglePeerLinkResult{}, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(p, actor, date, c.ID) if e != nil { s.unlinkLocked(id, p) return domain.CommunityTogglePeerLinkResult{}, e } return domain.CommunityTogglePeerLinkResult{Community: c, Peer: p, RequestedBy: r.RequestedBy, Link: &l, ServiceMessage: serviceMessage}, nil } func (s *CommunityStore) DecideCommunityPeerLinkRequest(_ context.Context, actor, id int64, p domain.Peer, reject bool, date int) (domain.CommunityTogglePeerLinkResult, error) { s.mu.Lock() defer s.mu.Unlock() return s.decideLocked(actor, id, p, reject, date) } func (s *CommunityStore) DecideAllCommunityPeerLinkRequests(_ context.Context, actor, id int64, reject bool, date int) ([]domain.CommunityTogglePeerLinkResult, error) { s.mu.Lock() defer s.mu.Unlock() _, m, e := s.actorLocked(id, actor) if e != nil { return nil, e } if !m.CanManageLinkedPeers() { return nil, domain.ErrCommunityAdminRequired } peers := make([]domain.Peer, 0, len(s.requests[id])) for p := range s.requests[id] { peers = append(peers, p) } sort.Slice(peers, func(i, j int) bool { if peers[i].Type != peers[j].Type { return peers[i].Type < peers[j].Type } return peers[i].ID < peers[j].ID }) if reject { s.requests[id] = map[domain.Peer]domain.CommunityPeerLinkRequest{} return make([]domain.CommunityTogglePeerLinkResult, len(peers)), nil } channels, bots := 0, 0 for p := range s.links[id] { if p.Type == domain.PeerTypeChannel { channels++ } else { bots++ } } for _, p := range peers { if p.Type == domain.PeerTypeChannel { channels++ } else { bots++ } if channels > domain.MaxCommunityPeers || bots > domain.MaxCommunityBotPeers { return nil, domain.ErrCommunityPeersTooMuch } r := s.requests[id][p] if e := s.validatePeerLocked(r.RequestedBy, p); e != nil { return nil, e } } out := []domain.CommunityTogglePeerLinkResult{} for _, p := range peers { r := s.requests[id][p] l, e := s.insertLinkLocked(id, r.RequestedBy, p, r.Visibility, date) if e != nil { return nil, e } delete(s.requests[id], p) serviceMessage, e := s.appendCommunityServiceMessageLocked(p, actor, date, s.communities[id].ID) if e != nil { s.unlinkLocked(id, p) return nil, e } out = append(out, domain.CommunityTogglePeerLinkResult{Community: s.communities[id], Peer: p, RequestedBy: r.RequestedBy, Link: &l, ServiceMessage: serviceMessage}) } return out, nil } func (s *CommunityStore) GetCommunityParticipantJoinedChats(_ context.Context, user, id, participant int64) (domain.CommunityParticipantJoinedChats, error) { s.mu.RLock() defer s.mu.RUnlock() _, m, e := s.actorLocked(id, user) if e != nil { return domain.CommunityParticipantJoinedChats{}, e } if !m.CanBanUsers() && user != participant { return domain.CommunityParticipantJoinedChats{}, domain.ErrCommunityAdminRequired } out := domain.CommunityParticipantJoinedChats{} if s.channels != nil { s.channels.mu.RLock() for p := range s.links[id] { if p.Type != domain.PeerTypeChannel { continue } cm, ok := s.channels.members[p.ID][participant] if !ok || cm.Status != domain.ChannelMemberActive { continue } out.JoinedChatIDs = append(out.JoinedChatIDs, p.ID) if cm.Role == domain.ChannelRoleCreator { out.CreatorChatIDs = append(out.CreatorChatIDs, p.ID) } if ch, ok := s.channels.channels[p.ID]; ok { out.Channels = append(out.Channels, cloneChannel(ch)) } } s.channels.mu.RUnlock() } if s.users != nil { s.users.mu.RLock() if participantUser, ok := s.users.byID[participant]; ok { out.Users = append(out.Users, participantUser) } s.users.mu.RUnlock() } sort.Slice(out.JoinedChatIDs, func(i, j int) bool { return out.JoinedChatIDs[i] < out.JoinedChatIDs[j] }) sort.Slice(out.CreatorChatIDs, func(i, j int) bool { return out.CreatorChatIDs[i] < out.CreatorChatIDs[j] }) return out, nil } func (s *CommunityStore) ToggleCommunityParticipantBanned(_ context.Context, actor, id, participant int64, unban bool, date int) (domain.CommunityParticipantBanResult, error) { s.mu.Lock() defer s.mu.Unlock() c, m, e := s.actorLocked(id, actor) if e != nil { return domain.CommunityParticipantBanResult{}, e } if !m.CanBanUsers() || participant == c.CreatorUserID { return domain.CommunityParticipantBanResult{}, domain.ErrCommunityAdminRequired } if unban { old, ok := s.members[id][participant] if ok && old.Role == domain.CommunityRoleMember && old.Status == domain.CommunityMemberKicked { delete(s.members[id], participant) return domain.CommunityParticipantBanResult{Changed: true}, nil } return domain.CommunityParticipantBanResult{}, nil } if participantMember, ok := s.derivedMemberLocked(c, participant); !ok || (participantMember.Status != domain.CommunityMemberActive && participantMember.Status != domain.CommunityMemberKicked) { return domain.CommunityParticipantBanResult{}, domain.ErrCommunityParticipantInvalid } old, alreadyKicked := s.members[id][participant] alreadyKicked = alreadyKicked && old.Role == domain.CommunityRoleMember && old.Status == domain.CommunityMemberKicked result := domain.CommunityParticipantBanResult{} for p := range s.links[id] { owned := false if p.Type == domain.PeerTypeChannel && s.channels != nil { s.channels.mu.Lock() ch := s.channels.channels[p.ID] owned = ch.CreatorUserID == participant if !owned { if cm, ok := s.channels.members[p.ID][participant]; ok && cm.Status == domain.ChannelMemberActive { previous := cm cm.Status = domain.ChannelMemberKicked cm.Role = domain.ChannelRoleMember cm.InviterUserID = actor cm.LeftAt = date cm.BannedRights = domain.ChannelBannedRights{ViewMessages: true} s.channels.members[p.ID][participant] = cm ch.ParticipantsCount-- ch.KickedCount++ s.channels.channels[p.ID] = ch event := transientChannelParticipantEvent(ch.ID, actor, previous, cm, date) var serviceMessage domain.ChannelMessage var serviceEvent domain.ChannelUpdateEvent if ch.Megagroup { serviceMessage, serviceEvent = s.channels.appendChannelServiceMessageLocked(ch.ID, actor, date, domain.ChannelMessageAction{Type: domain.ChannelActionChatDelete, UserIDs: []int64{participant}}) ch = s.channels.channels[p.ID] ch.TopMessageID, ch.Pts = serviceMessage.ID, serviceEvent.Pts s.channels.channels[p.ID] = ch } recipients := s.channels.activeMemberIDsLocked(p.ID, 0, 0) recipients = append(recipients, participant) result.ChannelBans = append(result.ChannelBans, domain.EditChannelBannedResult{ Channel: ch, Previous: previous, Participant: cm, Event: event, Recipients: recipients, Date: date, Message: serviceMessage, ServiceEvent: serviceEvent, }) } } s.channels.mu.Unlock() } else if p.Type == domain.PeerTypeUser && s.bots != nil { s.bots.mu.RLock() owned = s.bots.byID[p.ID].OwnerUserID == participant s.bots.mu.RUnlock() } if owned { if e := s.setPeerLinkLocked(p, 0); e != nil { return domain.CommunityParticipantBanResult{}, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(p, actor, date, 0) if e != nil { _ = s.setPeerLinkLocked(p, c.ID) return domain.CommunityParticipantBanResult{}, e } delete(s.links[id], p) result.RemovedLinks = append(result.RemovedLinks, domain.CommunityTogglePeerLinkResult{Community: c, Peer: p, Removed: true, ServiceMessage: serviceMessage}) } } if !alreadyKicked { s.members[id][participant] = domain.CommunityMember{CommunityID: id, UserID: participant, Role: domain.CommunityRoleMember, Status: domain.CommunityMemberKicked, Date: date} } result.Changed = !alreadyKicked || len(result.ChannelBans) > 0 || len(result.RemovedLinks) > 0 return result, nil } func (s *CommunityStore) allParticipantsLocked(id int64) []domain.CommunityMember { byID := map[int64]domain.CommunityMember{} for uid, m := range s.members[id] { byID[uid] = m } if s.channels != nil { s.channels.mu.RLock() for p := range s.links[id] { if p.Type != domain.PeerTypeChannel { continue } for uid, cm := range s.channels.members[p.ID] { if cm.Status == domain.ChannelMemberActive { if _, ok := byID[uid]; !ok { byID[uid] = domain.CommunityMember{CommunityID: id, UserID: uid, Role: domain.CommunityRoleMember, Status: domain.CommunityMemberActive} } } } } s.channels.mu.RUnlock() } out := make([]domain.CommunityMember, 0, len(byID)) for _, m := range byID { out = append(out, m) } sort.Slice(out, func(i, j int) bool { rank := func(r domain.CommunityMemberRole) int { if r == domain.CommunityRoleCreator { return 0 } if r == domain.CommunityRoleAdmin { return 1 } return 2 } if rank(out[i].Role) != rank(out[j].Role) { return rank(out[i].Role) < rank(out[j].Role) } return out[i].UserID < out[j].UserID }) return out } func (s *CommunityStore) ListCommunityParticipants(_ context.Context, user, id int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.CommunityParticipantList, error) { s.mu.RLock() defer s.mu.RUnlock() c, m, e := s.actorLocked(id, user) if e != nil { return domain.CommunityParticipantList{}, e } restricted := filter.Kind == domain.ChannelParticipantsKicked || filter.Kind == domain.ChannelParticipantsBanned if restricted && !m.CanManageLinkedPeers() { return domain.CommunityParticipantList{}, domain.ErrCommunityAdminRequired } all := s.allParticipantsLocked(id) query := strings.ToLower(strings.TrimSpace(filter.Query)) usersByID := make(map[int64]domain.User, len(all)) if s.users != nil { s.users.mu.RLock() for _, participant := range all { if user, ok := s.users.byID[participant.UserID]; ok { usersByID[participant.UserID] = user } } s.users.mu.RUnlock() } items := all[:0] for _, p := range all { ok := p.Status == domain.CommunityMemberActive if filter.Kind == domain.ChannelParticipantsAdmins { ok = ok && (p.Role == domain.CommunityRoleCreator || p.Role == domain.CommunityRoleAdmin) } else if filter.Kind == domain.ChannelParticipantsKicked || filter.Kind == domain.ChannelParticipantsBanned { ok = p.Status == domain.CommunityMemberKicked } if ok && query != "" { user := usersByID[p.UserID] haystack := strings.ToLower(strings.Join([]string{ strconv.FormatInt(p.UserID, 10), user.FirstName, user.LastName, user.Username, user.Phone, }, " ")) ok = strings.Contains(haystack, query) } if ok { items = append(items, p) } } out := domain.CommunityParticipantList{Community: c, Count: len(items)} if offset > len(items) { offset = len(items) } end := offset + limit if end > len(items) { end = len(items) } out.Participants = append([]domain.CommunityMember(nil), items[offset:end]...) h := fnv.New64a() for _, p := range out.Participants { _, _ = h.Write([]byte(strconv.FormatInt(p.UserID, 10) + string(p.Role) + string(p.Status))) } out.Hash = int64(h.Sum64() & 0x7fffffffffffffff) for _, p := range out.Participants { if user, ok := usersByID[p.UserID]; ok { out.Users = append(out.Users, user) } } return out, nil } func (s *CommunityStore) editViewLocked(user, id int64, can func(domain.CommunityMember) bool, edit func(*domain.Community) bool) (domain.CommunityView, bool, error) { c, m, e := s.actorLocked(id, user) if e != nil { return domain.CommunityView{}, false, e } if !can(m) { return domain.CommunityView{}, false, domain.ErrCommunityAdminRequired } changed := edit(&c) s.communities[id] = c v, e := s.viewLocked(user, id) return v, changed, e } func (s *CommunityStore) EditCommunityTitle(_ context.Context, user, id int64, title string) (domain.CommunityView, bool, error) { s.mu.Lock() defer s.mu.Unlock() return s.editViewLocked(user, id, domain.CommunityMember.CanChangeInfo, func(c *domain.Community) bool { if c.Title == title { return false } c.Title = title return true }) } func (s *CommunityStore) EditCommunityAbout(_ context.Context, user, id int64, about string) (domain.CommunityView, bool, error) { s.mu.Lock() defer s.mu.Unlock() return s.editViewLocked(user, id, domain.CommunityMember.CanChangeInfo, func(c *domain.Community) bool { if c.About == about { return false } c.About = about return true }) } func (s *CommunityStore) EditCommunityDefaultBannedRights(_ context.Context, user, id int64, rights domain.ChannelBannedRights) (domain.CommunityView, bool, error) { s.mu.Lock() defer s.mu.Unlock() return s.editViewLocked(user, id, domain.CommunityMember.CanChangeInfo, func(c *domain.Community) bool { if c.DefaultBannedRights == rights { return false } c.DefaultBannedRights = rights return true }) } func (s *CommunityStore) SetCommunityPhoto(_ context.Context, user, id int64, photo *domain.Photo, date int) (domain.CommunityView, bool, error) { s.mu.Lock() defer s.mu.Unlock() return s.editViewLocked(user, id, domain.CommunityMember.CanChangeInfo, func(c *domain.Community) bool { pid, dc := int64(0), 0 var stripped []byte if photo != nil { pid, dc = photo.ID, photo.DCID stripped = domain.StrippedFromSizes(photo.Sizes) } if c.PhotoID == pid && c.PhotoDCID == dc && string(c.PhotoStripped) == string(stripped) { return false } c.PhotoID, c.PhotoDCID, c.PhotoStripped = pid, dc, append([]byte(nil), stripped...) return true }) } func (s *CommunityStore) EditCommunityAdmin(_ context.Context, req domain.CommunityEditAdminRequest) (domain.CommunityView, bool, error) { s.mu.Lock() defer s.mu.Unlock() c, m, e := s.actorLocked(req.CommunityID, req.ActorUserID) if e != nil { return domain.CommunityView{}, false, e } zero := req.Rights == (domain.ChannelAdminRights{}) if req.UserID == req.ActorUserID && m.Role == domain.CommunityRoleAdmin && zero { delete(s.members[c.ID], req.UserID) v, e := s.viewLocked(req.ActorUserID, c.ID) if errors.Is(e, domain.ErrCommunityPrivate) { return domain.CommunityView{Community: c, Self: m, Forbidden: true}, true, nil } return v, true, e } if !m.CanAddAdmins() { return domain.CommunityView{}, false, domain.ErrCommunityAdminRequired } if req.UserID == c.CreatorUserID { return domain.CommunityView{}, false, domain.ErrCommunityCreatorRequired } old, ok := s.members[c.ID][req.UserID] if zero { if ok && old.Role == domain.CommunityRoleAdmin { delete(s.members[c.ID], req.UserID) v, e := s.viewLocked(req.ActorUserID, c.ID) return v, true, e } v, e := s.viewLocked(req.ActorUserID, c.ID) return v, false, e } s.members[c.ID][req.UserID] = domain.CommunityMember{CommunityID: c.ID, UserID: req.UserID, Role: domain.CommunityRoleAdmin, Status: domain.CommunityMemberActive, AdminRights: req.Rights, Rank: req.Rank, Date: req.Date} v, e := s.viewLocked(req.ActorUserID, c.ID) return v, true, e } func (s *CommunityStore) DeleteCommunity(_ context.Context, user, id int64, date int) (domain.CommunityView, []domain.Peer, error) { s.mu.Lock() defer s.mu.Unlock() c, m, e := s.actorLocked(id, user) if e != nil { return domain.CommunityView{}, nil, e } if m.Role != domain.CommunityRoleCreator { return domain.CommunityView{}, nil, domain.ErrCommunityCreatorRequired } peers := make([]domain.Peer, 0, len(s.links[id])) serviceMessages := make([]domain.SendChannelMessageResult, 0, len(s.links[id])) for p := range s.links[id] { peers = append(peers, p) if e := s.setPeerLinkLocked(p, 0); e != nil { return domain.CommunityView{}, nil, e } serviceMessage, e := s.appendCommunityServiceMessageLocked(p, user, date, 0) if e != nil { _ = s.setPeerLinkLocked(p, c.ID) return domain.CommunityView{}, nil, e } if serviceMessage != nil { serviceMessages = append(serviceMessages, *serviceMessage) } } c.Deleted = true c.Title = "" c.About = "" s.communities[id] = c delete(s.links, id) delete(s.requests, id) delete(s.states, id) return domain.CommunityView{Community: c, Self: m, Forbidden: true, ServiceMessages: serviceMessages}, peers, nil } func (s *CommunityStore) SetCommunityPinned(_ context.Context, user, id int64, pinned bool) (bool, error) { s.mu.Lock() defer s.mu.Unlock() if _, _, e := s.actorLocked(id, user); e != nil { return false, e } st, ok := s.states[id][user] if !ok || !st.Collapsed { return false, domain.ErrCommunityInvalid } if st.Pinned == pinned { return false, nil } st.Pinned = pinned if pinned { max := 1000000000 for _, byUser := range s.states { if x, ok := byUser[user]; ok && x.Pinned && x.PinnedOrder > max { max = x.PinnedOrder } } st.PinnedOrder = max + 1 } else { st.PinnedOrder = 0 } s.states[id][user] = st return true, nil } func (s *CommunityStore) ReorderCommunityPinned(_ context.Context, user int64, order []domain.Peer, force bool) (bool, error) { s.mu.Lock() defer s.mu.Unlock() seen := map[int64]struct{}{} for _, peer := range order { if peer.Type != domain.PeerTypeCommunity { continue } if _, ok := seen[peer.ID]; ok { return false, domain.ErrCommunityInvalid } seen[peer.ID] = struct{}{} st, ok := s.states[peer.ID][user] if !ok || !st.Collapsed { return false, domain.ErrCommunityInvalid } } changed := false for id, byUser := range s.states { if st, ok := byUser[user]; ok && st.Pinned { if force { st.Pinned = false st.PinnedOrder = 0 s.states[id][user] = st changed = true } } } for i, peer := range order { if peer.Type != domain.PeerTypeCommunity { continue } st := s.states[peer.ID][user] pinnedOrder := len(order) - i if st.Pinned && st.PinnedOrder == pinnedOrder { continue } st.Pinned = true st.PinnedOrder = pinnedOrder s.states[peer.ID][user] = st changed = true } return changed, nil } func (s *CommunityStore) CommunitySearchScope(ctx context.Context, user, id int64) (domain.CommunitySearchScope, error) { v, e := s.GetCommunity(ctx, user, id) if e != nil { return domain.CommunitySearchScope{}, e } out := domain.CommunitySearchScope{CommunityID: id} for _, l := range v.Links { if l.Peer.Type == domain.PeerTypeChannel { if l.CanViewHistory { out.ChannelIDs = append(out.ChannelIDs, l.Peer.ID) } } else { out.BotUserIDs = append(out.BotUserIDs, l.Peer.ID) } } return out, nil }