owpengram-server/internal/store/postgres/channel_message_settings.go

178 lines
8.2 KiB
Go

package postgres
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
)
func (s *ChannelStore) SetPreHistoryHidden(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) {
if userID == 0 || channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
var channel domain.Channel
if err := withTx(ctx, s.db, "toggle channel prehistory", func(tx pgx.Tx) error {
var err error
var member domain.ChannelMember
channel, member, err = s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return err
}
if member.Role != domain.ChannelRoleCreator {
return domain.ErrChannelAdminRequired
}
prev := channel.PreHistoryHidden
if _, err := tx.Exec(ctx, `UPDATE channels SET pre_history_hidden = $2, updated_at = now() WHERE id = $1`, channelID, enabled); err != nil {
return fmt.Errorf("update channel prehistory: %w", err)
}
channel.PreHistoryHidden = enabled
if prev != enabled {
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
ChannelID: channelID,
UserID: userID,
Date: nowUnix(),
Type: domain.ChannelAdminLogTogglePreHistoryHidden,
PrevBool: prev,
NewBool: enabled,
}); err != nil {
return err
}
}
return nil
}); err != nil {
return domain.Channel{}, err
}
return channel, nil
}
func (s *ChannelStore) SetPaidMessagesPrice(ctx context.Context, userID, channelID int64, stars int64, broadcastMessagesAllowed bool) (domain.ChannelPaidMessagesPriceResult, error) {
if userID == 0 || channelID == 0 || stars < 0 {
return domain.ChannelPaidMessagesPriceResult{}, domain.ErrChannelInvalid
}
var channel domain.Channel
services := make([]domain.SendChannelMessageResult, 0, 2)
if err := withTx(ctx, s.db, "update channel paid messages price", func(tx pgx.Tx) error {
var err error
var member domain.ChannelMember
channel, member, err = s.getChannelForMember(ctx, tx, userID, channelID)
if err != nil {
return err
}
if !canChangeChannelInfo(member) {
return domain.ErrChannelAdminRequired
}
prevAllowed := channel.BroadcastMessagesAllowed
prevStars := channel.SendPaidMessagesStars
prevLinked := channel.LinkedMonoforumID
broadcastAllowed := channel.Broadcast && broadcastMessagesAllowed
if _, err := tx.Exec(ctx, `UPDATE channels SET send_paid_messages_stars = $2, broadcast_messages_allowed = $3, updated_at = now() WHERE id = $1`, channelID, stars, broadcastAllowed); err != nil {
return fmt.Errorf("update channel paid messages price: %w", err)
}
channel.SendPaidMessagesStars = stars
channel.BroadcastMessagesAllowed = broadcastAllowed
changed := channel.Broadcast && (prevAllowed != broadcastAllowed || prevStars != stars || (broadcastAllowed && prevLinked == 0))
// 开启频道私信(Direct Messages)时为母频道关联一个 monoforum 虚拟频道;再次开启复用既有
// monoforum,仅同步价格。关闭(broadcastAllowed=false)保留 monoforum 以便重新开启时复用。
var mono domain.Channel
if broadcastAllowed {
if channel.LinkedMonoforumID == 0 {
monoID, err := s.allocateFreshChannelID(ctx)
if err != nil {
return err
}
accessHash, err := randomChannelAccessHash()
if err != nil {
return err
}
mono = domain.Channel{
ID: monoID,
AccessHash: accessHash,
CreatorUserID: channel.CreatorUserID,
Title: channel.Title,
// Keep monoforum as a megagroup-like saved sublist container. TDesktop hangs when
// a monoforum peer is projected as both broadcast and megagroup.
Megagroup: true,
Monoforum: true,
LinkedMonoforumID: channelID,
SendPaidMessagesStars: stars,
Date: nowUnix(),
}
if err := insertChannelTx(ctx, tx, mono); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE channels SET linked_monoforum_id = $2, updated_at = now() WHERE id = $1`, channelID, monoID); err != nil {
return fmt.Errorf("link channel monoforum: %w", err)
}
channel.LinkedMonoforumID = monoID
} else {
// 复用既有 monoforum 只同步价格。创建路径恒以 megagroup 形态建 mono,无任何路径会把它
// 翻成 broadcast/forum,故此处无需(也不应)再规范化 kind——那是对从未发布的 broadcast+
// monoforum 实验形态的历史数据修复,在无遗留数据的项目里是死代码。
if _, err := tx.Exec(ctx, `UPDATE channels SET send_paid_messages_stars = $2, updated_at = now() WHERE id = $1`, channel.LinkedMonoforumID, stars); err != nil {
return fmt.Errorf("sync monoforum paid messages price: %w", err)
}
mono, err = getChannelByID(ctx, tx, channel.LinkedMonoforumID)
if err != nil {
return fmt.Errorf("load monoforum for paid messages service: %w", err)
}
}
// monoforum 的首条(也是唯一)服务消息是它的"创建"消息(messageActionChannelCreate):
// 客户端对 monoforum 专门把创建消息渲染为 "Direct messages were enabled in this channel."
// (TDesktop lng_action_created_monoforum)。开关/价格变更的 paid_messages_price 服务消息
// 只进母广播频道(在 broadcast 历史里渲染为 "Channel enabled/disabled Direct Messages"),
// **绝不进 mono**——mono 是 megagroup(非 broadcast),同一 action 会被渲染成"消息免费/
// 设为 N 星"的错误文案。关闭仅靠 monoforumDisabled 状态(隐藏母频道 linked_monoforum_id)
// 显示停用页脚,不再往 mono 追加气泡。
if mono.ID != 0 && mono.TopMessageID == 0 {
createMsg, createEvent, err := s.insertServiceMessage(ctx, tx, mono, userID, nowUnix(), domain.ChannelMessageAction{Type: domain.ChannelActionCreate, Title: mono.Title})
if err != nil {
return fmt.Errorf("append monoforum creation service: %w", err)
}
mono.TopMessageID = createMsg.ID
mono.Pts = createEvent.Pts
services = append(services, domain.SendChannelMessageResult{Channel: mono, Message: createMsg, Event: createEvent})
}
}
// monoforum 把母频道的 DM 启用状态镜像到自己的 broadcast_messages_allowed 列。投影时据此决定
// 是否下发 mono 自身的 linked_monoforum_id:开启=下发,关闭=隐藏。关闭时母频道与 mono 双方都
// 隐藏 link,客户端打开 monoforum 重新拉取时不会重新 setMonoforumLink(parent) 而清掉 MonoforumDisabled,
// 停用页脚得以保持。内部关联行(linked_monoforum_id)仍保留以便重新开启复用。
// 仅在 mono 的 DM 启用状态实际变化时才写镜像列:避免每次 SetPaidMessagesPrice(含未变更的
// 再次开启)都对 mono 行白写一遍并触发 channels 表多余的 LISTEN/NOTIFY 跨实例失效。开启分支已
// 把 mono 经 getChannelByID 读入(mono.ID!=0,字段反映库内当前值);关闭分支不加载 mono(mono.ID==0)
// 故无法廉价比对、保持照写以确保停用页脚状态正确。
if channel.LinkedMonoforumID != 0 && (mono.ID == 0 || mono.BroadcastMessagesAllowed != broadcastAllowed) {
if _, err := tx.Exec(ctx, `UPDATE channels SET broadcast_messages_allowed = $2, updated_at = now() WHERE id = $1`, channel.LinkedMonoforumID, broadcastAllowed); err != nil {
return fmt.Errorf("sync monoforum dm-enabled state: %w", err)
}
if mono.ID != 0 {
mono.BroadcastMessagesAllowed = broadcastAllowed
}
}
if changed && channel.LinkedMonoforumID != 0 {
action := domain.ChannelMessageAction{
Type: domain.ChannelActionPaidMessagesPrice,
BroadcastMessagesAllowed: broadcastAllowed,
Stars: stars,
}
parentMsg, parentEvent, err := s.insertServiceMessage(ctx, tx, channel, userID, nowUnix(), action)
if err != nil {
return fmt.Errorf("append parent paid messages price service: %w", err)
}
channel.TopMessageID = parentMsg.ID
channel.Pts = parentEvent.Pts
services = append(services, domain.SendChannelMessageResult{Channel: channel, Message: parentMsg, Event: parentEvent})
}
return nil
}); err != nil {
return domain.ChannelPaidMessagesPriceResult{}, err
}
var first *domain.SendChannelMessageResult
if len(services) > 0 {
first = &services[0]
}
return domain.ChannelPaidMessagesPriceResult{Channel: channel, ServiceMessage: first, ServiceMessages: services}, nil
}