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.
227 lines
8.5 KiB
Go
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
|
|
}
|