owpengram-server/internal/store/postgres/star_gift_craft_projection.go
A 14bf7d1e20 feat: sync official star gift lifecycle tooling
Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling).

Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
2026-07-19 03:11:35 +08:00

227 lines
8.5 KiB
Go

package postgres
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
)
// markCraftInputMessagesTx makes the chat projection part of the same commit
// as the craft outcome. TDesktop derives the Craft entry directly from the
// messageActionStarGiftUnique snapshot, so changing only peer_star_gifts and
// unique_star_gifts would leave an already-burned input actionable.
func (s *StarGiftLifecycleStore) markCraftInputMessagesTx(
ctx context.Context,
tx pgx.Tx,
req domain.StarGiftCraftRequest,
savedIDs []int64,
) ([]domain.EditedMessageForUser, []int32, error) {
edits := make([]domain.EditedMessageForUser, 0, len(savedIDs)*2)
ownerPTS := make([]int32, 0, len(savedIDs))
for _, savedID := range savedIDs {
saved, found, err := savedStarGiftByID(ctx, tx, savedID)
if err != nil || !found || saved.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) ||
saved.UniqueGiftID <= 0 || saved.UpgradeMsgID <= 0 {
if err != nil {
return nil, nil, err
}
return nil, nil, domain.ErrStarGiftCraftUnavailable
}
unique, found, err := NewStarGiftStore(tx).UniqueByID(ctx, saved.UniqueGiftID)
if err != nil || !found {
if err != nil {
return nil, nil, err
}
return nil, nil, domain.ErrStarGiftCraftUnavailable
}
inputEdits, ownerPT, err := s.markCraftInputMessageTx(ctx, tx, req, saved, unique)
if err != nil {
return nil, nil, err
}
edits = append(edits, inputEdits...)
ownerPTS = append(ownerPTS, int32(ownerPT))
}
return edits, ownerPTS, nil
}
func (s *StarGiftLifecycleStore) markCraftInputMessageTx(
ctx context.Context,
tx pgx.Tx,
req domain.StarGiftCraftRequest,
saved domain.SavedStarGift,
unique domain.UniqueStarGift,
) ([]domain.EditedMessageForUser, int, error) {
q := sqlcgen.New(tx)
target, err := q.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{
OwnerUserID: req.UserID,
BoxID: int32(saved.UpgradeMsgID),
PeerType: string(domain.PeerTypeUser),
PeerID: saved.FromUserID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, domain.ErrStarGiftCraftUnavailable
}
return nil, 0, fmt.Errorf("lock craft input message: %w", err)
}
boxes, err := q.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
OwnerUserIds: privateMessageOwnerIDs(req.UserID, saved.FromUserID),
MessageSenderID: target.MessageSenderID,
PrivateMessageID: target.PrivateMessageID,
})
if err != nil {
return nil, 0, fmt.Errorf("list craft input message boxes: %w", err)
}
if len(boxes) == 0 {
return nil, 0, domain.ErrStarGiftCraftUnavailable
}
edits := make([]domain.EditedMessageForUser, 0, len(boxes))
ownerPTS := 0
var privateMediaJSON []byte
for _, box := range boxes {
media, err := decodeMessageMedia(box.MediaJson)
if err != nil {
return nil, 0, fmt.Errorf("decode craft input message media: %w", err)
}
if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil ||
media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique || media.ServiceAction.StarGiftUnique == nil ||
media.ServiceAction.StarGiftUnique.Gift.ID != unique.ID {
return nil, 0, fmt.Errorf("craft input message %d has invalid unique gift projection", box.BoxID)
}
action := media.ServiceAction.StarGiftUnique
action.Gift = unique
action.Saved = saved.LifecycleStatus.Live() && !saved.Unsaved
action.CanExportAt = saved.CanExportAt
action.TransferStars = saved.TransferStars
action.CanTransferAt = saved.CanTransferAt
action.CanResellAt = saved.CanResellAt
action.DropOriginalDetailsStars = saved.DropOriginalDetailsStars
action.CanCraftAt = saved.CanCraftAt
mediaJSON, err := encodeMessageMedia(media)
if err != nil {
return nil, 0, fmt.Errorf("encode craft input message media: %w", err)
}
pts, err := s.messages.reservePts(ctx, tx, box.OwnerUserID)
if err != nil {
return nil, 0, fmt.Errorf("allocate craft input edit pts: %w", err)
}
tag, err := tx.Exec(ctx, `
UPDATE message_boxes SET media=$3,pts=$4
WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxID, mediaJSON, int32(pts))
if err != nil {
return nil, 0, fmt.Errorf("update craft input message box: %w", err)
}
if tag.RowsAffected() != 1 {
return nil, 0, fmt.Errorf("update craft input message box lost row")
}
msg, err := messageFromVisibleBoxRow(box)
if err != nil {
return nil, 0, err
}
msg.Media = media
msg.Pts = pts
if err := replaceMessageBoxMediaIndexTx(ctx, tx, msg.OwnerUserID, msg.Peer.ID, msg.ID, msg.Date, msg.Media, msg.Entities); err != nil {
return nil, 0, err
}
event := domain.UpdateEvent{UserID: msg.OwnerUserID, Type: domain.UpdateEventEditMessage,
Pts: pts, PtsCount: 1, Date: req.Date, Message: msg}
if err := appendUserUpdateEvent(ctx, tx, q, msg.OwnerUserID, event); err != nil {
return nil, 0, fmt.Errorf("append craft input edit event: %w", err)
}
dispatchAuthKeyID := [8]byte{}
dispatchSessionID := int64(0)
if msg.OwnerUserID == req.UserID {
dispatchAuthKeyID = req.OriginAuthKeyID
dispatchSessionID = req.OriginSessionID
ownerPTS = pts
}
if err := enqueueDispatch(ctx, q, sqlcgen.EnqueueDispatchParams{
TargetUserID: msg.OwnerUserID, Pts: int32(pts), EventType: string(domain.UpdateEventEditMessage),
ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID), ExcludeSessionID: dispatchSessionID,
}); err != nil {
return nil, 0, fmt.Errorf("enqueue craft input edit: %w", err)
}
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
privateMediaJSON = mediaJSON
}
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
}
if ownerPTS <= 0 || len(privateMediaJSON) == 0 {
return nil, 0, fmt.Errorf("craft input message missing owner projection")
}
if _, err := tx.Exec(ctx, `
UPDATE private_messages SET media=$3
WHERE sender_user_id=$1 AND id=$2`, target.MessageSenderID, target.PrivateMessageID, privateMediaJSON); err != nil {
return nil, 0, fmt.Errorf("update craft input private message: %w", err)
}
return edits, ownerPTS, nil
}
func (s *StarGiftLifecycleStore) loadCraftInputMessageReplays(
ctx context.Context,
req domain.StarGiftCraftRequest,
savedIDs []int64,
ptsValues []int32,
) ([]domain.EditedMessageForUser, error) {
if len(savedIDs) != len(ptsValues) {
return nil, domain.ErrStarGiftCraftUnavailable
}
edits := make([]domain.EditedMessageForUser, 0, len(savedIDs))
for i, savedID := range savedIDs {
saved, found, err := savedStarGiftByID(ctx, s.db, savedID)
if err != nil || !found || saved.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) ||
saved.UpgradeMsgID <= 0 || ptsValues[i] <= 0 {
if err != nil {
return nil, err
}
return nil, domain.ErrStarGiftCraftUnavailable
}
var privateMessageID, messageSenderID int64
err = s.db.QueryRow(ctx, `
SELECT private_message_id,message_sender_id FROM message_boxes
WHERE owner_user_id=$1 AND box_id=$2 AND peer_type='user' AND peer_id=$3 AND NOT deleted`,
req.UserID, saved.UpgradeMsgID, saved.FromUserID).Scan(&privateMessageID, &messageSenderID)
if errors.Is(err, pgx.ErrNoRows) {
continue
}
if err != nil {
return nil, fmt.Errorf("load craft input replay message: %w", err)
}
boxes, err := sqlcgen.New(s.db).ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
OwnerUserIds: []int64{req.UserID}, MessageSenderID: messageSenderID, PrivateMessageID: privateMessageID,
})
if err != nil {
return nil, fmt.Errorf("load craft input replay box: %w", err)
}
if len(boxes) != 1 || int(boxes[0].BoxID) != saved.UpgradeMsgID {
return nil, domain.ErrStarGiftCraftUnavailable
}
var eventDate int
err = s.db.QueryRow(ctx, `
SELECT date FROM user_update_events
WHERE user_id=$1 AND pts=$2 AND event_type='edit_message' AND message_box_id=$3`,
req.UserID, ptsValues[i], saved.UpgradeMsgID).Scan(&eventDate)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, domain.ErrStarGiftCraftUnavailable
}
return nil, fmt.Errorf("load craft input replay event: %w", err)
}
msg, err := messageFromVisibleBoxRow(boxes[0])
if err != nil {
return nil, err
}
msg.Pts = int(ptsValues[i])
event := domain.UpdateEvent{UserID: req.UserID, Type: domain.UpdateEventEditMessage,
Pts: int(ptsValues[i]), PtsCount: 1, Date: eventDate, Message: msg}
edits = append(edits, domain.EditedMessageForUser{UserID: req.UserID, Message: msg, Event: event})
}
return edits, nil
}