owpengram-server/internal/store/postgres/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

1324 lines
45 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
)
func (s *ChannelStore) EditChannelTitle(ctx context.Context, req domain.EditChannelTitleRequest) (domain.EditChannelTitleResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || strings.TrimSpace(req.Title) == "" {
return domain.EditChannelTitleResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.EditChannelTitleResult{}, fmt.Errorf("edit channel title: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
title := strings.TrimSpace(req.Title)
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.EditChannelTitleResult{}, fmt.Errorf("begin edit channel title: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelTitleResult{}, err
}
if !canChangeChannelInfo(member) {
return domain.EditChannelTitleResult{}, domain.ErrChannelAdminRequired
}
if channel.Title == title {
return domain.EditChannelTitleResult{}, domain.ErrChannelNotModified
}
prevTitle := channel.Title
if _, err := tx.Exec(ctx, `UPDATE channels SET title = $2, updated_at = now() WHERE id = $1`, req.ChannelID, title); err != nil {
return domain.EditChannelTitleResult{}, fmt.Errorf("update channel title: %w", err)
}
channel.Title = title
msg, event, err := s.insertServiceMessage(ctx, tx, channel, req.UserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionEditTitle,
Title: title,
})
if err != nil {
return domain.EditChannelTitleResult{}, err
}
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
// read_outbox 由公共已读水位派生服务消息只自读inbox不能把
// actor 的 outbox 水位顶到自己刚发的消息上造成虚假双勾。
if err := upsertChannelDialogTx(ctx, tx, req.UserID, channel, msg, msg.ID, 0); err != nil {
return domain.EditChannelTitleResult{}, err
}
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogChangeTitle,
PrevString: prevTitle,
NewString: title,
}); err != nil {
return domain.EditChannelTitleResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.EditChannelTitleResult{}, fmt.Errorf("commit edit channel title: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
return domain.EditChannelTitleResult{Channel: channel, Message: msg, Event: event, Recipients: recipients}, nil
}
func (s *ChannelStore) SetChannelWallpaper(ctx context.Context, req domain.SetChannelWallpaperRequest) (domain.SetChannelWallpaperResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.SetChannelWallpaperResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("set channel wallpaper: db does not support transactions")
}
if req.Date == 0 {
req.Date = nowUnix()
}
wallpaper := domain.CloneWallpaperPtr(req.Wallpaper)
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("begin set channel wallpaper: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.SetChannelWallpaperResult{}, err
}
if !canChangeChannelInfo(member) {
return domain.SetChannelWallpaperResult{}, domain.ErrChannelAdminRequired
}
if domain.WallpaperEqual(channel.Wallpaper, wallpaper) {
if err := tx.Commit(ctx); err != nil {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("commit unchanged channel wallpaper: %w", err)
}
committed = true
return domain.SetChannelWallpaperResult{Channel: channel}, nil
}
var wallpaperParam any
if wallpaper != nil {
raw, err := json.Marshal(wallpaper)
if err != nil {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("encode channel wallpaper: %w", err)
}
wallpaperParam = string(raw)
}
if _, err := tx.Exec(ctx, `UPDATE channels SET wallpaper = $2::jsonb, updated_at = now() WHERE id = $1`, req.ChannelID, wallpaperParam); err != nil {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("update channel wallpaper: %w", err)
}
channel.Wallpaper = domain.CloneWallpaperPtr(wallpaper)
var (
msg domain.ChannelMessage
event domain.ChannelUpdateEvent
)
if wallpaper != nil {
msg, event, err = s.insertServiceMessage(ctx, tx, channel, req.UserID, req.Date, domain.ChannelMessageAction{
Type: domain.ChannelActionSetChatWallpaper,
Wallpaper: domain.CloneWallpaperPtr(wallpaper),
})
if err != nil {
return domain.SetChannelWallpaperResult{}, err
}
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
if err := upsertChannelDialogTx(ctx, tx, req.UserID, channel, msg, msg.ID, 0); err != nil {
return domain.SetChannelWallpaperResult{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.SetChannelWallpaperResult{}, fmt.Errorf("commit set channel wallpaper: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
return domain.SetChannelWallpaperResult{
Channel: channel,
Message: msg,
Event: event,
Recipients: recipients,
Changed: true,
}, nil
}
func (s *ChannelStore) EditChannelAbout(ctx context.Context, req domain.EditChannelAboutRequest) (domain.Channel, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("edit channel about: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin edit channel about: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if _, err := tx.Exec(ctx, `UPDATE channels SET about = $2, updated_at = now() WHERE id = $1`, req.ChannelID, req.About); err != nil {
return domain.Channel{}, fmt.Errorf("update channel about: %w", err)
}
channel.About = req.About
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit edit channel about: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) CheckUsername(ctx context.Context, userID, channelID int64, username string) (bool, error) {
if userID == 0 || channelID == 0 || strings.TrimSpace(username) == "" {
return false, domain.ErrChannelInvalid
}
if _, _, err := s.getChannelForMember(ctx, s.db, userID, channelID); err != nil {
return false, err
}
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
return peerUsernameAvailable(ctx, s.db, usernameLower, peerUsernameTypeChannel, channelID)
}
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
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("update channel username: db does not support transactions")
}
username := strings.TrimSpace(strings.TrimPrefix(req.Username, "@"))
usernameLower := strings.ToLower(username)
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin update channel username: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.Channel{}, err
}
if member.Role != domain.ChannelRoleCreator {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if strings.EqualFold(channel.Username, username) {
return domain.Channel{}, domain.ErrChannelNotModified
}
if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeChannel, req.ChannelID, username, usernameLower); err != nil {
return domain.Channel{}, err
}
// 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, so the creator
// can hide history again once the group is private.
if _, err := tx.Exec(ctx, `UPDATE channels SET username = NULLIF($2,''), pre_history_hidden = (pre_history_hidden AND $2 = ''), updated_at = now() WHERE id = $1`, req.ChannelID, username); err != nil {
return domain.Channel{}, fmt.Errorf("update channel username: %w", err)
}
if err := markUserChannelMemberIndexPublicTx(ctx, tx, req.ChannelID, username != ""); err != nil {
return domain.Channel{}, err
}
if username != "" && channel.PreHistoryHidden {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: nowUnix(),
Type: domain.ChannelAdminLogTogglePreHistoryHidden,
PrevBool: true,
NewBool: false,
}); err != nil {
return domain.Channel{}, err
}
channel.PreHistoryHidden = false
}
prevUsername := channel.Username
channel.Username = username
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: nowUnix(),
Type: domain.ChannelAdminLogChangeUsername,
PrevString: prevUsername,
NewString: username,
}); err != nil {
return domain.Channel{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit update channel username: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetChannelVerified(ctx context.Context, channelID int64, verified bool) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel, err := s.channelByID(ctx, s.db, channelID)
if err != nil {
return domain.Channel{}, err
}
if channel.Verified == verified {
return channel, nil
}
if _, err := s.db.Exec(ctx, `UPDATE channels SET verified = $2, updated_at = now() WHERE id = $1 AND NOT deleted`, channelID, verified); err != nil {
return domain.Channel{}, fmt.Errorf("set channel verified: %w", err)
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.Verified = verified
return channel, nil
}
// SetChannelScamFake 设置/取消频道的 scam 与 fake 标记。
func (s *ChannelStore) SetChannelScamFake(ctx 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
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("set channel scam/fake: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin set channel scam/fake: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
var currentScam, currentFake bool
if err := tx.QueryRow(ctx, `
SELECT scam, fake
FROM channels
WHERE id = $1 AND NOT deleted
FOR UPDATE`, channelID).Scan(&currentScam, &currentFake); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Channel{}, domain.ErrChannelInvalid
}
return domain.Channel{}, fmt.Errorf("lock channel scam/fake: %w", err)
}
channel, err := s.channelByID(ctx, tx, channelID)
if err != nil {
return domain.Channel{}, err
}
if channel.Scam == scam && channel.Fake == fake {
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit unchanged channel scam/fake: %w", err)
}
committed = true
return channel, nil
}
if currentScam != channel.Scam || currentFake != channel.Fake {
return domain.Channel{}, fmt.Errorf("channel scam/fake snapshot changed while locked")
}
if _, err := tx.Exec(ctx, `UPDATE channels SET scam = $2, fake = $3, updated_at = now() WHERE id = $1 AND NOT deleted`, channelID, scam, fake); err != nil {
return domain.Channel{}, fmt.Errorf("set channel scam/fake: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit channel scam/fake: %w", err)
}
committed = true
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.Scam = scam
channel.Fake = fake
return channel, nil
}
// SetChannelAdminSettings applies an admin-direct moderation-settings patch
// (no membership/permission checks). nil fields are left unchanged.
func (s *ChannelStore) SetChannelAdminSettings(ctx context.Context, channelID int64, patch domain.ChannelAdminSettings) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if patch.Empty() {
return s.channelByID(ctx, s.db, channelID)
}
sets := make([]string, 0, 7)
args := []any{channelID}
idx := 2
add := func(col string, val any) {
sets = append(sets, fmt.Sprintf("%s = $%d", col, idx))
args = append(args, val)
idx++
}
if patch.Gigagroup != nil {
add("gigagroup", *patch.Gigagroup)
}
if patch.AntiSpam != nil {
add("antispam", *patch.AntiSpam)
}
if patch.ParticipantsHidden != nil {
add("participants_hidden", *patch.ParticipantsHidden)
}
if patch.NoForwards != nil {
add("noforwards", *patch.NoForwards)
}
if patch.JoinToSend != nil {
add("join_to_send", *patch.JoinToSend)
}
if patch.JoinRequest != nil {
add("join_request", *patch.JoinRequest)
}
if patch.SlowmodeSeconds != nil {
add("slowmode_seconds", *patch.SlowmodeSeconds)
}
query := "UPDATE channels SET " + strings.Join(sets, ", ") + ", updated_at = now() WHERE id = $1 AND NOT deleted"
if _, err := s.db.Exec(ctx, query, args...); err != nil {
return domain.Channel{}, fmt.Errorf("set channel admin settings: %w", err)
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
return s.channelByID(ctx, s.db, channelID)
}
// SetChannelUsernameAdmin force-sets or clears (empty) a channel username with
// no permission checks. Username uniqueness is still enforced by peer_usernames.
func (s *ChannelStore) SetChannelUsernameAdmin(ctx context.Context, channelID int64, username string) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("set channel username: db does not support transactions")
}
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
usernameLower := strings.ToLower(username)
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin set channel username: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, err := s.channelByID(ctx, tx, channelID)
if err != nil {
return domain.Channel{}, err
}
if strings.EqualFold(channel.Username, username) {
return channel, nil
}
if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeChannel, channelID, username, usernameLower); err != nil {
return domain.Channel{}, err
}
if _, err := tx.Exec(ctx, `UPDATE channels SET username = NULLIF($2,''), updated_at = now() WHERE id = $1`, channelID, username); err != nil {
return domain.Channel{}, fmt.Errorf("set channel username: %w", err)
}
if err := markUserChannelMemberIndexPublicTx(ctx, tx, channelID, username != ""); err != nil {
return domain.Channel{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit set channel username: %w", err)
}
committed = true
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.Username = username
return channel, nil
}
// SetChannelColorAdmin force-sets a channel name/profile color (no permission checks).
func (s *ChannelStore) SetChannelColorAdmin(ctx context.Context, channelID int64, forProfile bool, color domain.ChannelPeerColor) (domain.Channel, error) {
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel, err := s.channelByID(ctx, s.db, channelID)
if err != nil {
return domain.Channel{}, err
}
if forProfile {
if _, err := s.db.Exec(ctx, `UPDATE channels SET profile_color_set = $2, profile_color = $3, profile_color_background_emoji_id = $4, updated_at = now() WHERE id = $1`,
channelID, color.HasColor, color.Color, color.BackgroundEmojiID); err != nil {
return domain.Channel{}, fmt.Errorf("set channel profile color: %w", err)
}
channel.ProfileColor = color
} else {
if _, err := s.db.Exec(ctx, `UPDATE channels SET color_set = $2, color = $3, color_background_emoji_id = $4, updated_at = now() WHERE id = $1`,
channelID, color.HasColor, color.Color, color.BackgroundEmojiID); err != nil {
return domain.Channel{}, fmt.Errorf("set channel color: %w", err)
}
channel.Color = color
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
return channel, nil
}
// SetChannelEmojiStatusAdmin force-sets or clears a channel emoji status (no permission checks).
func (s *ChannelStore) SetChannelEmojiStatusAdmin(ctx 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
}
channel, err := s.channelByID(ctx, s.db, channelID)
if err != nil {
return domain.Channel{}, err
}
if _, err := s.db.Exec(ctx, `UPDATE channels SET emoji_status_document_id = $2, emoji_status_until = $3, updated_at = now() WHERE id = $1`,
channelID, status.DocumentID, status.Until); err != nil {
return domain.Channel{}, fmt.Errorf("set channel emoji status: %w", err)
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.EmojiStatus = status
return channel, nil
}
// SetChannelPhotoAdmin force-sets a channel's avatar with no permission
// checks and no "changed photo" service message — unlike SetChannelPhoto,
// which requires an acting admin member and broadcasts to the channel's
// timeline. Mirrors SetChannelColorAdmin/SetChannelEmojiStatusAdmin's shape.
func (s *ChannelStore) SetChannelPhotoAdmin(ctx context.Context, channelID int64, photo domain.Photo) (domain.Channel, error) {
if channelID == 0 || photo.ID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
channel, err := s.channelByID(ctx, s.db, channelID)
if err != nil {
return domain.Channel{}, err
}
stripped := domain.StrippedFromSizes(photo.Sizes)
if stripped == nil {
stripped = []byte{}
}
result, err := s.db.Exec(ctx, `UPDATE channels
SET photo_id = $2, photo_dc_id = $3, photo_stripped = $4, updated_at = now()
WHERE id = $1 AND NOT deleted`, channelID, photo.ID, photo.DCID, stripped)
if err != nil {
return domain.Channel{}, fmt.Errorf("set channel photo admin: %w", err)
}
if result.RowsAffected() != 1 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.PhotoID = photo.ID
channel.PhotoDCID = photo.DCID
channel.PhotoStripped = stripped
return channel, nil
}
func (s *ChannelStore) ResolvePublicChannelUsername(ctx context.Context, viewerUserID int64, username string) (domain.Channel, bool, error) {
_ = viewerUserID // zero is the anonymous public-web view; this query is viewer-independent.
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
if usernameLower == "" {
return domain.Channel{}, false, nil
}
owner, found, err := getPeerUsernameOwner(ctx, s.db, usernameLower, false)
if err != nil {
return domain.Channel{}, false, fmt.Errorf("resolve public channel username: %w", err)
}
if !found || owner.peerType != peerUsernameTypeChannel {
return domain.Channel{}, false, nil
}
if !owner.active {
return domain.Channel{}, false, nil
}
ch, err := getChannelByID(ctx, s.db, owner.peerID)
if err != nil {
if errors.Is(err, domain.ErrChannelInvalid) {
return domain.Channel{}, false, nil
}
return domain.Channel{}, false, fmt.Errorf("resolve public channel username channel: %w", err)
}
if !publicPreviewableChannel(ch, true) {
return domain.Channel{}, false, nil
}
// A collectible row is authoritative for its own name: the channel's scalar
// username column only ever mirrors the editable slot, so re-checking it here
// would make collectible names unresolvable. The scalar comparison stays for
// the editable slot, where it guards against a stale registry row.
if !owner.collectible && !strings.EqualFold(ch.Username, usernameLower) {
return domain.Channel{}, false, nil
}
return ch, true, nil
}
func (s *ChannelStore) SetSignatures(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel signatures: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel signatures: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.Signatures
if _, err := tx.Exec(ctx, `UPDATE channels SET signatures = $2, updated_at = now() WHERE id = $1`, channelID, enabled); err != nil {
return domain.Channel{}, fmt.Errorf("update channel signatures: %w", err)
}
channel.Signatures = enabled
if prev != enabled {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogToggleSignatures,
PrevBool: prev,
NewBool: enabled,
}); err != nil {
return domain.Channel{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel signatures: %w", err)
}
committed = true
return channel, nil
}
// SetChannelPhoto 设置/清除频道头像并生成对应频道服务消息。photo==nil 表示清除。
func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) {
if userID == 0 || channelID == 0 {
return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.SetChannelPhotoResult{}, fmt.Errorf("set channel photo: db does not support transactions")
}
if date == 0 {
date = nowUnix()
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.SetChannelPhotoResult{}, fmt.Errorf("begin set channel photo: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.SetChannelPhotoResult{}, err
}
if !canChangeChannelInfo(member) {
return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired
}
var (
photoID int64
dcID int
stripped []byte
action domain.ChannelMessageAction
)
if photo != nil && photo.ID != 0 {
if channel.PhotoID == photo.ID {
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
}
photoID = photo.ID
dcID = photo.DCID
stripped = domain.StrippedFromSizes(photo.Sizes)
action = domain.ChannelMessageAction{
Type: domain.ChannelActionChatEditPhoto,
Photo: domain.ClonePhotoPtr(photo),
}
} else {
if channel.PhotoID == 0 {
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
}
action = domain.ChannelMessageAction{Type: domain.ChannelActionChatDeletePhoto}
}
if stripped == nil {
stripped = []byte{}
}
if _, err := tx.Exec(ctx, `UPDATE channels SET photo_id = $2, photo_dc_id = $3, photo_stripped = $4, updated_at = now() WHERE id = $1`,
channelID, photoID, dcID, stripped); err != nil {
return domain.SetChannelPhotoResult{}, fmt.Errorf("update channel photo: %w", err)
}
if s.rowCache != nil {
s.rowCache.delete(channelID)
}
channel.PhotoID = photoID
channel.PhotoDCID = dcID
channel.PhotoStripped = stripped
msg, event, err := s.insertServiceMessage(ctx, tx, channel, userID, date, action)
if err != nil {
return domain.SetChannelPhotoResult{}, err
}
channel.TopMessageID = msg.ID
channel.Pts = event.Pts
if err := upsertChannelDialogTx(ctx, tx, userID, channel, msg, msg.ID, 0); err != nil {
return domain.SetChannelPhotoResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.SetChannelPhotoResult{}, fmt.Errorf("commit set channel photo: %w", err)
}
committed = true
recipients, _ := s.ListActiveChannelMemberIDs(ctx, userID, channelID, 0)
return domain.SetChannelPhotoResult{
Channel: channel,
Message: msg,
Event: event,
Recipients: recipients,
Changed: true,
}, nil
}
func (s *ChannelStore) SetAutotranslation(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel autotranslation: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel autotranslation: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.Autotranslation
if _, err := tx.Exec(ctx, `UPDATE channels SET autotranslation = $2, updated_at = now() WHERE id = $1`, channelID, enabled); err != nil {
return domain.Channel{}, fmt.Errorf("update channel autotranslation: %w", err)
}
channel.Autotranslation = enabled
if prev != enabled {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogToggleAutotranslation,
PrevBool: prev,
NewBool: enabled,
}); err != nil {
return domain.Channel{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel autotranslation: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetRestrictedSponsored(ctx context.Context, userID, channelID int64, restricted bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel restricted sponsored: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel restricted sponsored: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if _, err := tx.Exec(ctx, `UPDATE channels SET restricted_sponsored = $2, updated_at = now() WHERE id = $1`, channelID, restricted); err != nil {
return domain.Channel{}, fmt.Errorf("update channel restricted sponsored: %w", err)
}
channel.RestrictedSponsored = restricted
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel restricted sponsored: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetAntiSpam(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel antispam: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel antispam: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !channel.Megagroup || !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.AntiSpam
if _, err := tx.Exec(ctx, `UPDATE channels SET antispam = $2, updated_at = now() WHERE id = $1`, channelID, enabled); err != nil {
return domain.Channel{}, fmt.Errorf("update channel antispam: %w", err)
}
channel.AntiSpam = enabled
if prev != enabled {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogToggleAntiSpam,
PrevBool: prev,
NewBool: enabled,
}); err != nil {
return domain.Channel{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel antispam: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetSlowMode(ctx context.Context, userID, channelID int64, seconds int) (domain.Channel, error) {
if userID == 0 || channelID == 0 || !domain.ValidChannelSlowModeSeconds(seconds) {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel slowmode: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel slowmode: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !channel.Megagroup || !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
prev := channel.SlowmodeSeconds
if _, err := tx.Exec(ctx, `UPDATE channels SET slowmode_seconds = $2, updated_at = now() WHERE id = $1`, channelID, seconds); err != nil {
return domain.Channel{}, fmt.Errorf("update channel slowmode: %w", err)
}
channel.SlowmodeSeconds = seconds
if prev != seconds {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogToggleSlowMode,
PrevInt: prev,
NewInt: seconds,
}); err != nil {
return domain.Channel{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel slowmode: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetNoForwards(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("toggle channel noforwards: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin toggle channel noforwards: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if _, err := tx.Exec(ctx, `UPDATE channels SET noforwards = $2, updated_at = now() WHERE id = $1`, channelID, enabled); err != nil {
return domain.Channel{}, fmt.Errorf("update channel noforwards: %w", err)
}
channel.NoForwards = enabled
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit toggle channel noforwards: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetColor(ctx context.Context, userID, channelID int64, forProfile bool, color domain.ChannelPeerColor) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("set channel color: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin set channel color: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if forProfile {
if _, err := tx.Exec(ctx, `UPDATE channels SET profile_color_set = $2, profile_color = $3, profile_color_background_emoji_id = $4, updated_at = now() WHERE id = $1`,
channelID, color.HasColor, color.Color, color.BackgroundEmojiID); err != nil {
return domain.Channel{}, fmt.Errorf("update channel profile color: %w", err)
}
channel.ProfileColor = color
} else {
if _, err := tx.Exec(ctx, `UPDATE channels SET color_set = $2, color = $3, color_background_emoji_id = $4, updated_at = now() WHERE id = $1`,
channelID, color.HasColor, color.Color, color.BackgroundEmojiID); err != nil {
return domain.Channel{}, fmt.Errorf("update channel color: %w", err)
}
channel.Color = color
}
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit set channel color: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) SetEmojiStatus(ctx context.Context, userID, channelID int64, status domain.ChannelEmojiStatus) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if status.DocumentID == 0 {
status.Until = 0
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.Channel{}, fmt.Errorf("set channel emoji status: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.Channel{}, fmt.Errorf("begin set channel emoji status: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return domain.Channel{}, err
}
if !canChangeChannelInfo(member) {
return domain.Channel{}, domain.ErrChannelAdminRequired
}
if _, err := tx.Exec(ctx, `UPDATE channels SET emoji_status_document_id = $2, emoji_status_until = $3, updated_at = now() WHERE id = $1`,
channelID, status.DocumentID, status.Until); err != nil {
return domain.Channel{}, fmt.Errorf("update channel emoji status: %w", err)
}
channel.EmojiStatus = status
if err := tx.Commit(ctx); err != nil {
return domain.Channel{}, fmt.Errorf("commit set channel emoji status: %w", err)
}
committed = true
return channel, nil
}
func (s *ChannelStore) ListChannelRecommendations(ctx 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
}
probeLimit := domain.MaxChannelRecommendationsLimit
if probeLimit < limit {
probeLimit = limit
}
args := []any{req.UserID, req.SourceChannelID}
where := []string{
"($1::bigint <> 0)",
"c.broadcast",
"NOT c.megagroup",
"NOT c.deleted",
"COALESCE(c.username, '') <> ''",
"($2::bigint = 0 OR c.id <> $2)",
}
if req.SourceChannelID == 0 {
where = append(where, `NOT EXISTS (
SELECT 1
FROM user_channel_member_index i
WHERE i.user_id = $1
AND i.channel_id = c.id
AND i.status = 'active'
)`)
}
whereSQL := strings.Join(where, " AND ")
args = append(args, probeLimit)
rows, err := s.db.Query(ctx, `
SELECT `+channelColumns+`
FROM channels c
WHERE `+whereSQL+`
ORDER BY c.participants_count DESC, c.date DESC, c.id DESC
LIMIT $3`, args...)
if err != nil {
return domain.ChannelRecommendationsResult{}, fmt.Errorf("list channel recommendations: %w", err)
}
defer rows.Close()
out := domain.ChannelRecommendationsResult{Channels: make([]domain.Channel, 0, limit)}
for rows.Next() {
ch, err := scanChannel(rows)
if err != nil {
return domain.ChannelRecommendationsResult{}, err
}
out.Count++
if len(out.Channels) < limit {
out.Channels = append(out.Channels, ch)
}
}
if err := rows.Err(); err != nil {
return domain.ChannelRecommendationsResult{}, err
}
return out, nil
}
func (s *ChannelStore) ListDiscussionGroups(ctx 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
}
rows, err := s.db.Query(ctx, `
SELECT channel_id
FROM user_channel_member_index
WHERE user_id = $1
AND status = 'active'
AND megagroup
AND NOT broadcast
AND NOT forum
AND NOT deleted
AND (role = 'creator' OR can_pin_messages)
ORDER BY channel_id DESC
LIMIT $2`, userID, limit)
if err != nil {
return nil, fmt.Errorf("list discussion groups: %w", err)
}
defer rows.Close()
ids := make([]int64, 0, limit)
for rows.Next() {
var channelID int64
if err := rows.Scan(&channelID); err != nil {
return nil, err
}
ids = append(ids, channelID)
}
if err := rows.Err(); err != nil {
return nil, err
}
channels, err := listChannelsByIDsInOrder(ctx, s.db, ids)
if err != nil {
return nil, fmt.Errorf("list discussion group details: %w", err)
}
out := make([]domain.Channel, 0, len(channels))
for _, channel := range channels {
if !channel.Megagroup || channel.Broadcast || channel.Forum || channel.Deleted {
continue
}
out = append(out, channel)
}
return out, nil
}
func (s *ChannelStore) SetDiscussionGroup(ctx 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
}
beginner, ok := s.db.(txBeginner)
if !ok {
return domain.DiscussionGroupUpdateResult{}, fmt.Errorf("set discussion group: db does not support transactions")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return domain.DiscussionGroupUpdateResult{}, fmt.Errorf("begin set discussion group: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
changed := make(map[int64]domain.Channel)
markChanged := func(channel domain.Channel) {
if channel.ID != 0 {
changed[channel.ID] = channel
}
}
setLinked := func(channel domain.Channel, linkedID int64) (domain.Channel, error) {
if channel.LinkedChatID == linkedID {
return channel, nil
}
if _, err := tx.Exec(ctx, `UPDATE channels SET linked_chat_id = $2, updated_at = now() WHERE id = $1`, channel.ID, linkedID); err != nil {
return domain.Channel{}, fmt.Errorf("update linked chat: %w", err)
}
channel.LinkedChatID = linkedID
markChanged(channel)
return channel, nil
}
logLinkChange := func(channelID, prev, next int64) error {
if prev == next || channelID == 0 {
return nil
}
return s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogChangeLinkedChat,
PrevInt: int(prev),
NewInt: int(next),
})
}
if broadcastID == 0 {
group, groupMember, err := s.getChannelForMember(ctx, tx, 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
}
oldBroadcast, err := getChannelByID(ctx, tx, oldBroadcastID)
if err == nil && oldBroadcast.LinkedChatID == groupID {
updated, err := setLinked(oldBroadcast, 0)
if err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := logLinkChange(updated.ID, groupID, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
} else if err != nil && !errors.Is(err, domain.ErrChannelInvalid) {
return domain.DiscussionGroupUpdateResult{}, err
}
if _, err := setLinked(group, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.DiscussionGroupUpdateResult{}, fmt.Errorf("commit set discussion group: %w", err)
}
committed = true
return discussionGroupUpdateResult(changed), nil
}
broadcast, broadcastMember, err := s.getChannelForMember(ctx, tx, 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, err := setLinked(broadcast, 0)
if err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := logLinkChange(updated.ID, oldGroupID, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
oldGroup, err := getChannelByID(ctx, tx, oldGroupID)
if err == nil && oldGroup.LinkedChatID == broadcastID {
if _, err := setLinked(oldGroup, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
} else if err != nil && !errors.Is(err, domain.ErrChannelInvalid) {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.DiscussionGroupUpdateResult{}, fmt.Errorf("commit set discussion group: %w", err)
}
committed = true
return discussionGroupUpdateResult(changed), nil
}
group, groupMember, err := s.getChannelForMember(ctx, tx, 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 {
oldGroup, err := getChannelByID(ctx, tx, oldGroupID)
if err == nil && oldGroup.LinkedChatID == broadcastID {
if _, err := setLinked(oldGroup, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
} else if err != nil && !errors.Is(err, domain.ErrChannelInvalid) {
return domain.DiscussionGroupUpdateResult{}, err
}
}
if oldBroadcastID != 0 && oldBroadcastID != broadcastID {
oldBroadcast, err := getChannelByID(ctx, tx, oldBroadcastID)
if err == nil && oldBroadcast.LinkedChatID == groupID {
updated, err := setLinked(oldBroadcast, 0)
if err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := logLinkChange(updated.ID, groupID, 0); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
} else if err != nil && !errors.Is(err, domain.ErrChannelInvalid) {
return domain.DiscussionGroupUpdateResult{}, err
}
}
updatedBroadcast, err := setLinked(broadcast, groupID)
if err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if _, err := setLinked(group, broadcastID); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := logLinkChange(updatedBroadcast.ID, oldGroupID, groupID); err != nil {
return domain.DiscussionGroupUpdateResult{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.DiscussionGroupUpdateResult{}, fmt.Errorf("commit set discussion group: %w", err)
}
committed = true
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
}