6790 lines
218 KiB
Go
6790 lines
218 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))
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
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) 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
|
|
}
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|