ListForumTopics / GetForumTopicsByID / GeneralForumTopic gated on membership while channel history uses the public-preview path, so a public forum's topics (General included) were invisible until you joined. Switch them to getChannelForViewer / channelForViewerLocked; private forums and write paths keep the membership gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
740 lines
24 KiB
Go
740 lines
24 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"telesrv/internal/domain"
|
|
"time"
|
|
)
|
|
|
|
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) 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) 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
|
|
}
|
|
selfBoostsApplied := 0
|
|
if channel.Megagroup {
|
|
now := req.Date
|
|
if now == 0 {
|
|
now = int(time.Now().Unix())
|
|
}
|
|
selfBoostsApplied = s.selfBoostsAppliedLocked(req.UserID, req.ChannelID, now)
|
|
}
|
|
if domain.ChannelBannedRightsBlockManageTopics(channel, member, selfBoostsApplied) {
|
|
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
|
|
}
|
|
selfBoostsApplied := 0
|
|
if channel.Megagroup {
|
|
now := req.Date
|
|
if now == 0 {
|
|
now = int(time.Now().Unix())
|
|
}
|
|
selfBoostsApplied = s.selfBoostsAppliedLocked(req.UserID, req.ChannelID, now)
|
|
}
|
|
if !canManageForumTopic(channel, member, topic, req.UserID, selfBoostsApplied) {
|
|
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, 0) && !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()
|
|
// channelForViewerLocked, not channelAndMemberLocked: a public forum's topic
|
|
// list is browsable before joining, exactly like its message history.
|
|
channel, member, _, err := s.channelForViewerLocked(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.channelForViewerLocked(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.channelAndMemberOrLinkedGuestLocked(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, Self: member, Count: 0}, nil
|
|
}
|
|
linked, linkedMember, linkedErr := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, root.Discussion.ChannelID)
|
|
if linkedErr != nil {
|
|
return domain.ChannelHistory{}, linkedErr
|
|
}
|
|
targetChannel = linked
|
|
targetMember = linkedMember
|
|
rootID = root.Discussion.MessageID
|
|
extraChannels = append(extraChannels, source)
|
|
}
|
|
if targetRoot, ok := s.findMessageLocked(targetChannel.ID, rootID); !ok || targetRoot.Deleted {
|
|
return domain.ChannelHistory{Channel: targetChannel, Self: targetMember, 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, Self: targetMember, Channels: extraChannels, Topics: topics, Messages: out, Count: len(base)}, nil
|
|
}
|
|
|
|
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 canManageForumTopic(channel domain.Channel, member domain.ChannelMember, topic domain.ChannelForumTopic, userID int64, selfBoostsApplied int) bool {
|
|
if domain.ChannelBannedRightsBlockManageTopics(channel, member, selfBoostsApplied) {
|
|
return false
|
|
}
|
|
if topic.CreatorUserID == userID {
|
|
return true
|
|
}
|
|
return canPinChannelMessages(channel, member)
|
|
}
|
|
|
|
func cloneChannelForumTopic(in domain.ChannelForumTopic) domain.ChannelForumTopic {
|
|
return in
|
|
}
|
|
|
|
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) nextForumTopicPinnedOrderLocked(channelID int64) int {
|
|
next := 1
|
|
for _, topic := range s.topics[channelID] {
|
|
if topic.PinnedOrder >= next {
|
|
next = topic.PinnedOrder + 1
|
|
}
|
|
}
|
|
return next
|
|
}
|
|
|
|
func cloneChannelMessageReplies(in *domain.ChannelMessageReplies) *domain.ChannelMessageReplies {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := *in
|
|
out.RecentRepliers = append([]domain.Peer(nil), in.RecentRepliers...)
|
|
return &out
|
|
}
|