Ctrl K
ComunidadGuías…

internal/store/postgres/channel_message_settings.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 {
			// Disabling "restrict new members from viewing history" must return the member-side read boundary. All writes to available_min_id
			// (upsert / batch / clear history) are monotonically increasing GREATEST, and no write can lower it;
			// without an explicit zeroing here, a member who "joined while history was hidden" would permanently see an empty channel, and rejoining
			// would not fix it either (rejoining computes minID=0, but the upsert takes GREATEST(old value,0)=old value).
			// Only reset the purely prehistory-sourced boundary: history_clear_anchor_id>0 means the boundary came from the member's
			// own "clear history", which has owner-local semantics and must not be rewritten by a group setting.
			if _, err := tx.Exec(ctx, `
UPDATE channel_members
SET available_min_id = 0,
    updated_at = now()
WHERE channel_id = $1
  AND available_min_id > 0
  AND COALESCE(history_clear_anchor_id, 0) = 0`, channelID); err != nil {
				return fmt.Errorf("release prehistory member boundaries: %w", err)
			}
			if _, err := tx.Exec(ctx, `
UPDATE user_channel_member_index
SET available_min_id = 0,
    history_clear_updated_at = extract(epoch from now())::int,
    updated_at = now()
WHERE channel_id = $1
  AND available_min_id > 0
  AND COALESCE(history_clear_anchor_id, 0) = 0`, channelID); err != nil {
				return fmt.Errorf("release prehistory member index boundaries: %w", err)
			}
		}
		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))
		// Enabling channel Direct Messages links a monoforum virtual channel to the parent channel; enabling again reuses the existing
		// monoforum and only syncs the price. Disabling (broadcastAllowed=false) keeps the monoforum so it can be reused when re-enabled.
		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 {
				// Reusing an existing monoforum only syncs the price. The creation path always builds a mono as a megagroup, and no path flips it
				// into broadcast/forum, so there is no need (and it would be wrong) to re-normalize the kind here—that would be a historical-data
				// repair for a never-released broadcast+monoforum experimental shape, dead code in a project with no legacy data.
				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)
				}
			}
			// A monoforum's first (and only) service message is its "creation" message (messageActionChannelCreate):
			// for a monoforum, clients specifically render the creation message as "Direct messages were enabled in this channel."
			// (TDesktop lng_action_created_monoforum). The paid_messages_price service message for toggling/price changes
			// goes only into the parent broadcast channel (rendered in broadcast history as "Channel enabled/disabled Direct Messages"),
			// **never into the mono**—the mono is a megagroup (not a broadcast), so the same action would render the wrong text
			// "messages are free / set to N stars". Disabling only shows the disabled footer via the monoforumDisabled state
			// (hiding the parent channel's linked_monoforum_id) and no longer appends a bubble to the 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})
			}
		}
		// The monoforum mirrors the parent channel's DM enabled state into its own broadcast_messages_allowed column. At projection time this decides
		// whether to send down the mono's own linked_monoforum_id: enabled=send, disabled=hide. When disabled, both the parent channel and the mono
		// hide the link, so when the client opens the monoforum and re-fetches it will not re-call setMonoforumLink(parent) and clear MonoforumDisabled,
		// keeping the disabled footer. The internal association row (linked_monoforum_id) is still kept for reuse when re-enabled.
		// Write the mirror column only when the mono's DM enabled state actually changes: this avoids a pointless rewrite of the mono row on every
		// SetPaidMessagesPrice (including an unchanged re-enable) and the resulting redundant cross-instance LISTEN/NOTIFY invalidation of the channels table. The enable branch already
		// loads the mono via getChannelByID (mono.ID!=0, fields reflect the current DB values); the disable branch does not load the mono (mono.ID==0),
		// so it cannot cheaply compare and keeps writing unconditionally to ensure the disabled footer state is correct.
		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
}