owpengram-server/internal/store/memory/channel.go
A d84fa6e126 fix: align private and channel update semantics
(cherry picked from commit c65f76f56278f74082c4fa792ed49104d5d33c38)
2026-06-10 22:06:48 +08:00

6858 lines
220 KiB
Go

package memory
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"errors"
"sort"
"strconv"
"strings"
"sync"
"time"
"telesrv/internal/domain"
)
const firstMemoryChannelID int64 = 2000000000
type channelRandomKey struct {
channelID int64
userID int64
randomID int64
}
// ChannelStore is an in-memory channel/supergroup store for tests and local development.
type ChannelStore struct {
mu sync.RWMutex
nextID int64
nextHash int64
channels map[int64]domain.Channel
members map[int64]map[int64]domain.ChannelMember
dialogs map[int64]map[int64]domain.ChannelDialog
topics map[int64]map[int]domain.ChannelForumTopic
messages map[int64][]domain.ChannelMessage
reactions map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction
top map[int64]map[string]domain.TopMessageReaction
recent map[int64]map[string]domain.RecentMessageReaction
savedTags map[int64]map[string]domain.SavedReactionTag
mentions map[int64]map[int64]map[int]int
msgViews map[int64]map[int]int
msgViewers map[int64]map[int]map[int64]struct{}
events map[int64][]domain.ChannelUpdateEvent
adminLogs map[int64][]domain.ChannelAdminLogEvent
invites map[string]domain.ChannelInvite
importers map[int64]map[int64]domain.ChannelInviteImporter
msgSeq map[int64]int
ptsSeq map[int64]int
logSeq map[int64]int64
randomToID map[channelRandomKey]int
}
// NewChannelStore creates an in-memory ChannelStore.
func NewChannelStore() *ChannelStore {
return &ChannelStore{
nextID: firstMemoryChannelID,
nextHash: 900000000000,
channels: make(map[int64]domain.Channel),
members: make(map[int64]map[int64]domain.ChannelMember),
dialogs: make(map[int64]map[int64]domain.ChannelDialog),
topics: make(map[int64]map[int]domain.ChannelForumTopic),
messages: make(map[int64][]domain.ChannelMessage),
reactions: make(map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction),
top: make(map[int64]map[string]domain.TopMessageReaction),
recent: make(map[int64]map[string]domain.RecentMessageReaction),
savedTags: make(map[int64]map[string]domain.SavedReactionTag),
mentions: make(map[int64]map[int64]map[int]int),
msgViews: make(map[int64]map[int]int),
msgViewers: make(map[int64]map[int]map[int64]struct{}),
events: make(map[int64][]domain.ChannelUpdateEvent),
adminLogs: make(map[int64][]domain.ChannelAdminLogEvent),
invites: make(map[string]domain.ChannelInvite),
importers: make(map[int64]map[int64]domain.ChannelInviteImporter),
msgSeq: make(map[int64]int),
ptsSeq: make(map[int64]int),
logSeq: make(map[int64]int64),
randomToID: make(map[channelRandomKey]int),
}
}
func (s *ChannelStore) CreateChannel(_ context.Context, req domain.CreateChannelRequest) (domain.CreateChannelResult, error) {
if req.CreatorUserID == 0 || strings.TrimSpace(req.Title) == "" {
return domain.CreateChannelResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channelID := s.nextChannelIDLocked()
channel := domain.Channel{
ID: channelID,
AccessHash: s.nextAccessHashLocked(),
CreatorUserID: req.CreatorUserID,
Title: strings.TrimSpace(req.Title),
About: req.About,
Broadcast: req.Broadcast,
Megagroup: req.Megagroup,
Forum: req.Forum,
ForumTabs: req.ForumTabs,
ParticipantsCount: 1,
AdminsCount: 1,
TTLPeriod: req.TTLPeriod,
Date: req.Date,
}
if !channel.Broadcast && !channel.Megagroup {
channel.Broadcast = true
}
creator := domain.ChannelMember{
ChannelID: channelID,
UserID: req.CreatorUserID,
Role: domain.ChannelRoleCreator,
Status: domain.ChannelMemberActive,
JoinedAt: req.Date,
AdminRights: domain.ChannelAdminRights{
ChangeInfo: true,
PostMessages: true,
EditMessages: true,
DeleteMessages: true,
BanUsers: true,
InviteUsers: true,
PinMessages: true,
AddAdmins: true,
ManageCall: true,
},
}
s.channels[channelID] = channel
s.members[channelID] = map[int64]domain.ChannelMember{creator.UserID: creator}
members := []domain.ChannelMember{creator}
for _, userID := range uniqueNonZero(req.MemberUserIDs, req.CreatorUserID) {
member := domain.ChannelMember{
ChannelID: channelID,
UserID: userID,
InviterUserID: req.CreatorUserID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
JoinedAt: req.Date,
}
s.members[channelID][userID] = member
members = append(members, member)
channel.ParticipantsCount++
}
msg, event := s.appendChannelServiceMessageLocked(channelID, req.CreatorUserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionCreate,
Title: channel.Title,
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[channelID] = channel
for _, member := range members {
s.upsertChannelDialogLocked(member.UserID, channel, msg, member.UserID == req.CreatorUserID)
}
return domain.CreateChannelResult{
Channel: channel,
Members: cloneChannelMembers(members),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(channelID, 0, 0),
}, nil
}
func (s *ChannelStore) GetChannel(_ context.Context, viewerUserID, channelID int64) (domain.ChannelView, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, preview, err := s.channelForViewerLocked(viewerUserID, channelID)
if err != nil {
return domain.ChannelView{}, err
}
dialog := s.dialogForUserLocked(viewerUserID, channel)
if preview {
dialog = previewChannelDialog(viewerUserID, channel, member)
}
return domain.ChannelView{
Channel: cloneChannel(channel),
Self: member,
Dialog: dialog,
}, nil
}
func (s *ChannelStore) SaveChannelDefaultSendAs(_ context.Context, req domain.SaveChannelDefaultSendAsRequest) (domain.ChannelView, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ChannelView{}, domain.ErrChannelInvalid
}
if req.SendAs != nil && req.SendAs.Type != domain.PeerTypeUser && req.SendAs.Type != domain.PeerTypeChannel {
return domain.ChannelView{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelView{}, err
}
dialog := s.dialogForUserLocked(req.UserID, channel)
if req.SendAs != nil {
p := *req.SendAs
dialog.DefaultSendAs = &p
} else {
dialog.DefaultSendAs = nil
}
if s.dialogs[req.UserID] == nil {
s.dialogs[req.UserID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[req.UserID][req.ChannelID] = dialog
member := s.members[req.ChannelID][req.UserID]
return domain.ChannelView{Channel: cloneChannel(channel), Self: member, Dialog: dialog}, nil
}
func (s *ChannelStore) GetChannelByID(_ context.Context, channelID int64) (domain.Channel, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
return cloneChannel(channel), nil
}
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]
if channel.ParticipantsHidden && !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
}
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) InviteToChannel(_ context.Context, channelID, inviterUserID int64, userIDs []int64, date int) (domain.CreateChannelResult, error) {
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(inviterUserID, channelID)
if err != nil {
return domain.CreateChannelResult{}, err
}
inviter := s.members[channelID][inviterUserID]
if !canInviteToChannel(channel, inviter) {
return domain.CreateChannelResult{}, domain.ErrChannelAdminRequired
}
requested := uniqueNonZero(userIDs, 0)
inviteOne := len(requested) == 1
canRestoreKicked := canBanChannelUsers(inviter)
added := make([]int64, 0, len(requested))
members := make([]domain.ChannelMember, 0, len(requested))
restoredKicked := 0
for _, userID := range requested {
if existing, ok := s.members[channelID][userID]; ok {
if existing.Status == domain.ChannelMemberActive {
if inviteOne {
return domain.CreateChannelResult{}, domain.ErrUserAlreadyParticipant
}
continue
}
if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages {
if !canRestoreKicked {
if inviteOne {
return domain.CreateChannelResult{}, domain.ErrUserKicked
}
continue
}
if existing.Status == domain.ChannelMemberKicked {
restoredKicked++
}
}
}
member := domain.ChannelMember{
ChannelID: channelID,
UserID: userID,
InviterUserID: inviterUserID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
JoinedAt: date,
AvailableMinID: channelInitialAvailableMinID(channel),
AvailableMinPts: channelInitialAvailableMinPts(channel),
ReadInboxMaxID: channel.TopMessageID,
}
s.members[channelID][userID] = member
members = append(members, member)
added = append(added, userID)
channel.ParticipantsCount++
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: inviterUserID,
Date: date,
Type: domain.ChannelAdminLogParticipantInvite,
Participant: ptrChannelMember(member),
})
}
if restoredKicked > 0 {
channel.KickedCount = maxInt(channel.KickedCount-restoredKicked, 0)
}
s.channels[channelID] = channel
var msg domain.ChannelMessage
var event domain.ChannelUpdateEvent
if len(added) > 0 && channel.Megagroup {
msg, event = s.appendChannelServiceMessageLocked(channelID, inviterUserID, date, domain.ChannelMessageAction{
Type: domain.ChannelActionChatAddUser,
UserIDs: append([]int64(nil), added...),
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[channelID] = channel
}
for _, member := range members {
s.upsertChannelDialogLocked(member.UserID, channel, msg, false)
}
return domain.CreateChannelResult{
Channel: channel,
Members: cloneChannelMembers(members),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(channelID, 0, 0),
}, nil
}
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,
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
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, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
return domain.CreateChannelResult{}, err
}
member := s.members[channelID][userID]
member.Status = domain.ChannelMemberLeft
member.LeftAt = date
s.members[channelID][userID] = member
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: date,
Type: domain.ChannelAdminLogParticipantLeave,
})
if channel.ParticipantsCount > 0 {
channel.ParticipantsCount--
}
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: []domain.ChannelMember{member},
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: append(s.activeMemberIDsLocked(channelID, 0, 0), userID),
}, nil
}
func (s *ChannelStore) EditChannelTitle(_ context.Context, req domain.EditChannelTitleRequest) (domain.EditChannelTitleResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Title) == "" {
return domain.EditChannelTitleResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelTitleResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canChangeChannelInfo(member) {
return domain.EditChannelTitleResult{}, domain.ErrChannelAdminRequired
}
title := strings.TrimSpace(req.Title)
if channel.Title == title {
return domain.EditChannelTitleResult{}, domain.ErrChannelNotModified
}
prevTitle := channel.Title
channel.Title = title
msg, event := s.appendChannelServiceMessageLocked(req.ChannelID, req.UserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionEditTitle,
Title: title,
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[req.ChannelID] = channel
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogChangeTitle,
PrevString: prevTitle,
NewString: title,
})
s.upsertChannelDialogLocked(req.UserID, channel, msg, true)
return domain.EditChannelTitleResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) EditChannelAbout(_ context.Context, req domain.EditChannelAboutRequest) (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
}
member := s.members[req.ChannelID][req.UserID]
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.About = req.About
s.channels[req.ChannelID] = channel
return channel, 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) 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)
}
}
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,
}, 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) DeleteChannel(_ context.Context, req domain.DeleteChannelRequest) (domain.DeleteChannelResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.DeleteChannelResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if member.Role != domain.ChannelRoleCreator {
return domain.DeleteChannelResult{}, domain.ErrChannelAdminRequired
}
recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0)
channel.Deleted = true
s.channels[req.ChannelID] = channel
return domain.DeleteChannelResult{Channel: channel, Recipients: recipients}, nil
}
func (s *ChannelStore) CheckUsername(_ context.Context, userID, channelID int64, username string) (bool, error) {
if userID == 0 || channelID == 0 || strings.TrimSpace(username) == "" {
return false, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(userID, channelID); err != nil {
return false, err
}
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
for id, channel := range s.channels {
if channel.Deleted || channel.Username == "" {
continue
}
if strings.ToLower(channel.Username) == usernameLower && id != channelID {
return false, nil
}
}
return true, nil
}
func (s *ChannelStore) UpdateUsername(_ context.Context, req domain.UpdateChannelUsernameRequest) (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
}
member := s.members[req.ChannelID][req.UserID]
if member.Role != domain.ChannelRoleCreator {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
username := strings.TrimSpace(strings.TrimPrefix(req.Username, "@"))
usernameLower := strings.ToLower(username)
if strings.EqualFold(channel.Username, username) {
return domain.Channel{}, domain.ErrChannelNotModified
}
if usernameLower != "" {
for id, existing := range s.channels {
if existing.Deleted || existing.Username == "" {
continue
}
if strings.ToLower(existing.Username) == usernameLower && id != req.ChannelID {
return domain.Channel{}, domain.ErrUsernameOccupied
}
}
}
prevUsername := channel.Username
channel.Username = username
s.channels[req.ChannelID] = channel
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogChangeUsername,
PrevString: prevUsername,
NewString: username,
})
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) ResolvePublicChannelUsername(_ context.Context, viewerUserID int64, username string) (domain.Channel, bool, error) {
if viewerUserID == 0 {
return domain.Channel{}, false, domain.ErrChannelInvalid
}
username = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
if username == "" {
return domain.Channel{}, false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, channel := range s.channels {
if !publicSearchableChannel(channel) {
continue
}
if strings.ToLower(channel.Username) == username {
return cloneChannel(channel), true, nil
}
}
return domain.Channel{}, false, nil
}
func (s *ChannelStore) SearchPublicChannels(_ context.Context, viewerUserID int64, query string, limit int) (domain.PublicChannelSearchResult, error) {
if viewerUserID == 0 {
return domain.PublicChannelSearchResult{}, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxPublicChannelSearchLimit {
limit = domain.MaxPublicChannelSearchLimit
}
query = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(query, "@")))
if query == "" {
return domain.PublicChannelSearchResult{}, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
type item struct {
channel domain.Channel
joined bool
rank int
}
items := make([]item, 0, limit)
for channelID, channel := range s.channels {
rank, ok := publicChannelSearchRank(channel, query)
if !ok {
continue
}
member, joined := s.members[channelID][viewerUserID]
joined = joined && member.Status == domain.ChannelMemberActive
items = append(items, item{
channel: cloneChannel(channel),
joined: joined,
rank: rank,
})
}
sort.SliceStable(items, func(i, j int) bool {
if items[i].rank != items[j].rank {
return items[i].rank < items[j].rank
}
if items[i].joined != items[j].joined {
return items[i].joined
}
if items[i].channel.ParticipantsCount != items[j].channel.ParticipantsCount {
return items[i].channel.ParticipantsCount > items[j].channel.ParticipantsCount
}
if items[i].channel.Date != items[j].channel.Date {
return items[i].channel.Date > items[j].channel.Date
}
return items[i].channel.ID > items[j].channel.ID
})
out := domain.PublicChannelSearchResult{}
for _, item := range items {
if len(out.MyResults)+len(out.Results) >= limit {
break
}
if item.joined {
out.MyResults = append(out.MyResults, item.channel)
} else {
out.Results = append(out.Results, item.channel)
}
}
return out, nil
}
func (s *ChannelStore) SetChannelPhoto(_ context.Context, userID, channelID int64, photo *domain.Photo) (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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if photo != nil && photo.ID != 0 {
channel.PhotoID = photo.ID
channel.PhotoDCID = photo.DCID
channel.PhotoStripped = domain.StrippedFromSizes(photo.Sizes)
} else {
channel.PhotoID = 0
channel.PhotoDCID = 0
channel.PhotoStripped = nil
}
s.channels[channelID] = channel
return channel, nil
}
func (s *ChannelStore) SetSignatures(_ 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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.Signatures
channel.Signatures = enabled
s.channels[channelID] = channel
if prev != enabled {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogToggleSignatures,
PrevBool: prev,
NewBool: enabled,
})
}
return channel, nil
}
func (s *ChannelStore) SetPreHistoryHidden(_ 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 member.Role != domain.ChannelRoleCreator {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.PreHistoryHidden
channel.PreHistoryHidden = enabled
s.channels[channelID] = channel
if prev != enabled {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogTogglePreHistoryHidden,
PrevBool: prev,
NewBool: enabled,
})
}
return channel, 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) SetForum(_ context.Context, userID, channelID int64, enabled, tabs 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 || channel.Broadcast {
return domain.Channel{}, domain.ErrChannelNotModified
}
if member.Role != domain.ChannelRoleCreator {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if enabled && channel.LinkedChatID != 0 {
return domain.Channel{}, domain.ErrChatDiscussionUnallowed
}
prevForum := channel.Forum
prevTabs := channel.ForumTabs
channel.Forum = enabled
channel.ForumTabs = enabled && tabs
s.channels[channelID] = channel
if prevForum != channel.Forum || prevTabs != channel.ForumTabs {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogToggleForum,
PrevBool: prevForum,
NewBool: enabled,
})
}
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetAutotranslation(_ 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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.Autotranslation
channel.Autotranslation = enabled
s.channels[channelID] = channel
if prev != enabled {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogToggleAutotranslation,
PrevBool: prev,
NewBool: enabled,
})
}
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetRestrictedSponsored(_ context.Context, userID, channelID int64, restricted 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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.RestrictedSponsored = restricted
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetPaidMessagesPrice(_ context.Context, userID, channelID int64, stars int64, broadcastMessagesAllowed bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 || stars < 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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.SendPaidMessagesStars = stars
channel.BroadcastMessagesAllowed = channel.Broadcast && broadcastMessagesAllowed
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetAntiSpam(_ 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 || !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.AntiSpam
channel.AntiSpam = enabled
s.channels[channelID] = channel
if prev != enabled {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogToggleAntiSpam,
PrevBool: prev,
NewBool: enabled,
})
}
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetSlowMode(_ context.Context, userID, channelID int64, seconds int) (domain.Channel, error) {
if userID == 0 || channelID == 0 || !domain.ValidChannelSlowModeSeconds(seconds) {
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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.SlowmodeSeconds
channel.SlowmodeSeconds = seconds
s.channels[channelID] = channel
if prev != seconds {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogToggleSlowMode,
PrevInt: prev,
NewInt: seconds,
})
}
return channel, nil
}
func (s *ChannelStore) SetNoForwards(_ 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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.NoForwards = 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) SetAvailableReactions(_ context.Context, userID, channelID int64, policy domain.ChannelReactionPolicy) (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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.ReactionPolicy = copyChannelReactionPolicy(policy)
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetColor(_ context.Context, userID, channelID int64, forProfile bool, color domain.ChannelPeerColor) (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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if forProfile {
channel.ProfileColor = color
} else {
channel.Color = color
}
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetEmojiStatus(_ context.Context, userID, channelID int64, status domain.ChannelEmojiStatus) (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 !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
channel.EmojiStatus = status
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) ListAdminLog(_ context.Context, req domain.ChannelAdminLogRequest) (domain.ChannelAdminLogResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MaxID < 0 || req.MinID < 0 {
return domain.ChannelAdminLogResult{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelAdminLogResult{}, err
}
if !isChannelAdmin(s.members[req.ChannelID][req.UserID]) {
return domain.ChannelAdminLogResult{}, domain.ErrChannelAdminRequired
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelAdminLogLimit {
limit = domain.MaxChannelAdminLogLimit
}
admins := int64Set(req.AdminUserIDs)
query := strings.ToLower(strings.TrimSpace(req.Query))
out := make([]domain.ChannelAdminLogEvent, 0, limit)
events := s.adminLogs[req.ChannelID]
for i := len(events) - 1; i >= 0 && len(out) < limit; i-- {
event := events[i]
if req.MaxID > 0 && event.ID >= req.MaxID {
continue
}
if req.MinID > 0 && event.ID <= req.MinID {
continue
}
if len(admins) > 0 {
if _, ok := admins[event.UserID]; !ok {
continue
}
}
if !adminLogEventMatchesFilter(event.Type, req.Filter) {
continue
}
if query != "" && !adminLogEventMatchesQuery(event, query) {
continue
}
out = append(out, cloneChannelAdminLogEvent(event))
}
return domain.ChannelAdminLogResult{Channel: channel, Events: out}, nil
}
func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChannelMessageRequest) (domain.SendChannelMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
if strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.SendChannelMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canSendChannelMessage(channel, member) {
return domain.SendChannelMessageResult{}, domain.ErrChannelWriteForbidden
}
if req.RandomID != 0 {
if id, ok := s.randomToID[channelRandomKey{channelID: req.ChannelID, userID: req.UserID, randomID: req.RandomID}]; ok {
msg, ok := s.findMessageLocked(req.ChannelID, id)
if ok {
event := s.eventForMessageLocked(req.ChannelID, id)
if event.Message.ID != 0 {
msg = event.Message
}
return domain.SendChannelMessageResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: event,
Duplicate: true,
}, nil
}
}
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
if wait := channelSlowModeWait(channel, member, req.Date); wait > 0 {
return domain.SendChannelMessageResult{}, domain.NewSlowModeWaitError(wait)
}
replyTo, err := s.resolveChannelReplyLocked(req, member, channel)
if err != nil {
return domain.SendChannelMessageResult{}, err
}
var sendAs *domain.Peer
if req.SendAs != nil {
p := *req.SendAs
sendAs = &p
}
pts := s.nextChannelPtsLocked(req.ChannelID)
msgID := s.nextChannelMessageIDLocked(req.ChannelID)
var discussion *domain.SendChannelDiscussionResult
var discussionRef *domain.ChannelDiscussionRef
if channel.Broadcast && channel.LinkedChatID != 0 {
if linked, ok := s.channels[channel.LinkedChatID]; ok && !linked.Deleted && linked.Megagroup {
discussionPts := s.nextChannelPtsLocked(linked.ID)
discussionMsgID := s.nextChannelMessageIDLocked(linked.ID)
discussionRef = &domain.ChannelDiscussionRef{ChannelID: linked.ID, MessageID: discussionMsgID}
discussionMsg := domain.ChannelMessage{
ChannelID: linked.ID,
ID: discussionMsgID,
SenderUserID: req.UserID,
From: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
Date: req.Date,
Silent: req.Silent,
NoForwards: req.NoForwards || channel.NoForwards || linked.NoForwards,
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Forward: &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, Date: req.Date, ChannelPost: msgID, SavedFrom: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, SavedFromMsgID: msgID},
Pts: discussionPts,
}
discussionEvent := domain.ChannelUpdateEvent{
ChannelID: linked.ID,
Type: domain.ChannelUpdateNewMessage,
Pts: discussionPts,
PtsCount: 1,
Date: req.Date,
Message: cloneChannelMessage(discussionMsg),
}
s.messages[linked.ID] = append(s.messages[linked.ID], discussionMsg)
s.events[linked.ID] = append(s.events[linked.ID], discussionEvent)
linked.TopMessageID = discussionMsgID
linked.Pts = discussionPts
s.channels[linked.ID] = linked
s.addChannelUnreadMentionsLocked(linked.ID, discussionMsg, req.UserID, req.MentionUserIDs)
for userID, member := range s.members[linked.ID] {
if member.Status == domain.ChannelMemberActive {
s.upsertChannelDialogLocked(userID, linked, discussionMsg, false)
}
}
discussion = &domain.SendChannelDiscussionResult{
Channel: cloneChannel(linked),
Message: cloneChannelMessage(discussionMsg),
Event: cloneChannelEvent(discussionEvent),
Recipients: s.activeMemberIDsLocked(linked.ID, 0, 0),
}
}
}
msg := domain.ChannelMessage{
ChannelID: req.ChannelID,
ID: msgID,
RandomID: req.RandomID,
SenderUserID: req.UserID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID},
Date: req.Date,
Post: channel.Broadcast,
Silent: req.Silent,
NoForwards: req.NoForwards || channel.NoForwards,
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Media: req.Media,
ReplyTo: replyTo,
Forward: cloneMessageForward(req.Forward),
SendAs: sendAs,
Discussion: discussionRef,
Action: cloneChannelMessageAction(req.Action),
Pts: pts,
}
msg.Replies = s.channelMessageRepliesLocked(req.UserID, req.ChannelID, msg)
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdateNewMessage,
Pts: pts,
PtsCount: 1,
Date: req.Date,
Message: cloneChannelMessage(msg),
SenderUserID: req.UserID,
}
s.messages[req.ChannelID] = append(s.messages[req.ChannelID], msg)
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
s.addChannelUnreadMentionsLocked(req.ChannelID, msg, req.UserID, req.MentionUserIDs)
s.updateForumTopicTopMessageLocked(req.ChannelID, msg)
if channel.Broadcast {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogSendMessage,
Message: ptrChannelMessage(msg),
Query: msg.Body,
})
}
if req.RandomID != 0 {
s.randomToID[channelRandomKey{channelID: req.ChannelID, userID: req.UserID, randomID: req.RandomID}] = msg.ID
}
channel.TopMessageID = msg.ID
channel.Pts = pts
s.channels[req.ChannelID] = channel
member.SlowmodeLastSendDate = req.Date
s.members[req.ChannelID][req.UserID] = member
for userID, member := range s.members[req.ChannelID] {
if member.Status == domain.ChannelMemberActive {
s.upsertChannelDialogLocked(userID, channel, msg, userID == req.UserID)
}
}
return domain.SendChannelMessageResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Discussion: discussion,
}, nil
}
func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 || strings.TrimSpace(req.Message) == "" {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
idx, ok := s.findMessageIndexLocked(req.ChannelID, req.ID)
if !ok || s.messages[req.ChannelID][idx].Deleted || s.messages[req.ChannelID][idx].Action != nil {
return domain.EditChannelMessageResult{}, domain.ErrMessageIDInvalid
}
prevMsg := s.messages[req.ChannelID][idx]
msg := prevMsg
if msg.SenderUserID != req.UserID && !canEditChannelMessage(member) {
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
}
if msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
}
pts := s.nextChannelPtsLocked(req.ChannelID)
msg.Body = req.Message
msg.Entities = append([]domain.MessageEntity(nil), req.Entities...)
msg.EditDate = req.EditDate
msg.Pts = pts
s.messages[req.ChannelID][idx] = msg
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdateEditMessage,
Pts: pts,
PtsCount: 1,
Date: req.EditDate,
Message: cloneChannelMessage(msg),
SenderUserID: req.UserID,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.EditDate,
Type: domain.ChannelAdminLogEditMessage,
PrevMessage: ptrChannelMessage(prevMsg),
NewMessage: ptrChannelMessage(msg),
Query: msg.Body,
})
return domain.EditChannelMessageResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) DeleteChannelMessages(_ context.Context, req domain.DeleteChannelMessagesRequest) (domain.DeleteChannelMessagesResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || len(req.IDs) == 0 {
return domain.DeleteChannelMessagesResult{}, domain.ErrChannelInvalid
}
if len(req.IDs) > domain.MaxDeleteMessageIDs {
return domain.DeleteChannelMessagesResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelMessagesResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, req.IDs, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelMessagesResult{}, err
}
return domain.DeleteChannelMessagesResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) DeleteChannelHistory(_ context.Context, req domain.DeleteChannelHistoryRequest) (domain.DeleteChannelHistoryResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
maxID := req.MaxID
if maxID <= 0 || maxID > channel.TopMessageID {
maxID = channel.TopMessageID
}
member := s.members[req.ChannelID][req.UserID]
if !req.ForEveryone {
appliedMinID := maxInt(member.AvailableMinID, maxID)
member.AvailableMinID = appliedMinID
member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, appliedMinID)
member.UnreadMark = false
s.members[req.ChannelID][req.UserID] = member
s.deleteChannelUnreadMentionsUpToLocked(req.UserID, req.ChannelID, appliedMinID)
if s.dialogs[req.UserID] == nil {
s.dialogs[req.UserID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[req.UserID][req.ChannelID] = s.dialogForUserLocked(req.UserID, channel)
return domain.DeleteChannelHistoryResult{Channel: channel, AvailableMinID: appliedMinID}, nil
}
if !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
}
ids := make([]int, 0, domain.MaxDeleteHistoryBatch)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted || msg.ID > maxID {
continue
}
ids = append(ids, msg.ID)
if len(ids) >= domain.MaxDeleteHistoryBatch {
break
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, ids, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
offset := 0
if len(deleted) == domain.MaxDeleteHistoryBatch {
offset = 1
}
return domain.DeleteChannelHistoryResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Offset: offset,
}, nil
}
func (s *ChannelStore) DeleteChannelParticipantHistory(_ context.Context, req domain.DeleteChannelParticipantHistoryRequest) (domain.DeleteChannelHistoryResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.ParticipantUserID == 0 {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
}
ids := make([]int, 0, domain.MaxDeleteHistoryBatch)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted || msg.SenderUserID != req.ParticipantUserID {
continue
}
ids = append(ids, msg.ID)
if len(ids) >= domain.MaxDeleteHistoryBatch {
break
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, ids, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
offset := 0
if len(deleted) == domain.MaxDeleteHistoryBatch {
offset = 1
}
return domain.DeleteChannelHistoryResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Offset: offset,
}, nil
}
func (s *ChannelStore) UpdatePinnedMessage(_ context.Context, req domain.UpdateChannelPinnedMessageRequest) (domain.UpdateChannelPinnedMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.UpdateChannelPinnedMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canPinChannelMessages(channel, member) {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelAdminRequired
}
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || msg.Deleted {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrMessageIDInvalid
}
pinnedID := 0
if req.Pinned {
pinnedID = req.MessageID
}
if channel.PinnedMessageID == pinnedID {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelNotModified
}
pts := s.nextChannelPtsLocked(req.ChannelID)
channel.PinnedMessageID = pinnedID
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdatePinnedMessages,
Pts: pts,
PtsCount: 1,
Date: req.Date,
MessageIDs: []int{req.MessageID},
SenderUserID: req.UserID,
Pinned: req.Pinned,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogUpdatePinned,
Message: ptrChannelMessage(msg),
Query: msg.Body,
})
return domain.UpdateChannelPinnedMessageResult{
Channel: channel,
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) ExportInvite(_ context.Context, req domain.ExportChannelInviteRequest) (domain.ExportChannelInviteResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ExportChannelInviteResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ExportChannelInviteResult{}, domain.ErrChannelAdminRequired
}
if req.LegacyRevokePermanent {
for hash, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.UserID && invite.Permanent {
invite.Revoked = true
s.invites[hash] = invite
}
}
}
inviteID, err := randomMemoryPositiveInt64()
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
hash, err := randomMemoryInviteHash()
if err != nil {
return domain.ExportChannelInviteResult{}, err
}
invite := domain.ChannelInvite{
ChannelID: req.ChannelID,
InviteID: inviteID,
Hash: hash,
AdminUserID: req.UserID,
Title: req.Title,
Permanent: req.ExpireDate == 0 && req.UsageLimit == 0 && !req.RequestNeeded && req.Title == "",
RequestNeeded: req.RequestNeeded,
ExpireDate: req.ExpireDate,
UsageLimit: req.UsageLimit,
Date: req.Date,
}
s.invites[hash] = invite
return domain.ExportChannelInviteResult{Channel: channel, Invite: invite}, nil
}
func (s *ChannelStore) CheckInvite(_ context.Context, userID int64, hash string, date int) (domain.CheckChannelInviteResult, error) {
if userID == 0 || strings.TrimSpace(hash) == "" {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashEmpty
}
s.mu.RLock()
defer s.mu.RUnlock()
invite, ok := s.invites[strings.TrimSpace(hash)]
if !ok || invite.Revoked {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
if invite.ExpireDate > 0 && invite.ExpireDate < date {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashExpired
}
channel, ok := s.channels[invite.ChannelID]
if !ok || channel.Deleted {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
member := s.members[invite.ChannelID][userID]
if member.Status == domain.ChannelMemberKicked || member.Status == domain.ChannelMemberBanned || member.BannedRights.ViewMessages {
return domain.CheckChannelInviteResult{}, domain.ErrInviteHashInvalid
}
return domain.CheckChannelInviteResult{
Channel: channel,
Invite: invite,
Already: member.Status == domain.ChannelMemberActive,
Self: member,
}, nil
}
func (s *ChannelStore) ImportInvite(_ context.Context, req domain.ImportChannelInviteRequest) (domain.CreateChannelResult, error) {
if req.UserID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.CreateChannelResult{}, domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
invite, ok := s.invites[strings.TrimSpace(req.Hash)]
if !ok || invite.Revoked {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
if invite.ExpireDate > 0 && invite.ExpireDate < req.Date {
return domain.CreateChannelResult{}, domain.ErrInviteHashExpired
}
channel, ok := s.channels[invite.ChannelID]
if !ok || channel.Deleted {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
if invite.RequestNeeded {
if err := s.recordPendingInviteRequestLocked(invite, req.UserID, req.Date); err != nil {
return domain.CreateChannelResult{}, err
}
return domain.CreateChannelResult{Channel: channel}, domain.ErrInviteRequestSent
}
return s.approveInviteImporterLocked(channel, invite, req.UserID, 0, req.Date)
}
func (s *ChannelStore) ListExportedInvites(_ context.Context, req domain.ChannelInviteListRequest) (domain.ChannelInviteList, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.AdminUserID == 0 {
return domain.ChannelInviteList{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInviteList{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInviteList{}, domain.ErrChannelAdminRequired
}
all := make([]domain.ChannelInvite, 0)
for _, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.AdminUserID && invite.Revoked == req.Revoked {
all = append(all, invite)
}
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].Hash > all[j].Hash
})
total := len(all)
start := 0
if req.OffsetDate > 0 || req.OffsetHash != "" {
start = len(all)
for i, invite := range all {
if invite.Date == req.OffsetDate && invite.Hash == req.OffsetHash {
start = i + 1
break
}
}
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelInviteListLimit {
limit = domain.MaxChannelInviteListLimit
}
if start > len(all) {
start = len(all)
}
end := start + limit
if end > len(all) {
end = len(all)
}
return domain.ChannelInviteList{Count: total, Invites: cloneChannelInvites(all[start:end])}, nil
}
func (s *ChannelStore) GetExportedInvite(_ context.Context, req domain.GetChannelInviteRequest) (domain.ChannelInvite, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.ChannelInvite{}, domain.ErrInviteHashEmpty
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInvite{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInvite{}, domain.ErrChannelAdminRequired
}
return s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
}
func (s *ChannelStore) EditExportedInvite(_ context.Context, req domain.EditChannelInviteRequest) (domain.EditChannelInviteResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.EditChannelInviteResult{}, domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.EditChannelInviteResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.EditChannelInviteResult{}, domain.ErrChannelAdminRequired
}
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.EditChannelInviteResult{}, err
}
if req.Revoked {
if invite.Revoked {
return domain.EditChannelInviteResult{}, domain.ErrInviteRevokedMissing
}
invite.Revoked = true
s.invites[invite.Hash] = invite
if !invite.Permanent {
return domain.EditChannelInviteResult{Invite: invite}, nil
}
newInvite, err := s.newReplacementInviteLocked(invite, req.Date)
if err != nil {
return domain.EditChannelInviteResult{}, err
}
s.invites[newInvite.Hash] = newInvite
return domain.EditChannelInviteResult{Invite: invite, NewInvite: &newInvite}, nil
}
if invite.Permanent && ((req.HasExpireDate && req.ExpireDate > 0) || (req.HasUsageLimit && req.UsageLimit > 0) || (req.HasRequestNeeded && req.RequestNeeded)) {
return domain.EditChannelInviteResult{}, domain.ErrInvitePermanent
}
if req.HasExpireDate {
invite.ExpireDate = req.ExpireDate
}
if req.HasUsageLimit {
invite.UsageLimit = req.UsageLimit
}
if req.HasRequestNeeded {
invite.RequestNeeded = req.RequestNeeded
}
if req.HasTitle {
invite.Title = req.Title
}
invite.Permanent = invite.ExpireDate == 0 && invite.UsageLimit == 0 && !invite.RequestNeeded && invite.Title == ""
s.invites[invite.Hash] = invite
return domain.EditChannelInviteResult{Invite: invite}, nil
}
func (s *ChannelStore) DeleteExportedInvite(_ context.Context, req domain.DeleteChannelInviteRequest) error {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Hash) == "" {
return domain.ErrInviteHashEmpty
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ErrChannelAdminRequired
}
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return err
}
delete(s.invites, invite.Hash)
return nil
}
func (s *ChannelStore) DeleteRevokedExportedInvites(_ context.Context, req domain.DeleteRevokedChannelInvitesRequest) error {
if req.UserID == 0 || req.ChannelID == 0 || req.AdminUserID == 0 {
return domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ErrChannelAdminRequired
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelHideJoinRequests {
limit = domain.MaxChannelHideJoinRequests
}
deleted := 0
for hash, invite := range s.invites {
if invite.ChannelID == req.ChannelID && invite.AdminUserID == req.AdminUserID && invite.Revoked {
delete(s.invites, hash)
deleted++
if deleted >= limit {
break
}
}
}
return nil
}
func (s *ChannelStore) ListAdminsWithInvites(_ context.Context, userID, channelID int64) ([]domain.ChannelAdminInviteCount, error) {
if userID == 0 || channelID == 0 {
return nil, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(userID, channelID); err != nil {
return nil, err
}
member := s.members[channelID][userID]
if !canExportChannelInvite(member) {
return nil, domain.ErrChannelAdminRequired
}
byAdmin := map[int64]*domain.ChannelAdminInviteCount{}
for _, invite := range s.invites {
if invite.ChannelID != channelID {
continue
}
count := byAdmin[invite.AdminUserID]
if count == nil {
count = &domain.ChannelAdminInviteCount{AdminUserID: invite.AdminUserID}
byAdmin[invite.AdminUserID] = count
}
if invite.Revoked {
count.RevokedInvitesCount++
} else {
count.InvitesCount++
}
}
out := make([]domain.ChannelAdminInviteCount, 0, len(byAdmin))
for _, count := range byAdmin {
out = append(out, *count)
}
sort.Slice(out, func(i, j int) bool { return out[i].AdminUserID < out[j].AdminUserID })
return out, nil
}
func (s *ChannelStore) ListInviteImporters(_ context.Context, req domain.ChannelInviteImportersRequest) (domain.ChannelInviteImporterList, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ChannelInviteImporterList{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, err := s.channelForMemberLocked(req.UserID, req.ChannelID); err != nil {
return domain.ChannelInviteImporterList{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canExportChannelInvite(member) {
return domain.ChannelInviteImporterList{}, domain.ErrChannelAdminRequired
}
var inviteID int64
if req.Hash != "" {
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.ChannelInviteImporterList{}, err
}
inviteID = invite.InviteID
}
if req.Query != "" {
return domain.ChannelInviteImporterList{}, nil
}
all := make([]domain.ChannelInviteImporter, 0)
for _, importer := range s.importers[req.ChannelID] {
if importer.Requested != req.Requested {
continue
}
if inviteID != 0 && importer.InviteID != inviteID {
continue
}
all = append(all, importer)
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].UserID > all[j].UserID
})
total := len(all)
start := 0
if req.OffsetDate > 0 || req.OffsetUserID != 0 {
start = len(all)
for i, importer := range all {
if importer.Date == req.OffsetDate && importer.UserID == req.OffsetUserID {
start = i + 1
break
}
}
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelInviteListLimit {
limit = domain.MaxChannelInviteListLimit
}
if start > len(all) {
start = len(all)
}
end := start + limit
if end > len(all) {
end = len(all)
}
return domain.ChannelInviteImporterList{Count: total, Importers: cloneChannelInviteImporters(all[start:end])}, nil
}
func (s *ChannelStore) PendingJoinRequests(_ context.Context, channelID int64, limit int) (domain.ChannelPendingJoinRequests, error) {
if channelID == 0 {
return domain.ChannelPendingJoinRequests{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.ChannelPendingJoinRequests{}, domain.ErrChannelInvalid
}
all := make([]domain.ChannelInviteImporter, 0)
for _, importer := range s.importers[channelID] {
if importer.Requested {
all = append(all, importer)
}
}
sort.Slice(all, func(i, j int) bool {
if all[i].Date != all[j].Date {
return all[i].Date > all[j].Date
}
return all[i].UserID > all[j].UserID
})
if limit <= 0 || limit > domain.MaxChannelPendingJoinRecentRequesters {
limit = domain.MaxChannelPendingJoinRecentRequesters
}
if len(all) < limit {
limit = len(all)
}
recent := make([]int64, 0, limit)
for _, importer := range all[:limit] {
recent = append(recent, importer.UserID)
}
return domain.ChannelPendingJoinRequests{
ChannelID: channelID,
Count: len(all),
RecentRequesters: recent,
}, 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) HideAllChatJoinRequests(_ context.Context, req domain.HideChannelJoinRequestsRequest) (domain.CreateChannelResult, error) {
if req.UserID == 0 || req.ChannelID == 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
}
var inviteID int64
if req.Hash != "" {
invite, err := s.inviteByChannelHashLocked(req.ChannelID, req.Hash)
if err != nil {
return domain.CreateChannelResult{}, err
}
inviteID = invite.InviteID
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelHideJoinRequests {
limit = domain.MaxChannelHideJoinRequests
}
targets := make([]domain.ChannelInviteImporter, 0, limit)
for _, importer := range s.importers[req.ChannelID] {
if !importer.Requested {
continue
}
if inviteID != 0 && importer.InviteID != inviteID {
continue
}
targets = append(targets, importer)
if len(targets) >= limit {
break
}
}
var result domain.CreateChannelResult
for _, importer := range targets {
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, importer.UserID)
result = domain.CreateChannelResult{Channel: channel, Recipients: s.activeMemberIDsLocked(req.ChannelID, importer.UserID, 0)}
continue
}
result, err = s.approveInviteImporterLocked(channel, invite, importer.UserID, req.UserID, req.Date)
if err != nil {
return domain.CreateChannelResult{}, err
}
channel = result.Channel
}
if result.Channel.ID == 0 {
result = domain.CreateChannelResult{Channel: channel, Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0)}
}
return result, nil
}
func (s *ChannelStore) approveInviteImporterLocked(channel domain.Channel, invite domain.ChannelInvite, userID, approvedBy int64, date int) (domain.CreateChannelResult, error) {
if invite.InviteID != 0 && invite.UsageLimit > 0 && invite.UsageCount >= invite.UsageLimit {
return domain.CreateChannelResult{}, domain.ErrUsersTooMuch
}
channelID := channel.ID
if channelID == 0 {
channelID = invite.ChannelID
}
if existing, ok := s.members[channelID][userID]; ok {
if existing.Status == domain.ChannelMemberActive {
return domain.CreateChannelResult{}, domain.ErrUserAlreadyParticipant
}
if existing.Status == domain.ChannelMemberKicked || existing.Status == domain.ChannelMemberBanned || existing.BannedRights.ViewMessages {
return domain.CreateChannelResult{}, domain.ErrInviteHashInvalid
}
}
preJoinTopID := channel.TopMessageID
minID := channelInitialAvailableMinID(channel)
inviterID := invite.AdminUserID
if inviterID == 0 {
inviterID = approvedBy
}
member := domain.ChannelMember{
ChannelID: channelID,
UserID: userID,
InviterUserID: inviterID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
JoinedAt: date,
AvailableMinID: minID,
AvailableMinPts: channelInitialAvailableMinPts(channel),
ReadInboxMaxID: maxInt(minID, preJoinTopID),
}
if s.members[channelID] == nil {
s.members[channelID] = make(map[int64]domain.ChannelMember)
}
s.members[channelID][userID] = member
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: date,
Type: domain.ChannelAdminLogParticipantJoin,
})
if importer, ok := s.importers[channelID][userID]; ok && importer.Requested {
if importer.InviteID == invite.InviteID {
if invite.InviteID != 0 && invite.RequestedCount > 0 {
invite.RequestedCount--
}
} else if importer.InviteID != 0 {
if pendingInvite, err := s.inviteByIDLocked(channelID, importer.InviteID); err == nil && pendingInvite.RequestedCount > 0 {
pendingInvite.RequestedCount--
s.invites[pendingInvite.Hash] = pendingInvite
}
}
}
if invite.InviteID != 0 && invite.Hash != "" {
invite.UsageCount++
s.invites[invite.Hash] = invite
}
s.refreshChannelCountsLocked(channelID)
channel = s.channels[channelID]
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 {
member.ReadOutboxMaxID = maxInt(member.ReadOutboxMaxID, msg.ID)
}
s.members[channelID][userID] = member
s.upsertChannelDialogLocked(userID, channel, msg, true)
if s.importers[channelID] == nil {
s.importers[channelID] = make(map[int64]domain.ChannelInviteImporter)
}
s.importers[channelID][userID] = domain.ChannelInviteImporter{
ChannelID: channelID,
InviteID: invite.InviteID,
UserID: userID,
Date: date,
ApprovedBy: approvedBy,
}
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) 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) recordPendingInviteRequestLocked(invite domain.ChannelInvite, userID int64, date int) error {
if existing, ok := s.members[invite.ChannelID][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[invite.ChannelID] == nil {
s.importers[invite.ChannelID] = make(map[int64]domain.ChannelInviteImporter)
}
if existing, ok := s.importers[invite.ChannelID][userID]; ok && existing.Requested {
return domain.ErrInviteRequestSent
}
s.importers[invite.ChannelID][userID] = domain.ChannelInviteImporter{
ChannelID: invite.ChannelID,
InviteID: invite.InviteID,
UserID: userID,
Date: date,
Requested: true,
}
invite.RequestedCount++
s.invites[invite.Hash] = invite
return nil
}
func (s *ChannelStore) deletePendingInviteImporterLocked(invite domain.ChannelInvite, userID int64) {
if existing, ok := s.importers[invite.ChannelID][userID]; ok && existing.Requested {
delete(s.importers[invite.ChannelID], userID)
if invite.InviteID != 0 && invite.Hash != "" && invite.RequestedCount > 0 {
invite.RequestedCount--
s.invites[invite.Hash] = invite
}
}
}
func (s *ChannelStore) inviteByChannelHashLocked(channelID int64, hash string) (domain.ChannelInvite, error) {
hash = strings.TrimSpace(hash)
if hash == "" {
return domain.ChannelInvite{}, domain.ErrInviteHashEmpty
}
invite, ok := s.invites[hash]
if !ok || invite.ChannelID != channelID {
return domain.ChannelInvite{}, domain.ErrInviteHashInvalid
}
return invite, nil
}
func (s *ChannelStore) inviteByIDLocked(channelID, inviteID int64) (domain.ChannelInvite, error) {
for _, invite := range s.invites {
if invite.ChannelID == channelID && invite.InviteID == inviteID {
return invite, nil
}
}
return domain.ChannelInvite{}, domain.ErrInviteHashInvalid
}
func (s *ChannelStore) newReplacementInviteLocked(old domain.ChannelInvite, date int) (domain.ChannelInvite, error) {
inviteID, err := randomMemoryPositiveInt64()
if err != nil {
return domain.ChannelInvite{}, err
}
hash, err := randomMemoryInviteHash()
if err != nil {
return domain.ChannelInvite{}, err
}
if date == 0 {
date = int(time.Now().Unix())
}
return domain.ChannelInvite{
ChannelID: old.ChannelID,
InviteID: inviteID,
Hash: hash,
AdminUserID: old.AdminUserID,
Permanent: old.Permanent,
Date: date,
}, nil
}
func cloneChannelInvites(in []domain.ChannelInvite) []domain.ChannelInvite {
if len(in) == 0 {
return nil
}
out := make([]domain.ChannelInvite, len(in))
copy(out, in)
return out
}
func cloneChannelInviteImporters(in []domain.ChannelInviteImporter) []domain.ChannelInviteImporter {
if len(in) == 0 {
return nil
}
out := make([]domain.ChannelInviteImporter, len(in))
copy(out, in)
return out
}
func (s *ChannelStore) ListChannelDialogs(_ context.Context, viewerUserID int64, filter domain.DialogFilter) (domain.ChannelDialogList, error) {
if viewerUserID == 0 {
return domain.ChannelDialogList{}, nil
}
limit := filter.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
s.mu.RLock()
defer s.mu.RUnlock()
channelIDs := make([]int64, 0, len(s.dialogs[viewerUserID]))
seen := make(map[int64]struct{}, len(s.dialogs[viewerUserID]))
for channelID := range s.dialogs[viewerUserID] {
channelIDs = append(channelIDs, channelID)
seen[channelID] = struct{}{}
}
for channelID, members := range s.members {
if _, ok := seen[channelID]; ok {
continue
}
if member, ok := members[viewerUserID]; ok && member.Status == domain.ChannelMemberActive {
channelIDs = append(channelIDs, channelID)
seen[channelID] = struct{}{}
}
}
items := make([]domain.Dialog, 0, len(channelIDs))
for _, channelID := range channelIDs {
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
continue
}
if _, err := s.channelForMemberLocked(viewerUserID, channelID); err != nil {
continue
}
item := channelDialogToDialog(s.dialogForUserLocked(viewerUserID, channel))
if !channelDialogMatchesFilter(item, channel, filter) {
continue
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Pinned != items[j].Pinned {
return items[i].Pinned
}
if items[i].PinnedOrder != items[j].PinnedOrder {
return items[i].PinnedOrder > items[j].PinnedOrder
}
if items[i].TopMessageDate != items[j].TopMessageDate {
return items[i].TopMessageDate > items[j].TopMessageDate
}
if items[i].TopMessage != items[j].TopMessage {
return items[i].TopMessage > items[j].TopMessage
}
return items[i].Peer.ID > items[j].Peer.ID
})
out := domain.ChannelDialogList{Count: len(items)}
for _, dialog := range items {
if len(out.Dialogs) >= limit {
break
}
out.Dialogs = append(out.Dialogs, dialog)
channel := s.channels[dialog.Peer.ID]
out.Channels = append(out.Channels, channel)
if msg, ok := s.findMessageLocked(dialog.Peer.ID, dialog.TopMessage); ok && !msg.Deleted {
out.Messages = append(out.Messages, cloneChannelMessage(msg))
}
}
return out, nil
}
func (s *ChannelStore) GetChannelDialogs(_ context.Context, viewerUserID int64, channelIDs []int64) (domain.ChannelDialogList, error) {
if viewerUserID == 0 || len(channelIDs) == 0 {
return domain.ChannelDialogList{}, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
out := domain.ChannelDialogList{}
seen := make(map[int64]struct{}, len(channelIDs))
for _, channelID := range channelIDs {
if channelID == 0 {
continue
}
if _, ok := seen[channelID]; ok {
continue
}
seen[channelID] = struct{}{}
channel, err := s.channelForMemberLocked(viewerUserID, channelID)
if err != nil {
continue
}
dialog := channelDialogToDialog(s.dialogForUserLocked(viewerUserID, channel))
out.Dialogs = append(out.Dialogs, dialog)
out.Channels = append(out.Channels, channel)
if msg, ok := s.findMessageLocked(channelID, dialog.TopMessage); ok && !msg.Deleted {
out.Messages = append(out.Messages, cloneChannelMessage(msg))
}
}
out.Count = len(out.Dialogs)
return out, nil
}
func (s *ChannelStore) ListCommonChannels(_ context.Context, req domain.CommonChannelsRequest) (domain.CommonChannelsResult, error) {
if req.UserID == 0 || req.TargetUserID == 0 || req.UserID == req.TargetUserID || req.MaxID < 0 {
return domain.CommonChannelsResult{}, domain.ErrChannelInvalid
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxCommonChannelsLimit {
limit = domain.MaxCommonChannelsLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
ids := make([]int64, 0)
for channelID, members := range s.members {
self, selfOK := members[req.UserID]
target, targetOK := members[req.TargetUserID]
if !selfOK || !targetOK || self.Status != domain.ChannelMemberActive || target.Status != domain.ChannelMemberActive {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted || !channel.Megagroup || channel.Broadcast {
continue
}
ids = append(ids, channelID)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
out := domain.CommonChannelsResult{Count: len(ids)}
if req.CountOnly {
return out, nil
}
for _, channelID := range ids {
if req.MaxID > 0 && channelID <= req.MaxID {
continue
}
out.Channels = append(out.Channels, cloneChannel(s.channels[channelID]))
if len(out.Channels) >= limit {
break
}
}
return out, nil
}
func (s *ChannelStore) ListLeftChannels(_ context.Context, userID int64, offset, limit int) (domain.LeftChannelsResult, error) {
if userID == 0 || offset < 0 || offset > domain.MaxLeftChannelsOffset {
return domain.LeftChannelsResult{}, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxLeftChannelsLimit {
limit = domain.MaxLeftChannelsLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
all := make([]domain.LeftChannel, 0)
for channelID, members := range s.members {
member, ok := members[userID]
if !ok || member.Status != domain.ChannelMemberLeft {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted || (!channel.Broadcast && !channel.Megagroup) {
continue
}
all = append(all, domain.LeftChannel{
Channel: cloneChannel(channel),
Self: member,
})
}
sort.Slice(all, func(i, j int) bool {
if all[i].Self.LeftAt != all[j].Self.LeftAt {
return all[i].Self.LeftAt > all[j].Self.LeftAt
}
return all[i].Channel.ID > all[j].Channel.ID
})
out := domain.LeftChannelsResult{Count: len(all)}
if offset >= len(all) {
return out, nil
}
end := offset + limit
if end > len(all) {
end = len(all)
}
out.Channels = append(out.Channels, all[offset:end]...)
return out, nil
}
func (s *ChannelStore) ListInactiveChannels(_ context.Context, userID int64, limit int) (domain.ChannelDialogList, error) {
if userID == 0 {
return domain.ChannelDialogList{}, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxInactiveChannelsLimit {
limit = domain.MaxInactiveChannelsLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
type item struct {
channel domain.Channel
dialog domain.Dialog
}
items := make([]item, 0, limit)
for channelID, members := range s.members {
member, ok := members[userID]
if !ok || member.Status != domain.ChannelMemberActive {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted || (!channel.Broadcast && !channel.Megagroup) {
continue
}
if _, err := s.channelForMemberLocked(userID, channelID); err != nil {
continue
}
dialog := channelDialogToDialog(s.dialogForUserLocked(userID, channel))
dialog.TopMessageDate = inactiveChannelDate(dialog, channel, member)
items = append(items, item{channel: cloneChannel(channel), dialog: dialog})
}
sort.Slice(items, func(i, j int) bool {
if items[i].dialog.TopMessageDate != items[j].dialog.TopMessageDate {
return items[i].dialog.TopMessageDate < items[j].dialog.TopMessageDate
}
if items[i].dialog.TopMessage != items[j].dialog.TopMessage {
return items[i].dialog.TopMessage < items[j].dialog.TopMessage
}
return items[i].channel.ID < items[j].channel.ID
})
if len(items) > limit {
items = items[:limit]
}
out := domain.ChannelDialogList{Count: len(items)}
for _, item := range items {
out.Dialogs = append(out.Dialogs, item.dialog)
out.Channels = append(out.Channels, item.channel)
}
return out, nil
}
func (s *ChannelStore) ListChannelRecommendations(_ context.Context, req domain.ChannelRecommendationsRequest) (domain.ChannelRecommendationsResult, error) {
if req.UserID == 0 || req.SourceChannelID < 0 {
return domain.ChannelRecommendationsResult{}, domain.ErrChannelInvalid
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelRecommendationsLimit {
limit = domain.DefaultChannelRecommendationsLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]domain.Channel, 0, limit)
for channelID, channel := range s.channels {
if !recommendableChannel(channel) || channelID == req.SourceChannelID {
continue
}
if req.SourceChannelID == 0 {
if member, ok := s.members[channelID][req.UserID]; ok && member.Status == domain.ChannelMemberActive {
continue
}
}
items = append(items, cloneChannel(channel))
}
sort.Slice(items, func(i, j int) bool {
if items[i].ParticipantsCount != items[j].ParticipantsCount {
return items[i].ParticipantsCount > items[j].ParticipantsCount
}
if items[i].Date != items[j].Date {
return items[i].Date > items[j].Date
}
return items[i].ID > items[j].ID
})
out := domain.ChannelRecommendationsResult{Count: len(items)}
if len(items) > limit {
items = items[:limit]
}
out.Channels = append(out.Channels, items...)
return out, nil
}
func (s *ChannelStore) ListDiscussionGroups(_ context.Context, userID int64, limit int) ([]domain.Channel, error) {
if userID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxDiscussionGroupsLimit {
limit = domain.MaxDiscussionGroupsLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]domain.Channel, 0, limit)
for channelID, channel := range s.channels {
if !validDiscussionGroup(channel) || channel.Deleted {
continue
}
member := s.members[channelID][userID]
if member.Status != domain.ChannelMemberActive || !canManageDiscussionGroup(member) {
continue
}
items = append(items, cloneChannel(channel))
}
sort.Slice(items, func(i, j int) bool {
return items[i].ID > items[j].ID
})
if len(items) > limit {
items = items[:limit]
}
return items, nil
}
func (s *ChannelStore) SetDiscussionGroup(_ context.Context, userID, broadcastID, groupID int64) (domain.DiscussionGroupUpdateResult, error) {
if userID == 0 {
return domain.DiscussionGroupUpdateResult{}, domain.ErrChannelInvalid
}
if broadcastID == 0 && groupID == 0 {
return domain.DiscussionGroupUpdateResult{}, domain.ErrLinkNotModified
}
s.mu.Lock()
defer s.mu.Unlock()
changed := make(map[int64]domain.Channel)
markChanged := func(channel domain.Channel) {
if channel.ID != 0 {
changed[channel.ID] = cloneChannel(channel)
}
}
setLinked := func(channelID, linkedID int64) (domain.Channel, bool) {
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, false
}
if channel.LinkedChatID == linkedID {
return channel, true
}
channel.LinkedChatID = linkedID
s.channels[channelID] = channel
markChanged(channel)
return channel, true
}
if broadcastID == 0 {
group, groupMember, err := s.channelAndMemberLocked(userID, groupID)
if err != nil || !validDiscussionGroup(group) {
return domain.DiscussionGroupUpdateResult{}, domain.ErrMegagroupIDInvalid
}
if !canManageDiscussionGroup(groupMember) {
return domain.DiscussionGroupUpdateResult{}, domain.ErrChannelAdminRequired
}
oldBroadcastID := group.LinkedChatID
if oldBroadcastID == 0 {
return domain.DiscussionGroupUpdateResult{}, domain.ErrLinkNotModified
}
if oldBroadcast, ok := s.channels[oldBroadcastID]; ok && oldBroadcast.LinkedChatID == groupID {
if updated, ok := setLinked(oldBroadcastID, 0); ok {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: updated.ID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogChangeLinkedChat,
PrevInt: int(groupID),
NewInt: 0,
})
}
}
setLinked(groupID, 0)
return discussionGroupUpdateResult(changed), nil
}
broadcast, broadcastMember, err := s.channelAndMemberLocked(userID, broadcastID)
if err != nil || !broadcast.Broadcast || broadcast.Megagroup {
return domain.DiscussionGroupUpdateResult{}, domain.ErrBroadcastIDInvalid
}
if !canManageDiscussionBroadcast(broadcastMember) {
return domain.DiscussionGroupUpdateResult{}, domain.ErrChannelAdminRequired
}
oldGroupID := broadcast.LinkedChatID
if groupID == 0 {
if oldGroupID == 0 {
return domain.DiscussionGroupUpdateResult{}, domain.ErrLinkNotModified
}
updated, _ := setLinked(broadcastID, 0)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: updated.ID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogChangeLinkedChat,
PrevInt: int(oldGroupID),
NewInt: 0,
})
if oldGroup, ok := s.channels[oldGroupID]; ok && oldGroup.LinkedChatID == broadcastID {
setLinked(oldGroupID, 0)
}
return discussionGroupUpdateResult(changed), nil
}
group, groupMember, err := s.channelAndMemberLocked(userID, groupID)
if err != nil || !validDiscussionGroup(group) {
return domain.DiscussionGroupUpdateResult{}, domain.ErrMegagroupIDInvalid
}
if group.PreHistoryHidden {
return domain.DiscussionGroupUpdateResult{}, domain.ErrMegagroupPrehistoryHidden
}
if !canManageDiscussionGroup(groupMember) {
return domain.DiscussionGroupUpdateResult{}, domain.ErrChannelAdminRequired
}
if oldGroupID == groupID && group.LinkedChatID == broadcastID {
return domain.DiscussionGroupUpdateResult{}, domain.ErrLinkNotModified
}
oldBroadcastID := group.LinkedChatID
if oldGroupID != 0 && oldGroupID != groupID {
if oldGroup, ok := s.channels[oldGroupID]; ok && oldGroup.LinkedChatID == broadcastID {
setLinked(oldGroupID, 0)
}
}
if oldBroadcastID != 0 && oldBroadcastID != broadcastID {
if oldBroadcast, ok := s.channels[oldBroadcastID]; ok && oldBroadcast.LinkedChatID == groupID {
if updated, ok := setLinked(oldBroadcastID, 0); ok {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: updated.ID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogChangeLinkedChat,
PrevInt: int(groupID),
NewInt: 0,
})
}
}
}
updatedBroadcast, _ := setLinked(broadcastID, groupID)
setLinked(groupID, broadcastID)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: updatedBroadcast.ID,
UserID: userID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogChangeLinkedChat,
PrevInt: int(oldGroupID),
NewInt: int(groupID),
})
return discussionGroupUpdateResult(changed), nil
}
func (s *ChannelStore) SetChannelDialogPinned(_ context.Context, userID, channelID int64, pinned bool) (bool, error) {
if userID == 0 || channelID == 0 {
return false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
return false, nil
}
nextOrder := 1
for _, dialog := range s.dialogs[userID] {
if dialog.Pinned && dialog.PinnedOrder >= nextOrder {
nextOrder = dialog.PinnedOrder + 1
}
}
dialog := s.dialogForUserLocked(userID, channel)
changed := dialog.Pinned != pinned || (pinned && dialog.PinnedOrder == 0)
dialog.Pinned = pinned
if pinned {
if dialog.PinnedOrder == 0 {
dialog.PinnedOrder = nextOrder
}
} else {
dialog.PinnedOrder = 0
}
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
return changed, nil
}
func (s *ChannelStore) ReorderChannelPinnedDialogs(_ context.Context, userID int64, order []domain.Peer, force bool) error {
if userID == 0 {
return nil
}
positions := make(map[int64]int, len(order))
for i, peer := range order {
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
continue
}
if _, ok := positions[peer.ID]; ok {
continue
}
positions[peer.ID] = len(order) - i
}
s.mu.Lock()
defer s.mu.Unlock()
for channelID, dialog := range s.dialogs[userID] {
if pos, ok := positions[channelID]; ok {
dialog.Pinned = true
dialog.PinnedOrder = pos
s.dialogs[userID][channelID] = dialog
continue
}
if force && dialog.Pinned {
dialog.Pinned = false
dialog.PinnedOrder = 0
s.dialogs[userID][channelID] = dialog
}
}
return nil
}
func (s *ChannelStore) SetChannelDialogUnreadMark(_ context.Context, userID, channelID int64, unread bool) (bool, error) {
if userID == 0 || channelID == 0 {
return false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
return false, nil
}
dialog := s.dialogForUserLocked(userID, channel)
changed := dialog.UnreadMark != unread
dialog.UnreadMark = unread
member := s.members[channelID][userID]
member.UnreadMark = unread
s.members[channelID][userID] = member
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
return changed, nil
}
func (s *ChannelStore) SetChannelViewForumAsMessages(_ context.Context, userID, channelID int64, enabled bool) (bool, error) {
if userID == 0 || channelID == 0 {
return false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
return false, nil
}
dialog := s.dialogForUserLocked(userID, channel)
changed := dialog.ViewForumAsMessages != enabled
dialog.ViewForumAsMessages = enabled
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
return changed, nil
}
func (s *ChannelStore) ListChannelUnreadMarked(_ context.Context, userID int64) ([]domain.Peer, error) {
if userID == 0 {
return nil, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]domain.Peer, 0, len(s.dialogs[userID]))
for channelID, dialog := range s.dialogs[userID] {
if !dialog.UnreadMark {
continue
}
if _, err := s.channelForMemberLocked(userID, channelID); err != nil {
continue
}
out = append(out, domain.Peer{Type: domain.PeerTypeChannel, ID: channelID})
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
func (s *ChannelStore) EditChannelPeerFolders(_ context.Context, userID int64, peers []domain.FolderPeerUpdate) error {
if userID == 0 || len(peers) == 0 {
return nil
}
updates := make(map[int64]int, len(peers))
for _, item := range peers {
if item.Peer.Type != domain.PeerTypeChannel || item.Peer.ID == 0 {
continue
}
if item.FolderID != domain.DialogMainFolderID && item.FolderID != domain.DialogArchiveFolderID {
continue
}
updates[item.Peer.ID] = item.FolderID
}
if len(updates) == 0 {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
for channelID, folderID := range updates {
channel, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
continue
}
dialog := s.dialogForUserLocked(userID, channel)
dialog.FolderID = folderID
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
}
return nil
}
func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64, filter domain.ChannelHistoryFilter) (domain.ChannelHistory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, _, err := s.channelForViewerLocked(viewerUserID, filter.ChannelID)
if err != nil {
return domain.ChannelHistory{}, err
}
limit := filter.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
items := append([]domain.ChannelMessage(nil), s.messages[filter.ChannelID]...)
sort.Slice(items, func(i, j int) bool { return items[i].ID > items[j].ID })
out := make([]domain.ChannelMessage, 0, limit)
query := strings.ToLower(strings.TrimSpace(filter.Query))
matched := 0
for _, msg := range items {
if msg.Deleted {
continue
}
if msg.ID <= member.AvailableMinID {
continue
}
if query != "" && !strings.Contains(strings.ToLower(msg.Body), query) {
continue
}
if filter.SenderUserID != 0 && msg.SenderUserID != filter.SenderUserID {
continue
}
if filter.MinDate > 0 && msg.Date <= filter.MinDate {
continue
}
if filter.MaxDate > 0 && msg.Date >= filter.MaxDate {
continue
}
if filter.OffsetID > 0 && msg.ID >= filter.OffsetID {
continue
}
if filter.OffsetID <= 0 && filter.OffsetDate > 0 && msg.Date >= filter.OffsetDate {
continue
}
if filter.MaxID > 0 && msg.ID > filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
matched++
if len(out) < limit {
out = append(out, cloneChannelMessage(msg))
}
}
s.populateChannelMessageRepliesLocked(viewerUserID, filter.ChannelID, out)
s.populateChannelMessageReactionsLocked(viewerUserID, channel, out)
return domain.ChannelHistory{
Channel: channel,
Self: member,
Messages: out,
Count: matched,
}, nil
}
func (s *ChannelStore) SearchPublicPosts(_ context.Context, viewerUserID int64, req domain.ChannelSearchPostsRequest) (domain.ChannelHistory, error) {
query := strings.ToLower(strings.TrimSpace(req.Query))
hashtag := strings.ToLower(strings.TrimSpace(req.Hashtag))
if (query == "") == (hashtag == "") {
return domain.ChannelHistory{}, domain.ErrChannelInvalid
}
if req.Limit <= 0 || req.Limit > domain.MaxChannelSearchPostsLimit {
req.Limit = domain.MaxChannelSearchPostsLimit
}
type hit struct {
channel domain.Channel
message domain.ChannelMessage
}
s.mu.RLock()
defer s.mu.RUnlock()
hits := make([]hit, 0, req.Limit+1)
for channelID, channel := range s.channels {
if channel.Deleted || strings.TrimSpace(channel.Username) == "" {
continue
}
for _, msg := range s.messages[channelID] {
if msg.Deleted || strings.TrimSpace(msg.Body) == "" {
continue
}
if !channelSearchPostAfterCursor(msg, req) {
continue
}
body := strings.ToLower(msg.Body)
if query != "" && !strings.Contains(body, query) {
continue
}
if hashtag != "" && !strings.Contains(body, "#"+hashtag) {
continue
}
hits = append(hits, hit{channel: channel, message: cloneChannelMessage(msg)})
}
}
sort.Slice(hits, func(i, j int) bool {
a, b := hits[i].message, hits[j].message
if a.Date != b.Date {
return a.Date > b.Date
}
if a.ChannelID != b.ChannelID {
return a.ChannelID > b.ChannelID
}
return a.ID > b.ID
})
out := domain.ChannelHistory{Count: len(hits)}
if out.Count > req.Limit {
out.Count = req.Limit + 1
hits = hits[:req.Limit]
}
channelSeen := make(map[int64]struct{}, len(hits))
for _, h := range hits {
out.Messages = append(out.Messages, h.message)
if _, ok := channelSeen[h.channel.ID]; ok {
continue
}
channelSeen[h.channel.ID] = struct{}{}
out.Channels = append(out.Channels, h.channel)
}
s.populateChannelMessagesReactionsLocked(viewerUserID, out.Channels, out.Messages)
return out, nil
}
func (s *ChannelStore) SearchJoinedMessages(_ context.Context, viewerUserID int64, req domain.ChannelGlobalSearchRequest) (domain.ChannelHistory, error) {
query := strings.ToLower(strings.TrimSpace(req.Query))
if viewerUserID == 0 || query == "" {
return domain.ChannelHistory{}, domain.ErrChannelInvalid
}
if req.Limit <= 0 || req.Limit > domain.MaxChannelGlobalSearchLimit {
req.Limit = domain.MaxChannelGlobalSearchLimit
}
type hit struct {
channel domain.Channel
message domain.ChannelMessage
}
s.mu.RLock()
defer s.mu.RUnlock()
hits := make([]hit, 0, req.Limit+1)
for channelID, channel := range s.channels {
if channel.Deleted {
continue
}
if req.BroadcastsOnly && (!channel.Broadcast || channel.Megagroup) {
continue
}
if req.GroupsOnly && !channel.Megagroup {
continue
}
member, ok := s.members[channelID][viewerUserID]
if !ok || member.Status != domain.ChannelMemberActive || member.BannedRights.ViewMessages {
continue
}
if req.HasFolderID {
dialog, ok := s.dialogs[viewerUserID][channelID]
if !ok || dialog.FolderID != req.FolderID {
continue
}
}
for _, msg := range s.messages[channelID] {
if msg.Deleted || strings.TrimSpace(msg.Body) == "" {
continue
}
if member.AvailableMinID > 0 && msg.ID <= member.AvailableMinID {
continue
}
if req.MinDate > 0 && msg.Date <= req.MinDate {
continue
}
if req.MaxDate > 0 && msg.Date >= req.MaxDate {
continue
}
if !channelGlobalSearchAfterCursor(msg, req) {
continue
}
if !strings.Contains(strings.ToLower(msg.Body), query) {
continue
}
hits = append(hits, hit{channel: channel, message: cloneChannelMessage(msg)})
}
}
sort.Slice(hits, func(i, j int) bool {
a, b := hits[i].message, hits[j].message
if a.Date != b.Date {
return a.Date > b.Date
}
if a.ChannelID != b.ChannelID {
return a.ChannelID > b.ChannelID
}
return a.ID > b.ID
})
out := domain.ChannelHistory{Count: len(hits)}
if out.Count > req.Limit {
out.Count = req.Limit + 1
hits = hits[:req.Limit]
}
channelSeen := make(map[int64]struct{}, len(hits))
for _, h := range hits {
out.Messages = append(out.Messages, h.message)
if _, ok := channelSeen[h.channel.ID]; ok {
continue
}
channelSeen[h.channel.ID] = struct{}{}
out.Channels = append(out.Channels, h.channel)
}
s.populateChannelMessagesReactionsLocked(viewerUserID, out.Channels, out.Messages)
return out, nil
}
func channelSearchPostAfterCursor(msg domain.ChannelMessage, req domain.ChannelSearchPostsRequest) bool {
if req.OffsetRate <= 0 && req.OffsetChannelID <= 0 && req.OffsetID <= 0 {
return true
}
if req.OffsetRate > 0 {
if msg.Date < req.OffsetRate {
return true
}
if msg.Date > req.OffsetRate {
return false
}
}
if req.OffsetChannelID > 0 {
if msg.ChannelID < req.OffsetChannelID {
return true
}
if msg.ChannelID > req.OffsetChannelID {
return false
}
}
if req.OffsetID > 0 {
return msg.ID < req.OffsetID
}
return false
}
func channelGlobalSearchAfterCursor(msg domain.ChannelMessage, req domain.ChannelGlobalSearchRequest) bool {
if req.OffsetRate <= 0 && req.OffsetChannelID <= 0 && req.OffsetID <= 0 {
return true
}
if req.OffsetRate > 0 {
if msg.Date < req.OffsetRate {
return true
}
if msg.Date > req.OffsetRate {
return false
}
}
if req.OffsetChannelID > 0 {
if msg.ChannelID < req.OffsetChannelID {
return true
}
if msg.ChannelID > req.OffsetChannelID {
return false
}
}
if req.OffsetID > 0 {
return msg.ID < req.OffsetID
}
return false
}
func (s *ChannelStore) GetChannelMessages(_ context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelHistory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID)
if err != nil {
return domain.ChannelHistory{}, err
}
if len(ids) == 0 {
return domain.ChannelHistory{Channel: channel, Self: member}, nil
}
if len(ids) > domain.MaxGetMessageIDs {
return domain.ChannelHistory{}, domain.ErrChannelInvalid
}
wanted := make(map[int]struct{}, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ChannelHistory{}, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
messages := make([]domain.ChannelMessage, 0, len(wanted))
for _, msg := range s.messages[channelID] {
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
messages = append(messages, cloneChannelMessage(msg))
}
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
s.populateChannelMessageRepliesLocked(viewerUserID, channelID, messages)
s.populateChannelMessageReactionsLocked(viewerUserID, channel, messages)
return domain.ChannelHistory{Channel: channel, Self: member, Messages: messages, Count: len(messages)}, nil
}
func (s *ChannelStore) ReadChannelMessageContents(_ context.Context, req domain.ReadChannelMessageContentsRequest) (domain.ReadChannelMessageContentsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ReadChannelMessageContentsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReadChannelMessageContentsResult{}, err
}
if len(req.IDs) == 0 {
return domain.ReadChannelMessageContentsResult{Channel: channel}, nil
}
if len(req.IDs) > domain.MaxGetMessageIDs {
return domain.ReadChannelMessageContentsResult{}, domain.ErrChannelInvalid
}
wanted := make(map[int]struct{}, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ReadChannelMessageContentsResult{}, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
messages := make([]domain.ChannelMessage, 0, len(wanted))
for _, msg := range s.messages[req.ChannelID] {
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
messages = append(messages, cloneChannelMessage(msg))
}
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
clearedSet := make(map[int]struct{})
for _, msg := range messages {
byUser := s.reactions[req.ChannelID][msg.ID]
if len(byUser) == 0 {
continue
}
for reactedUserID, rows := range byUser {
changed := false
for i := range rows {
if rows[i].SenderUserID == req.UserID && rows[i].UserID != req.UserID && rows[i].Unread {
rows[i].Unread = false
changed = true
clearedSet[msg.ID] = struct{}{}
}
}
if changed {
byUser[reactedUserID] = rows
}
}
}
cleared := make([]int, 0, len(clearedSet))
for id := range clearedSet {
cleared = append(cleared, id)
}
sort.Sort(sort.Reverse(sort.IntSlice(cleared)))
if len(cleared) > 0 {
s.refreshChannelUnreadReactionsDialogLocked(req.UserID, req.ChannelID)
}
s.populateChannelMessageRepliesLocked(req.UserID, req.ChannelID, messages)
s.populateChannelMessageReactionsLocked(req.UserID, channel, messages)
return domain.ReadChannelMessageContentsResult{
Channel: channel,
Messages: messages,
ClearedUnreadReactionMessageIDs: cleared,
}, nil
}
func (s *ChannelStore) GetChannelMessageViews(_ context.Context, req domain.ChannelMessageViewsRequest) (domain.ChannelMessageViewsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ChannelMessageViewsResult{}, domain.ErrChannelInvalid
}
if len(req.IDs) == 0 {
return domain.ChannelMessageViewsResult{Views: map[int]int{}}, nil
}
if len(req.IDs) > domain.MaxGetMessageIDs {
return domain.ChannelMessageViewsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
_, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageViewsResult{}, err
}
wanted := make(map[int]struct{}, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ChannelMessageViewsResult{}, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
visible := make(map[int]struct{}, len(wanted))
for _, msg := range s.messages[req.ChannelID] {
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
visible[msg.ID] = struct{}{}
}
if s.msgViews[req.ChannelID] == nil {
s.msgViews[req.ChannelID] = make(map[int]int)
}
if s.msgViewers[req.ChannelID] == nil {
s.msgViewers[req.ChannelID] = make(map[int]map[int64]struct{})
}
for id := range visible {
if req.Increment {
if s.msgViewers[req.ChannelID][id] == nil {
s.msgViewers[req.ChannelID][id] = make(map[int64]struct{})
}
if _, seen := s.msgViewers[req.ChannelID][id][req.UserID]; !seen {
s.msgViewers[req.ChannelID][id][req.UserID] = struct{}{}
s.msgViews[req.ChannelID][id]++
}
}
}
out := make(map[int]int, len(visible))
for id := range visible {
out[id] = s.msgViews[req.ChannelID][id]
}
return domain.ChannelMessageViewsResult{Views: out}, nil
}
func (s *ChannelStore) SetChannelMessageReactions(_ context.Context, req domain.SetChannelMessageReactionsRequest) (domain.ChannelMessageReactionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
if len(req.Reactions) > domain.MaxChannelMessageReactionsPerUser {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
idx, ok := s.findMessageIndexLocked(req.ChannelID, req.MessageID)
if !ok {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
msg := s.messages[req.ChannelID][idx]
if msg.Deleted || msg.Action != nil || msg.ID <= member.AvailableMinID {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if s.reactions[req.ChannelID] == nil {
s.reactions[req.ChannelID] = make(map[int]map[int64][]domain.ChannelMessagePeerReaction)
}
if s.reactions[req.ChannelID][req.MessageID] == nil {
s.reactions[req.ChannelID][req.MessageID] = make(map[int64][]domain.ChannelMessagePeerReaction)
}
if len(req.Reactions) == 0 {
delete(s.reactions[req.ChannelID][req.MessageID], req.UserID)
} else {
rows := make([]domain.ChannelMessagePeerReaction, 0, len(req.Reactions))
for i, reaction := range req.Reactions {
rows = append(rows, domain.ChannelMessagePeerReaction{
ChannelID: req.ChannelID,
MessageID: req.MessageID,
SenderUserID: msg.SenderUserID,
UserID: req.UserID,
Reaction: reaction,
Big: req.Big,
Unread: msg.SenderUserID != 0 && msg.SenderUserID != req.UserID,
ChosenOrder: i + 1,
Date: req.Date,
})
}
s.reactions[req.ChannelID][req.MessageID][req.UserID] = rows
if s.top[req.UserID] == nil {
s.top[req.UserID] = make(map[string]domain.TopMessageReaction)
}
for _, reaction := range req.Reactions {
key := messageReactionKey(reaction)
row := s.top[req.UserID][key]
row.UserID = req.UserID
row.Reaction = reaction
row.Count++
row.Date = req.Date
s.top[req.UserID][key] = row
}
if req.AddToRecent {
if s.recent[req.UserID] == nil {
s.recent[req.UserID] = make(map[string]domain.RecentMessageReaction)
}
for _, reaction := range req.Reactions {
s.recent[req.UserID][messageReactionKey(reaction)] = domain.RecentMessageReaction{
UserID: req.UserID,
Reaction: reaction,
Date: req.Date,
}
}
}
}
s.refreshChannelUnreadReactionsDialogLocked(msg.SenderUserID, req.ChannelID)
reactions := s.channelMessageReactionsLocked(req.UserID, channel, req.MessageID)
msg = cloneChannelMessage(msg)
msg.Reactions = cloneChannelMessageReactionsPtr(&reactions)
return domain.ChannelMessageReactionsResult{
Channel: cloneChannel(channel),
Message: msg,
Messages: []domain.ChannelMessage{msg},
Reactions: cloneChannelMessageReactions(reactions),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) DeleteChannelParticipantReaction(_ context.Context, req domain.DeleteChannelParticipantReactionRequest) (domain.ChannelMessageReactionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID || req.ParticipantUserID == 0 {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
if !canDeleteAnyChannelMessage(member) {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelAdminRequired
}
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if s.reactions[req.ChannelID] != nil && s.reactions[req.ChannelID][req.MessageID] != nil {
delete(s.reactions[req.ChannelID][req.MessageID], req.ParticipantUserID)
}
s.refreshChannelUnreadReactionsDialogLocked(msg.SenderUserID, req.ChannelID)
reactions := s.channelMessageReactionsLocked(req.UserID, channel, req.MessageID)
outMsg := cloneChannelMessage(msg)
outMsg.Reactions = cloneChannelMessageReactionsPtr(&reactions)
return domain.ChannelMessageReactionsResult{
Channel: cloneChannel(channel),
Message: outMsg,
Messages: []domain.ChannelMessage{outMsg},
Reactions: cloneChannelMessageReactions(reactions),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) DeleteChannelParticipantReactions(_ context.Context, req domain.DeleteChannelParticipantReactionsRequest) (domain.DeleteChannelParticipantReactionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.ParticipantUserID == 0 {
return domain.DeleteChannelParticipantReactionsResult{}, domain.ErrChannelInvalid
}
if req.Limit <= 0 || req.Limit > domain.MaxDeleteParticipantReactionsBatch {
req.Limit = domain.MaxDeleteParticipantReactionsBatch
}
s.mu.Lock()
defer s.mu.Unlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelParticipantReactionsResult{}, err
}
if !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelParticipantReactionsResult{}, domain.ErrChannelAdminRequired
}
type reactionMsg struct {
id int
sender int64
date int
}
candidates := make([]reactionMsg, 0)
for msgID, byUser := range s.reactions[req.ChannelID] {
rows := byUser[req.ParticipantUserID]
if len(rows) == 0 {
continue
}
msg, ok := s.findMessageLocked(req.ChannelID, msgID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
item := reactionMsg{id: msgID, sender: msg.SenderUserID}
for _, row := range rows {
if row.Date > item.date {
item.date = row.Date
}
}
candidates = append(candidates, item)
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].date != candidates[j].date {
return candidates[i].date > candidates[j].date
}
return candidates[i].id > candidates[j].id
})
if len(candidates) > req.Limit {
candidates = candidates[:req.Limit]
}
owners := make(map[int64]struct{})
ids := make([]int, 0, len(candidates))
for _, item := range candidates {
if s.reactions[req.ChannelID] != nil && s.reactions[req.ChannelID][item.id] != nil {
delete(s.reactions[req.ChannelID][item.id], req.ParticipantUserID)
}
if item.sender != 0 {
owners[item.sender] = struct{}{}
}
ids = append(ids, item.id)
}
for ownerID := range owners {
s.refreshChannelUnreadReactionsDialogLocked(ownerID, req.ChannelID)
}
messages := make([]domain.ChannelMessage, 0, len(ids))
for _, id := range ids {
msg, ok := s.findMessageLocked(req.ChannelID, id)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
reactions := s.channelMessageReactionsLocked(req.UserID, channel, id)
outMsg := cloneChannelMessage(msg)
outMsg.Reactions = cloneChannelMessageReactionsPtr(&reactions)
messages = append(messages, outMsg)
}
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
return domain.DeleteChannelParticipantReactionsResult{
Channel: cloneChannel(channel),
Messages: messages,
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Deleted: len(ids),
}, nil
}
func (s *ChannelStore) GetChannelMessageReactions(_ context.Context, req domain.ChannelMessageReactionsRequest) (domain.ChannelMessageReactionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
if len(req.IDs) > domain.MaxGetMessageIDs {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
wanted := make(map[int]struct{}, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
messages := make([]domain.ChannelMessage, 0, len(wanted))
for _, msg := range s.messages[req.ChannelID] {
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
item := cloneChannelMessage(msg)
reactions := s.channelMessageReactionsLocked(req.UserID, channel, msg.ID)
item.Reactions = cloneChannelMessageReactionsPtr(&reactions)
messages = append(messages, item)
}
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
res := domain.ChannelMessageReactionsResult{
Channel: cloneChannel(channel),
Messages: messages,
}
if len(messages) == 1 {
res.Message = messages[0]
if messages[0].Reactions != nil {
res.Reactions = cloneChannelMessageReactions(*messages[0].Reactions)
}
}
return res, nil
}
func (s *ChannelStore) ListChannelMessageReactions(_ context.Context, req domain.ChannelMessageReactionsListRequest) (domain.ChannelMessageReactionsList, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID {
return domain.ChannelMessageReactionsList{}, domain.ErrChannelInvalid
}
if req.Limit <= 0 || req.Limit > domain.MaxChannelMessageReactionListLimit {
req.Limit = domain.MaxChannelMessageReactionListLimit
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsList{}, err
}
if channel.Broadcast && !channel.Megagroup {
return domain.ChannelMessageReactionsList{}, domain.ErrChannelRightForbidden
}
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
return domain.ChannelMessageReactionsList{}, domain.ErrMessageIDInvalid
}
rows := s.channelMessageReactionRowsLocked(req.ChannelID, req.MessageID, req.UserID, req.Reaction)
sort.Slice(rows, func(i, j int) bool {
if rows[i].Date != rows[j].Date {
return rows[i].Date > rows[j].Date
}
if rows[i].UserID != rows[j].UserID {
return rows[i].UserID > rows[j].UserID
}
return rows[i].Reaction.Emoticon < rows[j].Reaction.Emoticon
})
total := len(rows)
if req.Offset != "" {
if offset, ok := parseMemoryReactionOffset(req.Offset); ok {
filtered := rows[:0]
for _, row := range rows {
if memoryReactionAfterOffset(row, offset) {
filtered = append(filtered, row)
}
}
rows = filtered
}
}
next := ""
if len(rows) > req.Limit {
rows = rows[:req.Limit]
next = memoryReactionOffset(rows[len(rows)-1])
}
return domain.ChannelMessageReactionsList{
Channel: cloneChannel(channel),
Message: cloneChannelMessage(msg),
Count: total,
Reactions: cloneChannelPeerReactions(rows),
NextOffset: next,
}, nil
}
func (s *ChannelStore) RecordMessageReactionUse(_ context.Context, userID int64, reactions []domain.MessageReaction, addToRecent bool, date int) error {
if userID == 0 || len(reactions) == 0 {
return nil
}
if date == 0 {
date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
if s.top[userID] == nil {
s.top[userID] = make(map[string]domain.TopMessageReaction)
}
if addToRecent && s.recent[userID] == nil {
s.recent[userID] = make(map[string]domain.RecentMessageReaction)
}
for _, reaction := range reactions {
if reaction.Type != domain.MessageReactionEmoji || strings.TrimSpace(reaction.Emoticon) == "" {
continue
}
key := messageReactionKey(reaction)
row := s.top[userID][key]
row.UserID = userID
row.Reaction = reaction
row.Count++
row.Date = date
s.top[userID][key] = row
if addToRecent {
s.recent[userID][key] = domain.RecentMessageReaction{
UserID: userID,
Reaction: reaction,
Date: date,
}
}
}
return nil
}
func (s *ChannelStore) ListTopMessageReactions(_ context.Context, userID int64, limit int) ([]domain.MessageReaction, error) {
if userID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 {
return []domain.MessageReaction{}, nil
}
if limit > domain.MaxTopMessageReactions {
limit = domain.MaxTopMessageReactions
}
s.mu.RLock()
defer s.mu.RUnlock()
rows := make([]domain.TopMessageReaction, 0, len(s.top[userID]))
for _, row := range s.top[userID] {
rows = append(rows, row)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Count != rows[j].Count {
return rows[i].Count > rows[j].Count
}
if rows[i].Date != rows[j].Date {
return rows[i].Date > rows[j].Date
}
if rows[i].Reaction.Type != rows[j].Reaction.Type {
return rows[i].Reaction.Type < rows[j].Reaction.Type
}
return rows[i].Reaction.Emoticon < rows[j].Reaction.Emoticon
})
if len(rows) > limit {
rows = rows[:limit]
}
out := make([]domain.MessageReaction, 0, len(rows))
for _, row := range rows {
out = append(out, row.Reaction)
}
return out, nil
}
func (s *ChannelStore) ListRecentMessageReactions(_ context.Context, userID int64, limit int) ([]domain.MessageReaction, error) {
if userID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 {
return []domain.MessageReaction{}, nil
}
if limit > domain.MaxRecentMessageReactions {
limit = domain.MaxRecentMessageReactions
}
s.mu.RLock()
defer s.mu.RUnlock()
rows := make([]domain.RecentMessageReaction, 0, len(s.recent[userID]))
for _, row := range s.recent[userID] {
rows = append(rows, row)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Date != rows[j].Date {
return rows[i].Date > rows[j].Date
}
if rows[i].Reaction.Type != rows[j].Reaction.Type {
return rows[i].Reaction.Type < rows[j].Reaction.Type
}
return rows[i].Reaction.Emoticon < rows[j].Reaction.Emoticon
})
if len(rows) > limit {
rows = rows[:limit]
}
out := make([]domain.MessageReaction, 0, len(rows))
for _, row := range rows {
out = append(out, row.Reaction)
}
return out, nil
}
func (s *ChannelStore) ClearRecentMessageReactions(_ context.Context, userID int64) error {
if userID == 0 {
return domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
delete(s.recent, userID)
return nil
}
func (s *ChannelStore) ListSavedReactionTags(_ context.Context, userID int64, limit int) ([]domain.SavedReactionTag, error) {
if userID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 {
return []domain.SavedReactionTag{}, nil
}
if limit > domain.MaxSavedReactionTags {
limit = domain.MaxSavedReactionTags
}
s.mu.RLock()
defer s.mu.RUnlock()
rows := make([]domain.SavedReactionTag, 0, len(s.savedTags[userID]))
for _, row := range s.savedTags[userID] {
rows = append(rows, row)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Count != rows[j].Count {
return rows[i].Count > rows[j].Count
}
if rows[i].Reaction.Type != rows[j].Reaction.Type {
return rows[i].Reaction.Type < rows[j].Reaction.Type
}
return rows[i].Reaction.Emoticon < rows[j].Reaction.Emoticon
})
if len(rows) > limit {
rows = rows[:limit]
}
return rows, nil
}
func (s *ChannelStore) UpsertSavedReactionTag(_ context.Context, tag domain.SavedReactionTag) error {
if tag.UserID == 0 || tag.Reaction.Type != domain.MessageReactionEmoji || strings.TrimSpace(tag.Reaction.Emoticon) == "" {
return domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if s.savedTags[tag.UserID] == nil {
s.savedTags[tag.UserID] = make(map[string]domain.SavedReactionTag)
}
tag.Reaction.Emoticon = strings.TrimSpace(tag.Reaction.Emoticon)
if tag.Count < 0 {
tag.Count = 0
}
s.savedTags[tag.UserID][messageReactionKey(tag.Reaction)] = tag
return nil
}
func (s *ChannelStore) CreateForumTopic(ctx context.Context, req domain.CreateChannelForumTopicRequest) (domain.CreateChannelForumTopicResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.RandomID == 0 {
return domain.CreateChannelForumTopicResult{}, domain.ErrChannelInvalid
}
title := strings.TrimSpace(req.Title)
if title == "" && !req.TitleMissing {
return domain.CreateChannelForumTopicResult{}, domain.ErrChannelInvalid
}
if req.IconColor == 0 {
req.IconColor = domain.DefaultForumTopicIconColor
}
s.mu.Lock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
s.mu.Unlock()
return domain.CreateChannelForumTopicResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !channel.Forum || channel.Broadcast || !channel.Megagroup {
s.mu.Unlock()
return domain.CreateChannelForumTopicResult{}, domain.ErrChannelForumMissing
}
if !canSendChannelMessage(channel, member) {
s.mu.Unlock()
return domain.CreateChannelForumTopicResult{}, domain.ErrChannelWriteForbidden
}
if id, ok := s.randomToID[channelRandomKey{channelID: req.ChannelID, userID: req.UserID, randomID: req.RandomID}]; ok {
if topic, ok := s.topics[req.ChannelID][id]; ok {
msg, _ := s.findMessageLocked(req.ChannelID, id)
event := s.eventForMessageLocked(req.ChannelID, id)
recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0)
s.mu.Unlock()
return domain.CreateChannelForumTopicResult{
Channel: cloneChannel(channel),
Topic: cloneChannelForumTopic(topic),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: recipients,
Duplicate: true,
}, nil
}
}
s.mu.Unlock()
res, err := s.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: req.UserID,
ChannelID: req.ChannelID,
RandomID: req.RandomID,
SendAs: req.SendAs,
Action: &domain.ChannelMessageAction{
Type: domain.ChannelActionTopicCreate,
Title: title,
IconColor: req.IconColor,
IconEmojiID: req.IconEmojiID,
TitleMissing: req.TitleMissing,
},
Date: req.Date,
})
if err != nil {
return domain.CreateChannelForumTopicResult{}, err
}
if res.Message.Action == nil || res.Message.Action.Type != domain.ChannelActionTopicCreate {
return domain.CreateChannelForumTopicResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel = s.channels[req.ChannelID]
if s.topics[req.ChannelID] == nil {
s.topics[req.ChannelID] = make(map[int]domain.ChannelForumTopic)
}
topic, ok := s.topics[req.ChannelID][res.Message.ID]
if !ok {
topic = domain.ChannelForumTopic{
ChannelID: req.ChannelID,
TopicID: res.Message.ID,
CreatorUserID: req.UserID,
Title: title,
IconColor: req.IconColor,
IconEmojiID: req.IconEmojiID,
TitleMissing: req.TitleMissing,
Date: res.Message.Date,
TopMessageID: res.Message.ID,
ReadInboxMaxID: res.Message.ID,
ReadOutboxMaxID: res.Message.ID,
}
s.topics[req.ChannelID][topic.TopicID] = topic
}
return domain.CreateChannelForumTopicResult{
Channel: cloneChannel(channel),
Topic: cloneChannelForumTopic(topic),
Message: cloneChannelMessage(res.Message),
Event: cloneChannelEvent(res.Event),
Recipients: append([]int64(nil), res.Recipients...),
Duplicate: res.Duplicate,
}, nil
}
func (s *ChannelStore) EditForumTopic(ctx context.Context, req domain.EditChannelForumTopicRequest) (domain.EditChannelForumTopicResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.TopicID <= 0 {
return domain.EditChannelForumTopicResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
s.mu.Unlock()
return domain.EditChannelForumTopicResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
topic, ok := s.topics[req.ChannelID][req.TopicID]
if !channel.Forum {
s.mu.Unlock()
return domain.EditChannelForumTopicResult{}, domain.ErrChannelForumMissing
}
if !ok {
s.mu.Unlock()
return domain.EditChannelForumTopicResult{}, domain.ErrMessageIDInvalid
}
if !canManageForumTopic(channel, member, topic, req.UserID) {
s.mu.Unlock()
return domain.EditChannelForumTopicResult{}, domain.ErrChannelAdminRequired
}
next := topic
action := domain.ChannelMessageAction{Type: domain.ChannelActionTopicEdit}
changed := false
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
s.mu.Unlock()
return domain.EditChannelForumTopicResult{}, domain.ErrChannelInvalid
}
if next.Title != title {
next.Title = title
action.Title = title
changed = true
}
}
if req.IconEmojiID != nil && next.IconEmojiID != *req.IconEmojiID {
next.IconEmojiID = *req.IconEmojiID
action.IconEmojiID = *req.IconEmojiID
action.IconEmojiIDSet = true
changed = true
}
if req.Closed != nil && next.Closed != *req.Closed {
next.Closed = *req.Closed
action.Closed = boolPtr(*req.Closed)
changed = true
}
if req.Hidden != nil && next.Hidden != *req.Hidden {
next.Hidden = *req.Hidden
action.Hidden = boolPtr(*req.Hidden)
changed = true
}
s.mu.Unlock()
if !changed {
return domain.EditChannelForumTopicResult{}, domain.ErrChannelNotModified
}
res, err := s.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: req.UserID,
ChannelID: req.ChannelID,
ReplyTo: &domain.MessageReply{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID},
MessageID: req.TopicID,
TopMessageID: req.TopicID,
},
Action: &action,
Date: req.Date,
})
if err != nil {
return domain.EditChannelForumTopicResult{}, err
}
s.mu.Lock()
defer s.mu.Unlock()
channel = s.channels[req.ChannelID]
if _, ok := s.topics[req.ChannelID][req.TopicID]; !ok {
return domain.EditChannelForumTopicResult{}, domain.ErrChannelForumMissing
}
next.TopMessageID = maxInt(next.TopMessageID, res.Message.ID)
s.topics[req.ChannelID][req.TopicID] = next
return domain.EditChannelForumTopicResult{
Channel: cloneChannel(channel),
Topic: cloneChannelForumTopic(next),
Message: cloneChannelMessage(res.Message),
Event: cloneChannelEvent(res.Event),
Recipients: append([]int64(nil), res.Recipients...),
}, nil
}
func (s *ChannelStore) UpdatePinnedForumTopic(_ context.Context, req domain.UpdateChannelForumTopicPinnedRequest) (domain.UpdateChannelForumTopicPinnedResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.TopicID <= 0 {
return domain.UpdateChannelForumTopicPinnedResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.UpdateChannelForumTopicPinnedResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
topic, ok := s.topics[req.ChannelID][req.TopicID]
if !channel.Forum {
return domain.UpdateChannelForumTopicPinnedResult{}, domain.ErrChannelForumMissing
}
if !ok {
return domain.UpdateChannelForumTopicPinnedResult{}, domain.ErrMessageIDInvalid
}
if !canPinChannelMessages(channel, member) {
return domain.UpdateChannelForumTopicPinnedResult{}, domain.ErrChannelAdminRequired
}
if topic.Pinned == req.Pinned {
return domain.UpdateChannelForumTopicPinnedResult{}, domain.ErrChannelNotModified
}
topic.Pinned = req.Pinned
if req.Pinned && topic.PinnedOrder == 0 {
topic.PinnedOrder = s.nextForumTopicPinnedOrderLocked(req.ChannelID)
}
if !req.Pinned {
topic.PinnedOrder = 0
}
s.topics[req.ChannelID][req.TopicID] = topic
return domain.UpdateChannelForumTopicPinnedResult{
Channel: cloneChannel(channel),
Topic: cloneChannelForumTopic(topic),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) ReorderPinnedForumTopics(_ context.Context, req domain.ReorderChannelPinnedForumTopicsRequest) (domain.ReorderChannelPinnedForumTopicsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || len(req.Order) > domain.MaxChannelForumTopicIDs {
return domain.ReorderChannelPinnedForumTopicsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReorderChannelPinnedForumTopicsResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !channel.Forum {
return domain.ReorderChannelPinnedForumTopicsResult{}, domain.ErrChannelForumMissing
}
if !canPinChannelMessages(channel, member) {
return domain.ReorderChannelPinnedForumTopicsResult{}, domain.ErrChannelAdminRequired
}
seen := make(map[int]struct{}, len(req.Order))
order := make([]int, 0, len(req.Order))
for _, id := range req.Order {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ReorderChannelPinnedForumTopicsResult{}, domain.ErrMessageIDInvalid
}
if _, ok := seen[id]; ok {
continue
}
topic, ok := s.topics[req.ChannelID][id]
if !ok || !topic.Pinned {
if req.Force {
continue
}
return domain.ReorderChannelPinnedForumTopicsResult{}, domain.ErrMessageIDInvalid
}
seen[id] = struct{}{}
order = append(order, id)
}
for i, id := range order {
topic := s.topics[req.ChannelID][id]
topic.PinnedOrder = len(order) - i
s.topics[req.ChannelID][id] = topic
}
return domain.ReorderChannelPinnedForumTopicsResult{
Channel: cloneChannel(channel),
Order: append([]int(nil), order...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func (s *ChannelStore) DeleteForumTopicHistory(_ context.Context, req domain.DeleteChannelForumTopicHistoryRequest) (domain.DeleteChannelHistoryResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.TopicID <= 0 {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
topic, ok := s.topics[req.ChannelID][req.TopicID]
if !channel.Forum {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelForumMissing
}
if !ok {
return domain.DeleteChannelHistoryResult{}, domain.ErrMessageIDInvalid
}
if !canManageForumTopic(channel, member, topic, req.UserID) && !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
}
ids := make([]int, 0, domain.MaxDeleteHistoryBatch)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted {
continue
}
if msg.ID != req.TopicID && (msg.ReplyTo == nil || msg.ReplyTo.TopMessageID != req.TopicID) {
continue
}
ids = append(ids, msg.ID)
if len(ids) >= domain.MaxDeleteHistoryBatch {
break
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, ids, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
offset := 0
if s.topicHasVisibleMessagesLocked(req.ChannelID, req.TopicID) {
offset = 1
} else {
delete(s.topics[req.ChannelID], req.TopicID)
}
return domain.DeleteChannelHistoryResult{
Channel: cloneChannel(channel),
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Offset: offset,
}, nil
}
func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
if err != nil {
return domain.ChannelForumTopicList{}, err
}
if !channel.Forum {
return domain.ChannelForumTopicList{}, domain.ErrChannelForumMissing
}
limit := filter.Limit
if limit <= 0 || limit > domain.MaxChannelForumTopicsLimit {
limit = domain.MaxChannelForumTopicsLimit
}
query := strings.TrimSpace(strings.ToLower(filter.Query))
all := make([]domain.ChannelForumTopic, 0, len(s.topics[filter.ChannelID]))
for _, topic := range s.topics[filter.ChannelID] {
if topic.TopicID <= member.AvailableMinID {
continue
}
if query != "" && !strings.Contains(strings.ToLower(topic.Title), query) {
continue
}
if forumTopicBeforeOrAtOffset(topic, filter) {
continue
}
all = append(all, s.topicWithViewerCountersLocked(viewerUserID, filter.ChannelID, topic, member))
}
sortForumTopics(all)
count := len(all)
if len(all) > limit {
all = all[:limit]
}
messages := s.forumTopicRootMessagesLocked(filter.ChannelID, all, member.AvailableMinID)
return domain.ChannelForumTopicList{
Channel: cloneChannel(channel),
Dialog: s.dialogForUserLocked(viewerUserID, channel),
Topics: all,
Messages: messages,
Count: count,
}, nil
}
func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID)
if err != nil {
return domain.ChannelForumTopicList{}, err
}
if !channel.Forum {
return domain.ChannelForumTopicList{}, domain.ErrChannelForumMissing
}
wanted := make(map[int]struct{}, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.ChannelForumTopicList{}, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
topics := make([]domain.ChannelForumTopic, 0, len(wanted))
for id := range wanted {
topic, ok := s.topics[channelID][id]
if !ok || topic.TopicID <= member.AvailableMinID {
continue
}
topics = append(topics, s.topicWithViewerCountersLocked(viewerUserID, channelID, topic, member))
}
sortForumTopics(topics)
messages := s.forumTopicRootMessagesLocked(channelID, topics, member.AvailableMinID)
return domain.ChannelForumTopicList{
Channel: cloneChannel(channel),
Dialog: s.dialogForUserLocked(viewerUserID, channel),
Topics: topics,
Messages: messages,
Count: len(topics),
}, nil
}
func (s *ChannelStore) ListChannelReplies(_ context.Context, viewerUserID int64, filter domain.ChannelRepliesFilter) (domain.ChannelHistory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
source, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
if err != nil {
return domain.ChannelHistory{}, err
}
root, ok := s.findMessageLocked(filter.ChannelID, filter.RootMessageID)
if !ok || root.Deleted || root.ID <= member.AvailableMinID {
return domain.ChannelHistory{}, domain.ErrMessageIDInvalid
}
targetChannel := source
targetMember := member
rootID := root.ID
extraChannels := []domain.Channel(nil)
if source.Broadcast {
if root.Discussion == nil || root.Discussion.ChannelID == 0 || root.Discussion.MessageID == 0 {
return domain.ChannelHistory{Channel: source, Count: 0}, nil
}
linked, ok := s.channels[root.Discussion.ChannelID]
if !ok || linked.Deleted {
return domain.ChannelHistory{Channel: source, Count: 0}, nil
}
targetChannel = linked
rootID = root.Discussion.MessageID
if linkedMember, ok := s.members[linked.ID][viewerUserID]; ok {
targetMember = linkedMember
} else {
targetMember = domain.ChannelMember{}
}
extraChannels = append(extraChannels, source)
}
if targetRoot, ok := s.findMessageLocked(targetChannel.ID, rootID); !ok || targetRoot.Deleted {
return domain.ChannelHistory{Channel: targetChannel, Channels: extraChannels, Count: 0}, nil
}
limit := filter.Limit
if limit <= 0 || limit > domain.MaxChannelRepliesLimit {
limit = domain.MaxChannelRepliesLimit
}
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
base := make([]domain.ChannelMessage, 0, limit)
for _, msg := range s.messages[targetChannel.ID] {
if msg.Deleted || msg.ID <= targetMember.AvailableMinID {
continue
}
if !channelReplyBelongsToRoot(msg, targetChannel.ID, rootID) {
continue
}
if filter.MaxID > 0 && msg.ID >= filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
if filter.OffsetDate > 0 && msg.Date == 0 {
continue
}
base = append(base, msg)
}
sort.SliceStable(base, func(i, j int) bool { return channelMessageLess(base[i], base[j]) })
page := pageChannelMessageHistory(base, filter, limit)
out := make([]domain.ChannelMessage, 0, len(page))
for _, msg := range page {
out = append(out, cloneChannelMessage(msg))
}
s.populateChannelMessageRepliesLocked(viewerUserID, targetChannel.ID, out)
s.populateChannelMessageReactionsLocked(viewerUserID, targetChannel, out)
topics := []domain.ChannelForumTopic(nil)
if targetChannel.Forum {
if topic, ok := s.topics[targetChannel.ID][rootID]; ok && !topic.Hidden {
topic = s.topicWithViewerCountersLocked(viewerUserID, targetChannel.ID, topic, targetMember)
topics = append(topics, cloneChannelForumTopic(topic))
}
}
return domain.ChannelHistory{Channel: targetChannel, Channels: extraChannels, Topics: topics, Messages: out, Count: len(base)}, nil
}
func (s *ChannelStore) ListChannelUnreadMentions(_ context.Context, viewerUserID int64, filter domain.ChannelUnreadMentionsFilter) (domain.ChannelHistory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
if err != nil {
return domain.ChannelHistory{}, err
}
limit := filter.Limit
if limit <= 0 || limit > domain.MaxChannelUnreadMentionsLimit {
limit = domain.MaxChannelUnreadMentionsLimit
}
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
base := make([]domain.ChannelMessage, 0, limit)
for msgID, topID := range s.mentions[viewerUserID][filter.ChannelID] {
if filter.TopMsgID > 0 && topID != filter.TopMsgID {
continue
}
msg, ok := s.findMessageLocked(filter.ChannelID, msgID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
if filter.MaxID > 0 && msg.ID >= filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
base = append(base, msg)
}
sort.SliceStable(base, func(i, j int) bool { return channelMessageLess(base[i], base[j]) })
page := pageChannelMessageHistory(base, domain.ChannelRepliesFilter{
OffsetID: filter.OffsetID,
OffsetDate: filter.OffsetDate,
AddOffset: filter.AddOffset,
Limit: limit,
MaxID: filter.MaxID,
MinID: filter.MinID,
}, limit)
out := make([]domain.ChannelMessage, 0, len(page))
for _, msg := range page {
out = append(out, cloneChannelMessage(msg))
}
s.populateChannelMessageRepliesLocked(viewerUserID, filter.ChannelID, out)
s.populateChannelMessageReactionsLocked(viewerUserID, channel, out)
return domain.ChannelHistory{Channel: channel, Messages: out, Count: len(base)}, nil
}
func (s *ChannelStore) ReadChannelMentions(_ context.Context, req domain.ReadChannelMentionsRequest) (domain.ReadChannelMentionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ReadChannelMentionsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReadChannelMentionsResult{}, err
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelReadMentionsBatch {
limit = domain.MaxChannelReadMentionsBatch
}
msgIDs := make([]int, 0, limit)
for msgID, topID := range s.mentions[req.UserID][req.ChannelID] {
if req.TopMsgID > 0 && topID != req.TopMsgID {
continue
}
msgIDs = append(msgIDs, msgID)
}
sort.Sort(sort.Reverse(sort.IntSlice(msgIDs)))
if len(msgIDs) > limit {
msgIDs = msgIDs[:limit]
}
for _, msgID := range msgIDs {
delete(s.mentions[req.UserID][req.ChannelID], msgID)
}
remaining := s.countChannelUnreadMentionsLocked(req.UserID, req.ChannelID, req.TopMsgID)
if dialogs := s.dialogs[req.UserID]; dialogs != nil {
dialog := dialogs[req.ChannelID]
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(req.UserID, req.ChannelID, 0)
dialog.UserID = req.UserID
dialog.ChannelID = req.ChannelID
dialogs[req.ChannelID] = dialog
}
offset := 0
if remaining > 0 {
offset = 1
}
return domain.ReadChannelMentionsResult{
Channel: channel,
Cleared: len(msgIDs),
Remaining: remaining,
Offset: offset,
ChannelPts: channel.Pts,
}, nil
}
func (s *ChannelStore) ListChannelUnreadReactions(_ context.Context, viewerUserID int64, filter domain.ChannelUnreadReactionsFilter) (domain.ChannelHistory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
if err != nil {
return domain.ChannelHistory{}, err
}
limit := filter.Limit
if limit <= 0 || limit > domain.MaxChannelUnreadReactionsLimit {
limit = domain.MaxChannelUnreadReactionsLimit
}
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
base := make([]domain.ChannelMessage, 0, limit)
for msgID, byUser := range s.reactions[filter.ChannelID] {
msg, ok := s.findMessageLocked(filter.ChannelID, msgID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
if filter.TopMsgID > 0 && msg.ID != filter.TopMsgID && channelMentionTopID(msg) != filter.TopMsgID {
continue
}
if filter.MaxID > 0 && msg.ID >= filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
if !channelMessageHasUnreadReactionForUser(byUser, viewerUserID) {
continue
}
base = append(base, msg)
}
sort.SliceStable(base, func(i, j int) bool { return channelMessageLess(base[i], base[j]) })
page := pageChannelMessageHistory(base, domain.ChannelRepliesFilter{
OffsetID: filter.OffsetID,
AddOffset: filter.AddOffset,
Limit: limit,
MaxID: filter.MaxID,
MinID: filter.MinID,
}, limit)
out := make([]domain.ChannelMessage, 0, len(page))
for _, msg := range page {
out = append(out, cloneChannelMessage(msg))
}
s.populateChannelMessageRepliesLocked(viewerUserID, filter.ChannelID, out)
s.populateChannelMessageReactionsLocked(viewerUserID, channel, out)
return domain.ChannelHistory{Channel: channel, Messages: out, Count: len(base)}, nil
}
func (s *ChannelStore) ReadChannelReactions(_ context.Context, req domain.ReadChannelReactionsRequest) (domain.ReadChannelReactionsResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.ReadChannelReactionsResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReadChannelReactionsResult{}, err
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelReadReactionsBatch {
limit = domain.MaxChannelReadReactionsBatch
}
msgIDs := make([]int, 0, limit)
for msgID, byUser := range s.reactions[req.ChannelID] {
msg, ok := s.findMessageLocked(req.ChannelID, msgID)
if !ok || msg.Deleted {
continue
}
if req.TopMsgID > 0 && msg.ID != req.TopMsgID && channelMentionTopID(msg) != req.TopMsgID {
continue
}
if channelMessageHasUnreadReactionForUser(byUser, req.UserID) {
msgIDs = append(msgIDs, msgID)
}
}
sort.Sort(sort.Reverse(sort.IntSlice(msgIDs)))
if len(msgIDs) > limit {
msgIDs = msgIDs[:limit]
}
for _, msgID := range msgIDs {
for reactedUserID, rows := range s.reactions[req.ChannelID][msgID] {
changed := false
for i := range rows {
if rows[i].SenderUserID == req.UserID && rows[i].Unread {
rows[i].Unread = false
changed = true
}
}
if changed {
s.reactions[req.ChannelID][msgID][reactedUserID] = rows
}
}
}
remaining := s.countChannelUnreadReactionsLocked(req.UserID, req.ChannelID, req.TopMsgID)
s.refreshChannelUnreadReactionsDialogLocked(req.UserID, req.ChannelID)
offset := 0
if remaining > 0 {
offset = 1
}
return domain.ReadChannelReactionsResult{
Channel: channel,
Cleared: len(msgIDs),
Remaining: remaining,
Offset: offset,
ChannelPts: channel.Pts,
}, nil
}
func (s *ChannelStore) GetDiscussionMessage(_ context.Context, viewerUserID, channelID int64, msgID int) (domain.ChannelDiscussionMessage, error) {
s.mu.RLock()
defer s.mu.RUnlock()
source, member, err := s.channelAndMemberLocked(viewerUserID, channelID)
if err != nil {
return domain.ChannelDiscussionMessage{}, err
}
msg, ok := s.findMessageLocked(channelID, msgID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
return domain.ChannelDiscussionMessage{}, domain.ErrMessageIDInvalid
}
result := domain.ChannelDiscussionMessage{PostChannel: source, DiscussionChannel: source, Channels: []domain.Channel{source}}
targetChannel := source
targetMsg := msg
targetMember := member
if source.Broadcast {
if msg.Discussion == nil || msg.Discussion.ChannelID == 0 || msg.Discussion.MessageID == 0 {
return result, nil
}
linked, ok := s.channels[msg.Discussion.ChannelID]
if !ok || linked.Deleted {
return result, nil
}
linkedMsg, ok := s.findMessageLocked(linked.ID, msg.Discussion.MessageID)
if !ok || linkedMsg.Deleted {
return result, nil
}
targetChannel = linked
targetMsg = linkedMsg
if linkedMember, ok := s.members[linked.ID][viewerUserID]; ok {
targetMember = linkedMember
} else {
targetMember = domain.ChannelMember{}
}
result.DiscussionChannel = linked
result.Channels = []domain.Channel{source, linked}
}
items := []domain.ChannelMessage{cloneChannelMessage(targetMsg)}
s.populateChannelMessageRepliesLocked(viewerUserID, targetChannel.ID, items)
s.populateChannelMessageReactionsLocked(viewerUserID, targetChannel, items)
if stats := s.channelMessageRepliesLocked(viewerUserID, targetChannel.ID, targetMsg); stats != nil {
result.MaxID = stats.MaxID
}
result.ReadInboxMaxID = targetMember.ReadInboxMaxID
result.ReadOutboxMaxID = targetMember.ReadOutboxMaxID
result.UnreadCount = s.channelThreadUnreadCountLocked(viewerUserID, targetChannel.ID, targetMsg.ID, targetMember.ReadInboxMaxID)
result.Messages = items
return result, nil
}
func (s *ChannelStore) ReadChannelHistory(_ context.Context, req domain.ReadChannelHistoryRequest) (domain.ReadChannelHistoryResult, error) {
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReadChannelHistoryResult{}, err
}
maxID := req.MaxID
if maxID <= 0 || maxID > channel.TopMessageID {
maxID = channel.TopMessageID
}
member := s.members[req.ChannelID][req.UserID]
previous := member.ReadInboxMaxID
changed := maxID > member.ReadInboxMaxID
var outboxUpdates []domain.ChannelReadOutboxUpdate
if changed {
member.ReadInboxMaxID = maxID
member.ReadInboxDate = req.Date
member.UnreadMark = false
s.members[req.ChannelID][req.UserID] = member
outboxUpdates = s.advanceChannelReadOutboxLocked(req.ChannelID, req.UserID, previous, maxID)
}
dialog := s.dialogForUserLocked(req.UserID, channel)
dialog.ReadInboxMaxID = member.ReadInboxMaxID
dialog.UnreadCount = s.channelUnreadCountLocked(req.UserID, channel.ID, member.ReadInboxMaxID, dialog.TopMessageID)
dialog.UnreadMark = false
if s.dialogs[req.UserID] == nil {
s.dialogs[req.UserID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[req.UserID][req.ChannelID] = dialog
return domain.ReadChannelHistoryResult{
ChannelID: req.ChannelID,
MaxID: maxID,
StillUnreadCount: dialog.UnreadCount,
Changed: changed,
Pts: channel.Pts,
Dialog: dialog,
OutboxUpdates: outboxUpdates,
}, nil
}
func (s *ChannelStore) advanceChannelReadOutboxLocked(channelID, readerUserID int64, previous, maxID int) []domain.ChannelReadOutboxUpdate {
if maxID <= previous {
return nil
}
lowerID := previous
if maxID-lowerID > domain.MaxChannelReadOutboxScanMessages {
lowerID = maxID - domain.MaxChannelReadOutboxScanMessages
}
bySender := make(map[int64]int, domain.MaxChannelReadOutboxFanout)
messages := s.messages[channelID]
for i := len(messages) - 1; i >= 0; i-- {
msg := messages[i]
if msg.ID <= lowerID {
break
}
if msg.ID > maxID || msg.Deleted || msg.SenderUserID == 0 || msg.SenderUserID == readerUserID {
continue
}
if _, ok := bySender[msg.SenderUserID]; ok {
continue
}
bySender[msg.SenderUserID] = msg.ID
if len(bySender) >= domain.MaxChannelReadOutboxFanout {
break
}
}
if len(bySender) == 0 {
return nil
}
senderIDs := make([]int64, 0, len(bySender))
for userID := range bySender {
senderIDs = append(senderIDs, userID)
}
sort.Slice(senderIDs, func(i, j int) bool { return senderIDs[i] < senderIDs[j] })
channel := s.channels[channelID]
out := make([]domain.ChannelReadOutboxUpdate, 0, len(senderIDs))
for _, userID := range senderIDs {
maxForSender := bySender[userID]
member, ok := s.members[channelID][userID]
if !ok || member.Status != domain.ChannelMemberActive || maxForSender <= member.ReadOutboxMaxID {
continue
}
member.ReadOutboxMaxID = maxForSender
s.members[channelID][userID] = member
dialog := s.dialogForUserLocked(userID, channel)
if dialog.ReadOutboxMaxID < maxForSender {
dialog.ReadOutboxMaxID = maxForSender
}
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
out = append(out, domain.ChannelReadOutboxUpdate{UserID: userID, MaxID: maxForSender})
}
return out
}
func (s *ChannelStore) ListMessageReadParticipants(_ context.Context, req domain.ChannelReadParticipantsRequest) (domain.ChannelReadParticipantsResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelReadParticipantsResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
msg, found := s.findMessageLocked(req.ChannelID, req.MessageID)
if !found || msg.Deleted || msg.ID <= member.AvailableMinID {
return domain.ChannelReadParticipantsResult{}, domain.ErrMessageIDInvalid
}
result := domain.ChannelReadParticipantsResult{
Channel: channel,
Message: cloneChannelMessage(msg),
}
if !channel.Megagroup || channel.ParticipantsHidden || channel.ParticipantsCount > domain.MaxChannelReadParticipants {
return result, nil
}
now := req.Date
if now > 0 && msg.Date+domain.ChannelReadMarkExpirePeriod <= now {
return result, nil
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelReadParticipants {
limit = domain.MaxChannelReadParticipants
}
for _, reader := range s.members[req.ChannelID] {
if reader.UserID == req.UserID || reader.Status != domain.ChannelMemberActive || reader.BannedRights.ViewMessages {
continue
}
if reader.ReadInboxDate <= 0 {
continue
}
if reader.AvailableMinID >= req.MessageID || reader.ReadInboxMaxID < req.MessageID {
continue
}
result.Participants = append(result.Participants, domain.ChannelReadParticipant{
UserID: reader.UserID,
Date: reader.ReadInboxDate,
})
if len(result.Participants) >= limit {
break
}
}
sort.Slice(result.Participants, func(i, j int) bool {
if result.Participants[i].Date == result.Participants[j].Date {
return result.Participants[i].UserID < result.Participants[j].UserID
}
return result.Participants[i].Date < result.Participants[j].Date
})
return result, nil
}
func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, preview, err := s.channelForViewerLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelDifference{}, err
}
if req.Pts < 0 || req.Pts > channel.Pts {
return domain.ChannelDifference{}, domain.ErrPersistentTimestamp
}
if !preview && member.AvailableMinPts > req.Pts {
req.Pts = minInt(member.AvailableMinPts, channel.Pts)
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
dialog := s.dialogForUserLocked(req.UserID, channel)
if preview {
dialog = previewChannelDialog(req.UserID, channel, member)
}
if channel.Pts-req.Pts > limit {
messages := make([]domain.ChannelMessage, 0, domain.MaxChannelDifferenceTooLongMessages)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0 && len(messages) < domain.MaxChannelDifferenceTooLongMessages; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted {
continue
}
if msg.ID <= member.AvailableMinID {
continue
}
messages = append(messages, cloneChannelMessage(msg))
}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, messages)
return domain.ChannelDifference{
Channel: channel,
Self: member,
NewMessages: messages,
Pts: channel.Pts,
Final: true,
TooLong: true,
Timeout: 30,
Dialog: dialog,
}, nil
}
events := make([]domain.ChannelUpdateEvent, 0, limit)
lastPts := req.Pts
for _, event := range s.events[req.ChannelID] {
if event.Pts <= req.Pts {
continue
}
lastPts = event.Pts
visible, ok := domain.FilterChannelUpdateEventForAvailableMinID(cloneChannelEvent(event), member.AvailableMinID)
if !ok {
continue
}
if preview && visible.Type == domain.ChannelUpdateParticipant {
continue
}
events = append(events, visible)
}
if len(events) == 0 {
return domain.ChannelDifference{
Channel: channel,
Self: member,
Pts: maxInt(lastPts, req.Pts),
Final: true,
Timeout: 30,
Dialog: dialog,
}, nil
}
diff := domain.ChannelDifference{
Channel: channel,
Self: member,
Events: events,
Pts: lastPts,
Final: lastPts >= channel.Pts,
Timeout: 30,
Dialog: dialog,
}
for _, event := range events {
switch event.Type {
case domain.ChannelUpdateNewMessage:
diff.NewMessages = append(diff.NewMessages, cloneChannelMessage(event.Message))
default:
diff.OtherUpdates = append(diff.OtherUpdates, cloneChannelEvent(event))
}
}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, diff.NewMessages)
for i := range diff.OtherUpdates {
if diff.OtherUpdates[i].Message.ID == 0 {
continue
}
messages := []domain.ChannelMessage{diff.OtherUpdates[i].Message}
s.populateChannelMessageUnreadFlagsLocked(req.UserID, messages)
diff.OtherUpdates[i].Message = messages[0]
}
return diff, nil
}
func (s *ChannelStore) ListActiveChannelIDsForUser(_ context.Context, userID, afterChannelID int64, limit int) ([]int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if userID == 0 || afterChannelID < 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
limit = domain.MaxSynchronousChannelDialogFanout
}
out := make([]int64, 0, limit)
for channelID, members := range s.members {
if channelID <= afterChannelID {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
continue
}
member, ok := members[userID]
if !ok || member.Status != domain.ChannelMemberActive {
continue
}
out = append(out, channelID)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (s *ChannelStore) ListDirtyActiveChannelsForUser(_ context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if userID == 0 || sinceDate <= 0 || afterChannelID < 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
out := make([]domain.DirtyChannel, 0, limit)
for channelID, members := range s.members {
if channelID <= afterChannelID {
continue
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
continue
}
member, ok := members[userID]
if !ok || member.Status != domain.ChannelMemberActive {
continue
}
dirty := false
for _, event := range s.events[channelID] {
if event.Date > sinceDate {
dirty = true
break
}
}
if dirty {
out = append(out, domain.DirtyChannel{ChannelID: channelID, Pts: channel.Pts})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].ChannelID < out[j].ChannelID })
if len(out) > limit {
out = out[:limit]
}
return out, 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) ListChannelInviteAdminMemberIDs(_ context.Context, channelID int64, limit int) ([]int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[channelID]
if channelID == 0 || !ok || channel.Deleted {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxChannelRealtimeFanout {
limit = domain.MaxChannelRealtimeFanout
}
members := s.members[channelID]
out := make([]int64, 0, minInt(len(members), limit))
for _, member := range members {
if member.Status != domain.ChannelMemberActive {
continue
}
if member.Role == domain.ChannelRoleCreator {
out = append(out, member.UserID)
if len(out) >= limit {
break
}
continue
}
if member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo) {
out = append(out, member.UserID)
if len(out) >= limit {
break
}
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out, 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) MaxChannelPts(_ context.Context, channelID int64) (int, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ptsSeq[channelID], nil
}
func (s *ChannelStore) MaxChannelMessageID(_ context.Context, channelID int64) (int, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.msgSeq[channelID], nil
}
func (s *ChannelStore) nextChannelIDLocked() int64 {
id := s.nextID
s.nextID++
return id
}
func (s *ChannelStore) nextAccessHashLocked() int64 {
hash := s.nextHash
s.nextHash += 17
return hash
}
func (s *ChannelStore) nextChannelMessageIDLocked(channelID int64) int {
s.msgSeq[channelID]++
return s.msgSeq[channelID]
}
func (s *ChannelStore) nextChannelPtsLocked(channelID int64) int {
s.ptsSeq[channelID]++
return s.ptsSeq[channelID]
}
func (s *ChannelStore) nextChannelPtsNLocked(channelID int64, count int) int {
if count <= 0 {
return s.ptsSeq[channelID]
}
s.ptsSeq[channelID] += count
return s.ptsSeq[channelID]
}
func (s *ChannelStore) appendChannelServiceMessageLocked(channelID, senderUserID int64, date int, action domain.ChannelMessageAction) (domain.ChannelMessage, domain.ChannelUpdateEvent) {
channel := s.channels[channelID]
pts := s.nextChannelPtsLocked(channelID)
msg := domain.ChannelMessage{
ChannelID: channelID,
ID: s.nextChannelMessageIDLocked(channelID),
SenderUserID: senderUserID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: senderUserID},
Date: date,
Post: channel.Broadcast,
Action: &action,
Pts: pts,
}
event := domain.ChannelUpdateEvent{
ChannelID: channelID,
Type: domain.ChannelUpdateNewMessage,
Pts: pts,
PtsCount: 1,
Date: date,
Message: cloneChannelMessage(msg),
SenderUserID: senderUserID,
UserIDs: append([]int64(nil), action.UserIDs...),
}
s.messages[channelID] = append(s.messages[channelID], msg)
s.events[channelID] = append(s.events[channelID], event)
return msg, event
}
func transientChannelParticipantEvent(channelID, actorUserID int64, previous, participant domain.ChannelMember, date int) domain.ChannelUpdateEvent {
return domain.ChannelUpdateEvent{
ChannelID: channelID,
Type: domain.ChannelUpdateParticipant,
Date: date,
SenderUserID: actorUserID,
UserIDs: uniqueNonZeroInt64s(actorUserID, previous.UserID, previous.InviterUserID, participant.UserID, participant.InviterUserID),
Previous: previous,
Participant: participant,
}
}
func (s *ChannelStore) channelForMemberLocked(userID, channelID int64) (domain.Channel, error) {
channel, _, err := s.channelAndMemberLocked(userID, channelID)
return channel, err
}
func (s *ChannelStore) channelForViewerLocked(userID, channelID int64) (domain.Channel, domain.ChannelMember, bool, error) {
channel, member, err := s.channelAndMemberLocked(userID, channelID)
if err == nil {
return channel, member, false, nil
}
if !errors.Is(err, domain.ErrChannelPrivate) {
return domain.Channel{}, domain.ChannelMember{}, false, err
}
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ChannelMember{}, false, domain.ErrChannelInvalid
}
existing, found := s.members[channelID][userID]
if found && (existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages) {
return domain.Channel{}, domain.ChannelMember{}, false, domain.ErrChannelUserBanned
}
if !publicPreviewableChannel(channel) {
return domain.Channel{}, domain.ChannelMember{}, false, domain.ErrChannelPrivate
}
return channel, publicPreviewMember(channel, userID, existing, found), true, nil
}
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) upsertChannelDialogLocked(userID int64, channel domain.Channel, top domain.ChannelMessage, selfAction bool) {
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
dialog := s.dialogs[userID][channel.ID]
dialog.UserID = userID
dialog.ChannelID = channel.ID
dialog.TopMessageID = s.visibleTopMessageIDLocked(userID, channel)
if top.ID != 0 {
dialog.TopMessageDate = top.Date
}
member := s.members[channel.ID][userID]
if member.ReadInboxMaxID > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = member.ReadInboxMaxID
}
if selfAction {
if channel.TopMessageID > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = channel.TopMessageID
}
dialog.ReadOutboxMaxID = channel.TopMessageID
}
dialog.UnreadCount = s.channelUnreadCountLocked(userID, channel.ID, dialog.ReadInboxMaxID, dialog.TopMessageID)
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(userID, channel.ID, 0)
dialog.UnreadReactions = s.countChannelUnreadReactionsLocked(userID, channel.ID, 0)
s.dialogs[userID][channel.ID] = dialog
}
func (s *ChannelStore) dialogForUserLocked(userID int64, channel domain.Channel) domain.ChannelDialog {
dialog := s.dialogs[userID][channel.ID]
dialog.UserID = userID
dialog.ChannelID = channel.ID
member := s.members[channel.ID][userID]
dialog.TopMessageID = s.visibleTopMessageIDForMemberLocked(channel, member)
if member.ReadInboxMaxID > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = member.ReadInboxMaxID
}
dialog.UnreadCount = s.channelUnreadCountLocked(userID, channel.ID, dialog.ReadInboxMaxID, dialog.TopMessageID)
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(userID, channel.ID, 0)
dialog.UnreadReactions = s.countChannelUnreadReactionsLocked(userID, channel.ID, 0)
return dialog
}
func (s *ChannelStore) findMessageLocked(channelID int64, id int) (domain.ChannelMessage, bool) {
for _, msg := range s.messages[channelID] {
if msg.ID == id {
return msg, true
}
}
return domain.ChannelMessage{}, false
}
func (s *ChannelStore) findMessageIndexLocked(channelID int64, id int) (int, bool) {
for i, msg := range s.messages[channelID] {
if msg.ID == id {
return i, true
}
}
return 0, false
}
func (s *ChannelStore) populateChannelMessageRepliesLocked(viewerUserID, channelID int64, messages []domain.ChannelMessage) {
for i := range messages {
messages[i].Replies = s.channelMessageRepliesLocked(viewerUserID, channelID, messages[i])
}
}
func (s *ChannelStore) channelMessageRepliesLocked(viewerUserID, channelID int64, msg domain.ChannelMessage) *domain.ChannelMessageReplies {
targetChannelID := channelID
rootID := msg.ID
stats := domain.ChannelMessageReplies{}
if msg.Discussion != nil && msg.Discussion.ChannelID != 0 && msg.Discussion.MessageID != 0 {
targetChannelID = msg.Discussion.ChannelID
rootID = msg.Discussion.MessageID
stats.Comments = true
stats.ChannelID = msg.Discussion.ChannelID
} else if channel, ok := s.channels[channelID]; ok && channel.Broadcast && channel.LinkedChatID != 0 && msg.Post {
stats.Comments = true
stats.ChannelID = channel.LinkedChatID
}
if rootID <= 0 {
return nil
}
if member, ok := s.members[targetChannelID][viewerUserID]; ok {
stats.ReadMaxID = member.ReadInboxMaxID
}
seenRecent := map[domain.Peer]struct{}{}
for i := len(s.messages[targetChannelID]) - 1; i >= 0; i-- {
reply := s.messages[targetChannelID][i]
if reply.Deleted || !channelReplyBelongsToRoot(reply, targetChannelID, rootID) {
continue
}
stats.Replies++
if stats.MaxID == 0 || reply.ID > stats.MaxID {
stats.MaxID = reply.ID
stats.RepliesPts = reply.Pts
}
if len(stats.RecentRepliers) < 3 {
peer := reply.From
if peer.ID == 0 && reply.SenderUserID != 0 {
peer = domain.Peer{Type: domain.PeerTypeUser, ID: reply.SenderUserID}
}
if peer.ID != 0 {
if _, ok := seenRecent[peer]; !ok {
seenRecent[peer] = struct{}{}
stats.RecentRepliers = append(stats.RecentRepliers, peer)
}
}
}
}
if stats.Comments && stats.RepliesPts == 0 {
if root, ok := s.findMessageLocked(targetChannelID, rootID); ok {
stats.RepliesPts = root.Pts
}
}
if !stats.Comments && stats.Replies == 0 {
return nil
}
return &stats
}
func (s *ChannelStore) channelThreadUnreadCountLocked(viewerUserID, channelID int64, rootID, readMaxID int) int {
unread := 0
for _, msg := range s.messages[channelID] {
if msg.Deleted || msg.ID <= readMaxID || msg.SenderUserID == viewerUserID {
continue
}
if channelReplyBelongsToRoot(msg, channelID, rootID) {
unread++
}
}
return unread
}
func (s *ChannelStore) channelUnreadCountLocked(viewerUserID, channelID int64, readMaxID, topID int) int {
if viewerUserID == 0 || channelID == 0 || topID <= readMaxID {
return 0
}
unread := 0
for _, msg := range s.messages[channelID] {
if msg.Deleted || msg.ID <= readMaxID || msg.ID > topID || msg.SenderUserID == viewerUserID {
continue
}
unread++
}
return unread
}
func (s *ChannelStore) addChannelUnreadMentionsLocked(channelID int64, msg domain.ChannelMessage, senderUserID int64, userIDs []int64) {
if len(userIDs) == 0 || msg.ID == 0 {
return
}
seen := make(map[int64]struct{}, len(userIDs))
written := 0
topID := channelMentionTopID(msg)
for _, userID := range userIDs {
if userID == 0 || userID == senderUserID {
continue
}
if _, ok := seen[userID]; ok {
continue
}
seen[userID] = struct{}{}
member, ok := s.members[channelID][userID]
if !ok || member.Status != domain.ChannelMemberActive || member.BannedRights.ViewMessages {
continue
}
if msg.ID <= member.AvailableMinID || msg.ID <= member.ReadInboxMaxID {
continue
}
if s.mentions[userID] == nil {
s.mentions[userID] = make(map[int64]map[int]int)
}
if s.mentions[userID][channelID] == nil {
s.mentions[userID][channelID] = make(map[int]int)
}
s.mentions[userID][channelID][msg.ID] = topID
written++
if written == domain.MaxChannelMentionRecipients {
return
}
}
}
func (s *ChannelStore) countChannelUnreadMentionsLocked(userID, channelID int64, topMsgID int) int {
count := 0
for _, mentionTopID := range s.mentions[userID][channelID] {
if topMsgID == 0 || mentionTopID == topMsgID {
count++
}
}
return count
}
func (s *ChannelStore) countChannelUnreadReactionsLocked(userID, channelID int64, topMsgID int) int {
count := 0
availableMinID := 0
if member, ok := s.members[channelID][userID]; ok {
availableMinID = member.AvailableMinID
}
for msgID, byUser := range s.reactions[channelID] {
msg, ok := s.findMessageLocked(channelID, msgID)
if !ok || msg.Deleted || msg.ID <= availableMinID {
continue
}
if topMsgID > 0 && msg.ID != topMsgID && channelMentionTopID(msg) != topMsgID {
continue
}
if channelMessageHasUnreadReactionForUser(byUser, userID) {
count++
}
}
return count
}
func channelMessageHasUnreadReactionForUser(byUser map[int64][]domain.ChannelMessagePeerReaction, userID int64) bool {
for _, rows := range byUser {
for _, row := range rows {
if row.SenderUserID == userID && row.UserID != userID && row.Unread {
return true
}
}
}
return false
}
func (s *ChannelStore) refreshChannelUnreadReactionsDialogLocked(userID, channelID int64) {
if userID == 0 || channelID == 0 {
return
}
channel, ok := s.channels[channelID]
if !ok {
return
}
member, ok := s.members[channelID][userID]
if !ok || member.Status != domain.ChannelMemberActive || member.BannedRights.ViewMessages {
return
}
dialog := s.dialogForUserLocked(userID, channel)
dialog.UnreadReactions = s.countChannelUnreadReactionsLocked(userID, channelID, 0)
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[userID][channelID] = dialog
}
func (s *ChannelStore) deleteChannelUnreadMentionsLocked(channelID int64, ids []int) {
if len(ids) == 0 {
return
}
set := make(map[int]struct{}, len(ids))
for _, id := range ids {
set[id] = struct{}{}
}
for userID, byChannel := range s.mentions {
mentions := byChannel[channelID]
if len(mentions) == 0 {
continue
}
for id := range set {
delete(mentions, id)
}
if len(mentions) == 0 {
delete(byChannel, channelID)
}
if len(byChannel) == 0 {
delete(s.mentions, userID)
}
}
}
func (s *ChannelStore) deleteChannelUnreadMentionsUpToLocked(userID, channelID int64, maxID int) {
if maxID <= 0 || len(s.mentions[userID][channelID]) == 0 {
return
}
for id := range s.mentions[userID][channelID] {
if id <= maxID {
delete(s.mentions[userID][channelID], id)
}
}
if len(s.mentions[userID][channelID]) == 0 {
delete(s.mentions[userID], channelID)
}
if len(s.mentions[userID]) == 0 {
delete(s.mentions, userID)
}
}
func channelMentionTopID(msg domain.ChannelMessage) int {
if msg.ReplyTo == nil {
return 0
}
if msg.ReplyTo.TopMessageID > 0 {
return msg.ReplyTo.TopMessageID
}
return msg.ReplyTo.MessageID
}
func channelReplyBelongsToRoot(msg domain.ChannelMessage, channelID int64, rootID int) bool {
if msg.ReplyTo == nil || rootID <= 0 {
return false
}
if msg.ReplyTo.Peer.ID != 0 && msg.ReplyTo.Peer != (domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}) {
return false
}
return msg.ReplyTo.TopMessageID == rootID || (msg.ReplyTo.TopMessageID == 0 && msg.ReplyTo.MessageID == rootID)
}
func pageChannelMessageHistory(base []domain.ChannelMessage, filter domain.ChannelRepliesFilter, limit int) []domain.ChannelMessage {
if limit <= 0 || len(base) == 0 {
return nil
}
switch messageHistoryLoadType(filter.AddOffset, limit) {
case messageHistoryLoadForward:
return forwardChannelMessageHistory(base, filter, limit)
case messageHistoryLoadAround:
forwardLimit := -filter.AddOffset
if forwardLimit > limit {
forwardLimit = limit
}
backwardLimit := limit + filter.AddOffset
if backwardLimit < 0 {
backwardLimit = 0
}
page := make([]domain.ChannelMessage, 0, limit)
page = append(page, forwardChannelMessageHistory(base, filter, forwardLimit)...)
page = append(page, backwardChannelMessageHistory(base, filter, backwardLimit, true)...)
sort.SliceStable(page, func(i, j int) bool { return channelMessageLess(page[i], page[j]) })
return page
default:
start := filter.AddOffset
if start < 0 {
start = 0
}
candidates := backwardChannelMessageHistory(base, filter, limit+start, false)
if start >= len(candidates) {
return nil
}
return candidates[start:]
}
}
func backwardChannelMessageHistory(base []domain.ChannelMessage, filter domain.ChannelRepliesFilter, limit int, includeOffset bool) []domain.ChannelMessage {
if limit <= 0 {
return nil
}
out := make([]domain.ChannelMessage, 0, limit)
for _, msg := range base {
if !channelMessageBeforeHistoryOffset(msg, filter, includeOffset) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
return out
}
func forwardChannelMessageHistory(base []domain.ChannelMessage, filter domain.ChannelRepliesFilter, limit int) []domain.ChannelMessage {
if limit <= 0 {
return nil
}
out := make([]domain.ChannelMessage, 0, limit)
for i := len(base) - 1; i >= 0; i-- {
msg := base[i]
if !channelMessageAfterHistoryOffset(msg, filter) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
sort.SliceStable(out, func(i, j int) bool { return channelMessageLess(out[i], out[j]) })
return out
}
func channelMessageBeforeHistoryOffset(msg domain.ChannelMessage, filter domain.ChannelRepliesFilter, includeOffset bool) bool {
if filter.OffsetDate > 0 {
if includeOffset {
return msg.Date <= filter.OffsetDate
}
return msg.Date < filter.OffsetDate
}
if filter.OffsetID <= 0 {
return true
}
if includeOffset {
return msg.ID <= filter.OffsetID
}
return msg.ID < filter.OffsetID
}
func channelMessageAfterHistoryOffset(msg domain.ChannelMessage, filter domain.ChannelRepliesFilter) bool {
if filter.OffsetDate > 0 {
return msg.Date >= filter.OffsetDate
}
if filter.OffsetID <= 0 {
return false
}
return msg.ID > filter.OffsetID
}
func channelMessageLess(a, b domain.ChannelMessage) bool {
if a.Date != b.Date {
return a.Date > b.Date
}
return a.ID > b.ID
}
func (s *ChannelStore) resolveChannelReplyLocked(req domain.SendChannelMessageRequest, member domain.ChannelMember, channel domain.Channel) (*domain.MessageReply, error) {
if req.ReplyTo == nil {
return nil, nil
}
if err := domain.ValidateMessageReplyBounds(req.ReplyTo); err != nil {
return nil, err
}
peer := req.ReplyTo.Peer
channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}
if peer.ID == 0 {
peer = channelPeer
}
if peer != channelPeer {
return nil, domain.ErrReplyMessageIDInvalid
}
if req.ReplyTo.MessageID == 0 {
if req.ReplyTo.TopMessageID <= 0 || !channel.Forum {
return nil, domain.ErrReplyMessageIDInvalid
}
topic, ok := s.topics[req.ChannelID][req.ReplyTo.TopMessageID]
if !ok || topic.Hidden {
return nil, domain.ErrReplyMessageIDInvalid
}
if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID) {
return nil, domain.ErrChannelWriteForbidden
}
reply := cloneMessageReply(req.ReplyTo)
reply.MessageID = 0
reply.Peer = channelPeer
reply.TopMessageID = topic.TopicID
reply.ForumTopic = true
return reply, nil
}
target, ok := s.findMessageLocked(req.ChannelID, req.ReplyTo.MessageID)
if !ok || target.Deleted || target.ID <= member.AvailableMinID {
return nil, domain.ErrReplyMessageIDInvalid
}
reply := cloneMessageReply(req.ReplyTo)
reply.MessageID = target.ID
reply.Peer = channelPeer
reply.TopMessageID = target.ID
if target.ReplyTo != nil && target.ReplyTo.TopMessageID > 0 {
reply.TopMessageID = target.ReplyTo.TopMessageID
}
if req.ReplyTo.TopMessageID > 0 && req.ReplyTo.TopMessageID != reply.TopMessageID {
return nil, domain.ErrReplyMessageIDInvalid
}
if channel.Forum && reply.TopMessageID > 0 {
if topic, ok := s.topics[req.ChannelID][reply.TopMessageID]; ok && !topic.Hidden {
if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID) {
return nil, domain.ErrChannelWriteForbidden
}
reply.ForumTopic = true
}
}
return reply, nil
}
func (s *ChannelStore) visibleTopMessageIDLocked(userID int64, channel domain.Channel) int {
return s.visibleTopMessageIDForMemberLocked(channel, s.members[channel.ID][userID])
}
func (s *ChannelStore) visibleTopMessageIDForMemberLocked(channel domain.Channel, member domain.ChannelMember) int {
for i := len(s.messages[channel.ID]) - 1; i >= 0; i-- {
msg := s.messages[channel.ID][i]
if !msg.Deleted && msg.ID > member.AvailableMinID {
return msg.ID
}
}
return 0
}
func (s *ChannelStore) deleteChannelMessagesLocked(channel domain.Channel, member domain.ChannelMember, ids []int, actorUserID int64, date int) ([]int, domain.ChannelUpdateEvent, domain.Channel, error) {
if len(ids) == 0 {
return nil, domain.ChannelUpdateEvent{}, channel, nil
}
seen := make(map[int]struct{}, len(ids))
deleted := make([]int, 0, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return nil, domain.ChannelUpdateEvent{}, channel, domain.ErrMessageIDInvalid
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
idx, ok := s.findMessageIndexLocked(channel.ID, id)
if !ok || s.messages[channel.ID][idx].Deleted {
continue
}
msg := s.messages[channel.ID][idx]
if msg.SenderUserID != actorUserID && !canDeleteAnyChannelMessage(member) {
return nil, domain.ChannelUpdateEvent{}, channel, domain.ErrChannelAdminRequired
}
msg.Deleted = true
s.messages[channel.ID][idx] = msg
deleted = append(deleted, id)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channel.ID,
UserID: actorUserID,
Date: date,
Type: domain.ChannelAdminLogDeleteMessage,
Message: ptrChannelMessage(msg),
Query: msg.Body,
})
}
if len(deleted) == 0 {
return nil, domain.ChannelUpdateEvent{}, channel, nil
}
s.deleteChannelUnreadMentionsLocked(channel.ID, deleted)
pts := s.nextChannelPtsNLocked(channel.ID, len(deleted))
channel.Pts = pts
channel.TopMessageID = s.topNonDeletedMessageIDLocked(channel.ID)
s.channels[channel.ID] = channel
for userID, member := range s.members[channel.ID] {
if member.Status != domain.ChannelMemberActive {
continue
}
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
dialog := s.dialogForUserLocked(userID, channel)
s.dialogs[userID][channel.ID] = dialog
}
event := domain.ChannelUpdateEvent{
ChannelID: channel.ID,
Type: domain.ChannelUpdateDeleteMessages,
Pts: pts,
PtsCount: len(deleted),
Date: date,
MessageIDs: append([]int(nil), deleted...),
SenderUserID: actorUserID,
}
s.events[channel.ID] = append(s.events[channel.ID], event)
return deleted, event, channel, nil
}
func (s *ChannelStore) topNonDeletedMessageIDLocked(channelID int64) int {
for i := len(s.messages[channelID]) - 1; i >= 0; i-- {
if !s.messages[channelID][i].Deleted {
return s.messages[channelID][i].ID
}
}
return 0
}
func (s *ChannelStore) eventForMessageLocked(channelID int64, id int) domain.ChannelUpdateEvent {
for _, event := range s.events[channelID] {
if event.Message.ID == id {
return cloneChannelEvent(event)
}
}
return domain.ChannelUpdateEvent{}
}
func (s *ChannelStore) appendChannelAdminLogLocked(event domain.ChannelAdminLogEvent) {
if event.ChannelID == 0 || event.UserID == 0 || event.Type == "" {
return
}
s.logSeq[event.ChannelID]++
event.ID = s.logSeq[event.ChannelID]
event.Query = adminLogSearchText(event)
s.adminLogs[event.ChannelID] = append(s.adminLogs[event.ChannelID], cloneChannelAdminLogEvent(event))
}
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 channelDialogToDialog(dialog domain.ChannelDialog) domain.Dialog {
return domain.Dialog{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: dialog.ChannelID},
FolderID: dialog.FolderID,
TopMessage: dialog.TopMessageID,
TopMessageDate: dialog.TopMessageDate,
ReadInboxMaxID: dialog.ReadInboxMaxID,
ReadOutboxMaxID: dialog.ReadOutboxMaxID,
UnreadCount: dialog.UnreadCount,
UnreadMentions: dialog.UnreadMentions,
UnreadReactions: dialog.UnreadReactions,
Pinned: dialog.Pinned,
PinnedOrder: dialog.PinnedOrder,
UnreadMark: dialog.UnreadMark,
ViewForumAsMessages: dialog.ViewForumAsMessages,
}
}
func inactiveChannelDate(dialog domain.Dialog, channel domain.Channel, member domain.ChannelMember) int {
if dialog.TopMessageDate > 0 {
return dialog.TopMessageDate
}
date := channel.Date
if member.JoinedAt > date {
date = member.JoinedAt
}
return date
}
func recommendableChannel(channel domain.Channel) bool {
return !channel.Deleted &&
channel.Broadcast &&
!channel.Megagroup &&
strings.TrimSpace(channel.Username) != ""
}
func publicSearchableChannel(channel domain.Channel) bool {
return !channel.Deleted &&
(channel.Broadcast || channel.Megagroup) &&
strings.TrimSpace(channel.Username) != ""
}
func publicPreviewableChannel(channel domain.Channel) bool {
return publicSearchableChannel(channel)
}
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 previewChannelDialog(userID int64, channel domain.Channel, member domain.ChannelMember) domain.ChannelDialog {
topMessageID := channel.TopMessageID
if topMessageID <= member.AvailableMinID {
topMessageID = 0
}
return domain.ChannelDialog{
UserID: userID,
ChannelID: channel.ID,
TopMessageID: topMessageID,
TopMessageDate: channel.Date,
ReadInboxMaxID: maxInt(channel.TopMessageID, member.ReadInboxMaxID),
ReadOutboxMaxID: maxInt(channel.TopMessageID, member.ReadOutboxMaxID),
}
}
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 channelDialogMatchesFilter(dialog domain.Dialog, channel domain.Channel, filter domain.DialogFilter) bool {
if filter.HasFolderID {
if filter.FolderID < domain.DialogCustomFolderMinID {
if dialog.FolderID != filter.FolderID {
return false
}
} else if filter.Folder == nil {
return false
}
}
if filter.PinnedOnly && !dialog.Pinned {
return false
}
if filter.ExcludePinned && dialog.Pinned {
return false
}
if !channelDialogAfterOffset(dialog, filter) {
return false
}
if filter.Folder == nil {
return true
}
folder := filter.Folder
if peerInFolderList(dialog.Peer, folder.ExcludePeers) {
return false
}
if folder.ExcludeRead && dialog.UnreadCount == 0 && !dialog.UnreadMark {
return false
}
if folder.ExcludeArchived && dialog.FolderID == domain.DialogArchiveFolderID {
return false
}
if peerInFolderList(dialog.Peer, folder.PinnedPeers) || peerInFolderList(dialog.Peer, folder.IncludePeers) {
return true
}
if channel.Megagroup && folder.Groups {
return true
}
if channel.Broadcast && folder.Broadcasts {
return true
}
return !folder.Groups && !folder.Broadcasts && len(folder.IncludePeers) == 0
}
func channelDialogAfterOffset(dialog domain.Dialog, filter domain.DialogFilter) bool {
if filter.OffsetDate <= 0 && filter.OffsetID <= 0 {
if filter.HasOffsetPeer && filter.OffsetPeer == dialog.Peer {
return false
}
return true
}
if filter.OffsetDate > 0 {
if dialog.TopMessageDate != filter.OffsetDate {
return dialog.TopMessageDate < filter.OffsetDate
}
if filter.OffsetID <= 0 {
return false
}
if dialog.TopMessage != filter.OffsetID {
return dialog.TopMessage < filter.OffsetID
}
if filter.HasOffsetPeer && filter.OffsetPeer.Type == dialog.Peer.Type {
return dialog.Peer.ID < filter.OffsetPeer.ID
}
return false
}
if dialog.TopMessage != filter.OffsetID {
return dialog.TopMessage < filter.OffsetID
}
if filter.HasOffsetPeer && filter.OffsetPeer.Type == dialog.Peer.Type {
return dialog.Peer.ID < filter.OffsetPeer.ID
}
return false
}
func peerInFolderList(peer domain.Peer, items []domain.DialogFolderPeer) bool {
for _, item := range items {
if item.Peer == peer {
return true
}
}
return false
}
func canPostToBroadcast(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PostMessages)
}
func canSendChannelMessage(channel domain.Channel, member domain.ChannelMember) bool {
if channel.Broadcast {
return canPostToBroadcast(member)
}
if member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin {
return true
}
return !channel.DefaultBannedRights.SendMessages && !member.BannedRights.SendMessages
}
func canInviteToChannel(channel domain.Channel, member domain.ChannelMember) bool {
if member.Role == domain.ChannelRoleCreator ||
(member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo)) {
return true
}
return channel.Megagroup && !channel.DefaultBannedRights.InviteUsers && !member.BannedRights.InviteUsers
}
func isChannelAdmin(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin
}
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:
return member.Status == domain.ChannelMemberActive && isChannelAdmin(member)
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 channelRoleOrder(role domain.ChannelMemberRole) int {
switch role {
case domain.ChannelRoleCreator:
return 0
case domain.ChannelRoleAdmin:
return 1
default:
return 2
}
}
func canChangeChannelInfo(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.ChangeInfo)
}
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 validDiscussionGroup(channel domain.Channel) bool {
return channel.Megagroup && !channel.Broadcast && !channel.Forum && !channel.Deleted
}
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 canExportChannelInvite(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator ||
(member.Role == domain.ChannelRoleAdmin && (member.AdminRights.InviteUsers || member.AdminRights.ChangeInfo))
}
func canPinChannelMessages(channel domain.Channel, member domain.ChannelMember) bool {
if member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PinMessages) {
return true
}
return channel.Megagroup && !channel.DefaultBannedRights.PinMessages && !member.BannedRights.PinMessages
}
func canManageForumTopic(channel domain.Channel, member domain.ChannelMember, topic domain.ChannelForumTopic, userID int64) bool {
if topic.CreatorUserID == userID {
return true
}
return canPinChannelMessages(channel, member)
}
func canEditChannelMessage(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.EditMessages)
}
func canDeleteAnyChannelMessage(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.DeleteMessages)
}
func channelSlowModeWait(channel domain.Channel, member domain.ChannelMember, now int) int {
if channel.SlowmodeSeconds <= 0 || member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin {
return 0
}
next := member.SlowmodeLastSendDate + channel.SlowmodeSeconds
if now >= next {
return 0
}
return next - now
}
func boolPtr(v bool) *bool {
return &v
}
func channelInitialAvailableMinID(channel domain.Channel) int {
if channel.PreHistoryHidden {
return channel.TopMessageID
}
return 0
}
func channelInitialAvailableMinPts(channel domain.Channel) int {
return channel.Pts
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func zeroChannelAdminRights(rights domain.ChannelAdminRights) bool {
return rights == domain.ChannelAdminRights{}
}
func adminRightsSubset(want, have domain.ChannelAdminRights) bool {
return (!want.ChangeInfo || have.ChangeInfo) &&
(!want.PostMessages || have.PostMessages) &&
(!want.EditMessages || have.EditMessages) &&
(!want.DeleteMessages || have.DeleteMessages) &&
(!want.BanUsers || have.BanUsers) &&
(!want.InviteUsers || have.InviteUsers) &&
(!want.PinMessages || have.PinMessages) &&
(!want.AddAdmins || have.AddAdmins) &&
(!want.ManageCall || have.ManageCall) &&
(!want.Anonymous || have.Anonymous)
}
func zeroChannelBannedRights(rights domain.ChannelBannedRights) bool {
return rights == domain.ChannelBannedRights{}
}
func adminLogBanType(previous, next domain.ChannelMember) domain.ChannelAdminLogEventType {
if next.Status == domain.ChannelMemberKicked || next.BannedRights.ViewMessages {
return domain.ChannelAdminLogParticipantKick
}
if previous.Status == domain.ChannelMemberKicked || previous.BannedRights.ViewMessages {
return domain.ChannelAdminLogParticipantUnkick
}
if !zeroChannelBannedRights(next.BannedRights) {
return domain.ChannelAdminLogParticipantBan
}
return domain.ChannelAdminLogParticipantUnban
}
func adminLogEventMatchesFilter(typ domain.ChannelAdminLogEventType, filter domain.ChannelAdminLogFilter) bool {
if filter.Empty() {
return true
}
switch typ {
case domain.ChannelAdminLogParticipantJoin:
return filter.Join
case domain.ChannelAdminLogParticipantLeave:
return filter.Leave
case domain.ChannelAdminLogParticipantInvite:
return filter.Invite || filter.Invites
case domain.ChannelAdminLogParticipantBan:
return filter.Ban
case domain.ChannelAdminLogParticipantUnban:
return filter.Unban
case domain.ChannelAdminLogParticipantKick:
return filter.Kick
case domain.ChannelAdminLogParticipantUnkick:
return filter.Unkick
case domain.ChannelAdminLogParticipantPromote:
return filter.Promote
case domain.ChannelAdminLogParticipantDemote:
return filter.Demote
case domain.ChannelAdminLogChangeTitle, domain.ChannelAdminLogChangeUsername, domain.ChannelAdminLogChangeLinkedChat, domain.ChannelAdminLogToggleSlowMode:
return filter.Info
case domain.ChannelAdminLogToggleSignatures, domain.ChannelAdminLogTogglePreHistoryHidden, domain.ChannelAdminLogToggleAntiSpam, domain.ChannelAdminLogToggleAutotranslation:
return filter.Settings
case domain.ChannelAdminLogToggleForum:
return filter.Settings || filter.Forums
case domain.ChannelAdminLogUpdatePinned:
return filter.Pinned
case domain.ChannelAdminLogEditMessage:
return filter.Edit
case domain.ChannelAdminLogDeleteMessage:
return filter.Delete
case domain.ChannelAdminLogSendMessage:
return filter.Send
default:
return false
}
}
func adminLogEventMatchesQuery(event domain.ChannelAdminLogEvent, query string) bool {
if strings.Contains(strings.ToLower(event.PrevString), query) ||
strings.Contains(strings.ToLower(event.NewString), query) ||
strings.Contains(event.Query, query) {
return true
}
for _, msg := range []*domain.ChannelMessage{event.Message, event.PrevMessage, event.NewMessage} {
if msg != nil && strings.Contains(strings.ToLower(msg.Body), query) {
return true
}
}
return false
}
func adminLogSearchText(event domain.ChannelAdminLogEvent) string {
parts := []string{
event.Query,
event.PrevString,
event.NewString,
}
for _, msg := range []*domain.ChannelMessage{event.Message, event.PrevMessage, event.NewMessage} {
if msg != nil {
parts = append(parts, msg.Body)
}
}
return strings.ToLower(strings.TrimSpace(strings.Join(parts, " ")))
}
func int64Set(items []int64) map[int64]struct{} {
if len(items) == 0 {
return nil
}
out := make(map[int64]struct{}, len(items))
for _, item := range items {
if item != 0 {
out[item] = struct{}{}
}
}
return out
}
func (s *ChannelStore) refreshChannelCountsLocked(channelID int64) {
channel := s.channels[channelID]
var participants, admins, kicked, banned int
for _, member := range s.members[channelID] {
if member.Status == domain.ChannelMemberKicked {
kicked++
}
if member.Status != domain.ChannelMemberActive {
continue
}
participants++
if member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin {
admins++
}
if !zeroChannelBannedRights(member.BannedRights) {
banned++
}
}
channel.ParticipantsCount = participants
channel.AdminsCount = admins
channel.KickedCount = kicked
channel.BannedCount = banned
s.channels[channelID] = channel
}
func sameMessageEntities(a, b []domain.MessageEntity) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func diffFinal(returned, all []domain.ChannelUpdateEvent) bool {
if len(returned) == 0 {
return true
}
return returned[len(returned)-1].Pts >= all[len(all)-1].Pts
}
func uniqueNonZero(ids []int64, exclude int64) []int64 {
seen := make(map[int64]struct{}, len(ids))
out := make([]int64, 0, len(ids))
for _, id := range ids {
if id == 0 || id == exclude {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
func randomMemoryPositiveInt64() (int64, error) {
var b [8]byte
if _, err := rand.Read(b[:]); err != nil {
return 0, err
}
return int64(binary.LittleEndian.Uint64(b[:]) & ((1 << 63) - 1)), nil
}
func randomMemoryInviteHash() (string, error) {
var b [18]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
func uniqueNonZeroInt64s(items ...int64) []int64 {
seen := make(map[int64]struct{}, len(items))
out := make([]int64, 0, len(items))
for _, item := range items {
if item == 0 {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
out = append(out, item)
}
return out
}
func cloneChannelMembers(in []domain.ChannelMember) []domain.ChannelMember {
return append([]domain.ChannelMember(nil), in...)
}
func discussionGroupUpdateResult(changed map[int64]domain.Channel) domain.DiscussionGroupUpdateResult {
ids := make([]int64, 0, len(changed))
for id := range changed {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool {
return ids[i] < ids[j]
})
out := domain.DiscussionGroupUpdateResult{Channels: make([]domain.Channel, 0, len(ids))}
for _, id := range ids {
out.Channels = append(out.Channels, cloneChannel(changed[id]))
}
return out
}
func cloneChannel(in domain.Channel) domain.Channel {
in.ReactionPolicy = copyChannelReactionPolicy(in.ReactionPolicy)
return in
}
func copyChannelReactionPolicy(in domain.ChannelReactionPolicy) domain.ChannelReactionPolicy {
in.Emoticons = append([]string(nil), in.Emoticons...)
in.CustomEmojiIDs = append([]int64(nil), in.CustomEmojiIDs...)
return in
}
func cloneChannelEvent(in domain.ChannelUpdateEvent) domain.ChannelUpdateEvent {
in.Message = cloneChannelMessage(in.Message)
in.MessageIDs = append([]int(nil), in.MessageIDs...)
in.UserIDs = append([]int64(nil), in.UserIDs...)
return in
}
func cloneChannelForumTopic(in domain.ChannelForumTopic) domain.ChannelForumTopic {
return in
}
func (s *ChannelStore) topicWithViewerCountersLocked(viewerUserID, channelID int64, topic domain.ChannelForumTopic, member domain.ChannelMember) domain.ChannelForumTopic {
out := cloneChannelForumTopic(topic)
out.UnreadCount = s.channelThreadUnreadCountLocked(viewerUserID, channelID, topic.TopicID, member.ReadInboxMaxID)
out.UnreadMentionsCount = s.countChannelUnreadMentionsLocked(viewerUserID, channelID, topic.TopicID)
out.UnreadReactionsCount = s.countChannelUnreadReactionsLocked(viewerUserID, channelID, topic.TopicID)
return out
}
func (s *ChannelStore) updateForumTopicTopMessageLocked(channelID int64, msg domain.ChannelMessage) {
if msg.ReplyTo == nil || !msg.ReplyTo.ForumTopic || msg.ReplyTo.TopMessageID <= 0 {
return
}
topic, ok := s.topics[channelID][msg.ReplyTo.TopMessageID]
if !ok {
return
}
topic.TopMessageID = msg.ID
topic.Date = msg.Date
s.topics[channelID][topic.TopicID] = topic
}
func sortForumTopics(topics []domain.ChannelForumTopic) {
sort.Slice(topics, func(i, j int) bool {
a, b := topics[i], topics[j]
if a.Pinned != b.Pinned {
return a.Pinned
}
if a.PinnedOrder != b.PinnedOrder {
return a.PinnedOrder > b.PinnedOrder
}
if a.Date != b.Date {
return a.Date > b.Date
}
return a.TopicID > b.TopicID
})
}
func forumTopicBeforeOrAtOffset(topic domain.ChannelForumTopic, filter domain.ChannelForumTopicFilter) bool {
if filter.OffsetDate == 0 && filter.OffsetID == 0 && filter.OffsetTopic == 0 {
return false
}
offsetID := filter.OffsetTopic
if offsetID == 0 {
offsetID = filter.OffsetID
}
if filter.OffsetDate != 0 {
if topic.Date < filter.OffsetDate {
return false
}
if topic.Date > filter.OffsetDate {
return true
}
}
if offsetID == 0 {
return false
}
return topic.TopicID >= offsetID
}
func (s *ChannelStore) forumTopicRootMessagesLocked(channelID int64, topics []domain.ChannelForumTopic, availableMinID int) []domain.ChannelMessage {
if len(topics) == 0 {
return nil
}
wanted := make(map[int]struct{}, len(topics))
for _, topic := range topics {
if topic.TopMessageID > 0 {
wanted[topic.TopMessageID] = struct{}{}
}
}
messages := make([]domain.ChannelMessage, 0, len(wanted))
for _, msg := range s.messages[channelID] {
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= availableMinID {
continue
}
messages = append(messages, cloneChannelMessage(msg))
}
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
return messages
}
func (s *ChannelStore) populateChannelMessageReactionsLocked(viewerUserID int64, channel domain.Channel, messages []domain.ChannelMessage) {
if len(messages) == 0 || channel.ID == 0 {
return
}
s.populateChannelMessageUnreadFlagsLocked(viewerUserID, messages)
for i := range messages {
if messages[i].ChannelID != channel.ID || messages[i].ID <= 0 {
continue
}
reactions := s.channelMessageReactionsLocked(viewerUserID, channel, messages[i].ID)
if len(reactions.Results) == 0 && len(reactions.Recent) == 0 {
continue
}
messages[i].Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
}
func (s *ChannelStore) populateChannelMessagesReactionsLocked(viewerUserID int64, channels []domain.Channel, messages []domain.ChannelMessage) {
if len(messages) == 0 {
return
}
s.populateChannelMessageUnreadFlagsLocked(viewerUserID, messages)
channelsByID := make(map[int64]domain.Channel, len(channels))
for _, ch := range channels {
if ch.ID != 0 {
channelsByID[ch.ID] = ch
}
}
for i := range messages {
ch := channelsByID[messages[i].ChannelID]
if ch.ID == 0 {
ch = s.channels[messages[i].ChannelID]
}
if ch.ID == 0 {
continue
}
reactions := s.channelMessageReactionsLocked(viewerUserID, ch, messages[i].ID)
if len(reactions.Results) == 0 && len(reactions.Recent) == 0 {
continue
}
messages[i].Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
}
func (s *ChannelStore) populateChannelMessageUnreadFlagsLocked(viewerUserID int64, messages []domain.ChannelMessage) {
if viewerUserID == 0 || len(messages) == 0 {
return
}
for i := range messages {
if messages[i].ChannelID == 0 || messages[i].ID <= 0 {
continue
}
if _, ok := s.mentions[viewerUserID][messages[i].ChannelID][messages[i].ID]; !ok {
continue
}
messages[i].Mentioned = true
messages[i].MediaUnread = !messages[i].Media.IsZero()
}
}
type memoryReactionCursor struct {
date int
userID int64
emoticon string
}
func (s *ChannelStore) channelMessageReactionsLocked(viewerUserID int64, channel domain.Channel, messageID int) domain.ChannelMessageReactions {
rows := s.channelMessageReactionRowsLocked(channel.ID, messageID, viewerUserID, nil)
out := domain.ChannelMessageReactions{
CanSeeList: !channel.Broadcast || channel.Megagroup,
Results: []domain.ChannelMessageReactionCount{},
Recent: []domain.ChannelMessagePeerReaction{},
}
if len(rows) == 0 {
return out
}
type aggregate struct {
reaction domain.MessageReaction
count int
chosenOrder int
latestDate int
}
aggregates := make(map[string]*aggregate)
for _, row := range rows {
key := string(row.Reaction.Type) + "\x00" + row.Reaction.Emoticon
item := aggregates[key]
if item == nil {
item = &aggregate{reaction: row.Reaction}
aggregates[key] = item
}
item.count++
if row.My && row.ChosenOrder > 0 {
item.chosenOrder = row.ChosenOrder
}
if row.Date > item.latestDate {
item.latestDate = row.Date
}
}
items := make([]aggregate, 0, len(aggregates))
for _, item := range aggregates {
items = append(items, *item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].count != items[j].count {
return items[i].count > items[j].count
}
if items[i].latestDate != items[j].latestDate {
return items[i].latestDate > items[j].latestDate
}
return items[i].reaction.Emoticon < items[j].reaction.Emoticon
})
for _, item := range items {
out.Results = append(out.Results, domain.ChannelMessageReactionCount{
Reaction: item.reaction,
Count: item.count,
ChosenOrder: item.chosenOrder,
})
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Date != rows[j].Date {
return rows[i].Date > rows[j].Date
}
if rows[i].UserID != rows[j].UserID {
return rows[i].UserID > rows[j].UserID
}
return rows[i].Reaction.Emoticon < rows[j].Reaction.Emoticon
})
if len(rows) > domain.MaxChannelMessageReactionRecent {
rows = rows[:domain.MaxChannelMessageReactionRecent]
}
out.Recent = cloneChannelPeerReactions(rows)
return out
}
func (s *ChannelStore) channelMessageReactionRowsLocked(channelID int64, messageID int, viewerUserID int64, filter *domain.MessageReaction) []domain.ChannelMessagePeerReaction {
byMessage := s.reactions[channelID]
if byMessage == nil {
return nil
}
byUser := byMessage[messageID]
if byUser == nil {
return nil
}
rows := make([]domain.ChannelMessagePeerReaction, 0, len(byUser))
for _, userRows := range byUser {
for _, row := range userRows {
if filter != nil && (row.Reaction.Type != filter.Type || row.Reaction.Emoticon != filter.Emoticon) {
continue
}
row.My = row.UserID == viewerUserID
rows = append(rows, row)
}
}
return rows
}
func memoryReactionOffset(row domain.ChannelMessagePeerReaction) string {
return strconv.Itoa(row.Date) + ":" + strconv.FormatInt(row.UserID, 10) + ":" + row.Reaction.Emoticon
}
func messageReactionKey(reaction domain.MessageReaction) string {
return string(reaction.Type) + "\x00" + reaction.Emoticon
}
func parseMemoryReactionOffset(offset string) (memoryReactionCursor, bool) {
parts := strings.SplitN(offset, ":", 3)
if len(parts) != 3 {
return memoryReactionCursor{}, false
}
date, err := strconv.Atoi(parts[0])
if err != nil || date < 0 {
return memoryReactionCursor{}, false
}
userID, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || userID < 0 {
return memoryReactionCursor{}, false
}
return memoryReactionCursor{date: date, userID: userID, emoticon: parts[2]}, true
}
func memoryReactionAfterOffset(row domain.ChannelMessagePeerReaction, cursor memoryReactionCursor) bool {
if row.Date != cursor.date {
return row.Date < cursor.date
}
if row.UserID != cursor.userID {
return row.UserID < cursor.userID
}
return row.Reaction.Emoticon > cursor.emoticon
}
func (s *ChannelStore) nextForumTopicPinnedOrderLocked(channelID int64) int {
next := 1
for _, topic := range s.topics[channelID] {
if topic.PinnedOrder >= next {
next = topic.PinnedOrder + 1
}
}
return next
}
func (s *ChannelStore) topicHasVisibleMessagesLocked(channelID int64, topicID int) bool {
for _, msg := range s.messages[channelID] {
if msg.Deleted {
continue
}
if msg.ID == topicID || (msg.ReplyTo != nil && msg.ReplyTo.TopMessageID == topicID) {
return true
}
}
return false
}
func cloneChannelMessage(in domain.ChannelMessage) domain.ChannelMessage {
in.Entities = append([]domain.MessageEntity(nil), in.Entities...)
in.ReplyTo = cloneMessageReply(in.ReplyTo)
in.Forward = cloneMessageForward(in.Forward)
in.Discussion = cloneChannelDiscussionRef(in.Discussion)
in.Replies = cloneChannelMessageReplies(in.Replies)
in.Reactions = cloneChannelMessageReactionsPtr(in.Reactions)
if in.SendAs != nil {
p := *in.SendAs
in.SendAs = &p
}
if in.Action != nil {
in.Action = cloneChannelMessageAction(in.Action)
}
return in
}
func cloneChannelMessageAction(in *domain.ChannelMessageAction) *domain.ChannelMessageAction {
if in == nil {
return nil
}
out := *in
out.UserIDs = append([]int64(nil), in.UserIDs...)
if in.Closed != nil {
v := *in.Closed
out.Closed = &v
}
if in.Hidden != nil {
v := *in.Hidden
out.Hidden = &v
}
return &out
}
func cloneChannelDiscussionRef(in *domain.ChannelDiscussionRef) *domain.ChannelDiscussionRef {
if in == nil {
return nil
}
out := *in
return &out
}
func cloneChannelMessageReplies(in *domain.ChannelMessageReplies) *domain.ChannelMessageReplies {
if in == nil {
return nil
}
out := *in
out.RecentRepliers = append([]domain.Peer(nil), in.RecentRepliers...)
return &out
}
func cloneChannelMessageReactionsPtr(in *domain.ChannelMessageReactions) *domain.ChannelMessageReactions {
if in == nil {
return nil
}
out := cloneChannelMessageReactions(*in)
return &out
}
func cloneChannelMessageReactions(in domain.ChannelMessageReactions) domain.ChannelMessageReactions {
in.Results = append([]domain.ChannelMessageReactionCount(nil), in.Results...)
in.Recent = cloneChannelPeerReactions(in.Recent)
return in
}
func cloneChannelPeerReactions(in []domain.ChannelMessagePeerReaction) []domain.ChannelMessagePeerReaction {
if len(in) == 0 {
return nil
}
return append([]domain.ChannelMessagePeerReaction(nil), in...)
}
func ptrChannelMember(in domain.ChannelMember) *domain.ChannelMember {
out := in
return &out
}
func ptrChannelMessage(in domain.ChannelMessage) *domain.ChannelMessage {
out := cloneChannelMessage(in)
return &out
}
func cloneChannelAdminLogEvent(in domain.ChannelAdminLogEvent) domain.ChannelAdminLogEvent {
if in.PrevParticipant != nil {
in.PrevParticipant = ptrChannelMember(*in.PrevParticipant)
}
if in.NewParticipant != nil {
in.NewParticipant = ptrChannelMember(*in.NewParticipant)
}
if in.Participant != nil {
in.Participant = ptrChannelMember(*in.Participant)
}
if in.Message != nil {
in.Message = ptrChannelMessage(*in.Message)
}
if in.PrevMessage != nil {
in.PrevMessage = ptrChannelMessage(*in.PrevMessage)
}
if in.NewMessage != nil {
in.NewMessage = ptrChannelMessage(*in.NewMessage)
}
return in
}