owpengram-server/internal/store/memory/community.go
A da6a57e1a3 feat: sync community aggregates
Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates).

Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
2026-07-20 16:46:02 +08:00

1112 lines
36 KiB
Go

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 = publicPreviewableChannel(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
}