owpengram-server/internal/store/memory/channel_settings.go
Astra 012f8a8d0e channels: force pre-history visible when a group gets a public username
New supergroups are created with "chat history for new members" hidden (the
client sets this right after creation, matching official Telegram). The
official server then forces it back to visible when the group is made public;
owpengram's UpdateUsername left the flag alone, leaving public groups in a
state where non-members (and post-join members) see no history at all.

UpdateUsername now clears pre_history_hidden whenever a non-empty username is
assigned, in the same transaction, with a matching admin-log event. Removing
the username leaves the flag untouched, so the creator can hide history again
once the group is private.
2026-09-14 11:57:12 +01:00

874 lines
28 KiB
Go

package memory
import (
"context"
"sort"
"strings"
"telesrv/internal/domain"
"time"
)
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) SetChannelWallpaper(_ context.Context, req domain.SetChannelWallpaperRequest) (domain.SetChannelWallpaperResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.SetChannelWallpaperResult{}, domain.ErrChannelInvalid
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
wallpaper := domain.CloneWallpaperPtr(req.Wallpaper)
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.SetChannelWallpaperResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canChangeChannelInfo(member) {
return domain.SetChannelWallpaperResult{}, domain.ErrChannelAdminRequired
}
if domain.WallpaperEqual(channel.Wallpaper, wallpaper) {
return domain.SetChannelWallpaperResult{Channel: cloneChannel(channel)}, nil
}
channel.Wallpaper = domain.CloneWallpaperPtr(wallpaper)
if wallpaper == nil {
s.channels[req.ChannelID] = channel
return domain.SetChannelWallpaperResult{
Channel: cloneChannel(channel),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Changed: true,
}, nil
}
msg, event := s.appendChannelServiceMessageLocked(req.ChannelID, req.UserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionSetChatWallpaper,
Wallpaper: domain.CloneWallpaperPtr(wallpaper),
})
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[req.ChannelID] = channel
s.upsertChannelDialogLocked(req.UserID, channel, msg, true)
return domain.SetChannelWallpaperResult{
Channel: cloneChannel(channel),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Changed: true,
}, 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) 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(ctx 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
}
}
}
if s.usernameRegistry != nil {
if _, err := s.usernameRegistry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}, username); err != nil {
return domain.Channel{}, err
}
}
prevUsername := channel.Username
channel.Username = username
// A public group cannot keep pre-history hidden: assigning a username forces
// "chat history for new members" back to visible (matches the official
// server). Removing the username leaves the flag untouched.
clearedPrehistory := username != "" && channel.PreHistoryHidden
if clearedPrehistory {
channel.PreHistoryHidden = false
}
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,
})
if clearedPrehistory {
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: int(time.Now().Unix()),
Type: domain.ChannelAdminLogTogglePreHistoryHidden,
PrevBool: true,
NewBool: false,
})
}
return channel, nil
}
func (s *ChannelStore) SetChannelVerified(_ context.Context, channelID int64, verified bool) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel.Verified = verified
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelScamFake(_ context.Context, channelID int64, scam, fake bool) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if scam && fake {
return domain.Channel{}, domain.ErrPeerModerationFlagsInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel.Scam = scam
channel.Fake = fake
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelAdminSettings(_ context.Context, channelID int64, patch domain.ChannelAdminSettings) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
if patch.Gigagroup != nil {
channel.Gigagroup = *patch.Gigagroup
}
if patch.AntiSpam != nil {
channel.AntiSpam = *patch.AntiSpam
}
if patch.ParticipantsHidden != nil {
channel.ParticipantsHidden = *patch.ParticipantsHidden
}
if patch.NoForwards != nil {
channel.NoForwards = *patch.NoForwards
}
if patch.JoinToSend != nil {
channel.JoinToSend = *patch.JoinToSend
}
if patch.JoinRequest != nil {
channel.JoinRequest = *patch.JoinRequest
}
if patch.SlowmodeSeconds != nil {
channel.SlowmodeSeconds = *patch.SlowmodeSeconds
}
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelUsernameAdmin(_ context.Context, channelID int64, username string) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel.Username = username
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelColorAdmin(_ context.Context, channelID int64, forProfile bool, color domain.ChannelPeerColor) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
if forProfile {
channel.ProfileColor = color
} else {
channel.Color = color
}
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelEmojiStatusAdmin(_ context.Context, channelID int64, status domain.ChannelEmojiStatus) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if status.DocumentID == 0 {
status.Until = 0
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel.EmojiStatus = status
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) SetChannelPhotoAdmin(_ context.Context, channelID int64, photo domain.Photo) (domain.Channel, error) {
if channelID == 0 || photo.ID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok || channel.Deleted {
return domain.Channel{}, domain.ErrChannelInvalid
}
stripped := domain.StrippedFromSizes(photo.Sizes)
if stripped == nil {
stripped = []byte{}
}
channel.PhotoID = photo.ID
channel.PhotoDCID = photo.DCID
channel.PhotoStripped = stripped
s.channels[channelID] = channel
return cloneChannel(channel), nil
}
func (s *ChannelStore) ResolvePublicChannelUsername(_ context.Context, viewerUserID int64, username string) (domain.Channel, bool, error) {
_ = viewerUserID // zero is the anonymous public-web view; no membership state is projected.
username = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
if username == "" {
return domain.Channel{}, false, nil
}
s.mu.RLock()
registry := s.usernameRegistry
for _, channel := range s.channels {
if !publicSearchableChannel(channel) {
continue
}
if strings.ToLower(channel.Username) == username {
s.mu.RUnlock()
return cloneChannel(channel), true, nil
}
}
s.mu.RUnlock()
if registry != nil {
if peer, ok := registry.activeUsernamePeer(username, domain.PeerTypeChannel); ok {
s.mu.RLock()
channel, found := s.channels[peer.ID]
s.mu.RUnlock()
if found && !channel.Deleted && (channel.Broadcast || channel.Megagroup) {
return cloneChannel(channel), true, nil
}
}
}
return domain.Channel{}, false, nil
}
func (s *ChannelStore) SetChannelPhoto(_ context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) {
if userID == 0 || channelID == 0 {
return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid
}
if date == 0 {
date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(userID, channelID)
if err != nil {
return domain.SetChannelPhotoResult{}, err
}
member := s.members[channelID][userID]
if !canChangeChannelInfo(member) {
return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired
}
var action domain.ChannelMessageAction
if photo != nil && photo.ID != 0 {
if channel.PhotoID == photo.ID {
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
}
action = domain.ChannelMessageAction{
Type: domain.ChannelActionChatEditPhoto,
Photo: domain.ClonePhotoPtr(photo),
}
channel.PhotoID = photo.ID
channel.PhotoDCID = photo.DCID
channel.PhotoStripped = domain.StrippedFromSizes(photo.Sizes)
} else {
if channel.PhotoID == 0 {
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
}
action = domain.ChannelMessageAction{Type: domain.ChannelActionChatDeletePhoto}
channel.PhotoID = 0
channel.PhotoDCID = 0
channel.PhotoStripped = nil
}
msg, event := s.appendChannelServiceMessageLocked(channelID, userID, date, action)
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
s.channels[channelID] = channel
s.upsertChannelDialogLocked(userID, channel, msg, true)
return domain.SetChannelPhotoResult{
Channel: cloneChannel(channel),
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(channelID, 0, 0),
Changed: true,
}, 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) 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) 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) SetBoostsToUnblockRestrictions(_ context.Context, userID, channelID int64, boosts int) (domain.Channel, error) {
if userID == 0 || channelID == 0 || boosts < 0 || boosts > domain.MaxChannelBoostsToUnblockRestrictions {
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.BoostsUnrestrict = boosts
s.channels[channelID] = channel
return cloneChannel(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) 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) 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 validDiscussionGroup(channel domain.Channel) bool {
return channel.Megagroup && !channel.Broadcast && !channel.Forum && !channel.Deleted
}
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 cloneChannelDiscussionRef(in *domain.ChannelDiscussionRef) *domain.ChannelDiscussionRef {
if in == nil {
return nil
}
out := *in
return &out
}