196 lines
6.8 KiB
Go
196 lines
6.8 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/postgres/sqlcgen"
|
|
)
|
|
|
|
// retireUserStarGiftMessagesTx closes every user-scoped unique-gift action
|
|
// emitted for the source ownership epoch. Ownership moves and terminal export
|
|
// must not leave an older chat card with Craft/transfer/resale capabilities.
|
|
// The aggregate mutation and all message edits share one transaction and each
|
|
// visible box receives its own durable pts/event/outbox entry.
|
|
func (s *StarGiftLifecycleStore) retireUserStarGiftMessagesTx(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
source domain.SavedStarGift,
|
|
current domain.UniqueStarGift,
|
|
date int,
|
|
) ([]domain.EditedMessageForUser, error) {
|
|
if s == nil || s.messages == nil || source.Owner.Type != domain.PeerTypeUser || source.Owner.ID <= 0 ||
|
|
source.ID <= 0 || source.UniqueGiftID <= 0 || current.ID != source.UniqueGiftID || date <= 0 {
|
|
return nil, domain.ErrStarGiftTransferUnavailable
|
|
}
|
|
|
|
messageIDs := map[int]struct{}{}
|
|
if source.MsgID > 0 {
|
|
messageIDs[source.MsgID] = struct{}{}
|
|
}
|
|
if source.UpgradeMsgID > 0 {
|
|
messageIDs[source.UpgradeMsgID] = struct{}{}
|
|
}
|
|
rows, err := tx.Query(ctx, `
|
|
SELECT msg_id FROM star_gift_user_message_refs
|
|
WHERE owner_user_id=$1 AND saved_gift_id=$2
|
|
ORDER BY msg_id`, source.Owner.ID, source.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list star gift message projections: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var msgID int
|
|
if err := rows.Scan(&msgID); err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("scan star gift message projection: %w", err)
|
|
}
|
|
if msgID > 0 {
|
|
messageIDs[msgID] = struct{}{}
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("iterate star gift message projections: %w", err)
|
|
}
|
|
rows.Close()
|
|
|
|
ids := make([]int, 0, len(messageIDs))
|
|
for msgID := range messageIDs {
|
|
ids = append(ids, msgID)
|
|
}
|
|
sort.Ints(ids)
|
|
|
|
q := sqlcgen.New(tx)
|
|
edits := make([]domain.EditedMessageForUser, 0, len(ids)*2)
|
|
seenPrivateMessages := make(map[string]struct{}, len(ids))
|
|
for _, msgID := range ids {
|
|
var peerType string
|
|
var peerID int64
|
|
err := tx.QueryRow(ctx, `
|
|
SELECT peer_type,peer_id FROM message_boxes
|
|
WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted
|
|
FOR UPDATE`, source.Owner.ID, msgID).Scan(&peerType, &peerID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lock star gift message projection: %w", err)
|
|
}
|
|
if peerType != string(domain.PeerTypeUser) || peerID <= 0 {
|
|
return nil, fmt.Errorf("star gift message projection %d is not private", msgID)
|
|
}
|
|
target, err := q.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{
|
|
OwnerUserID: source.Owner.ID, BoxID: int32(msgID), PeerType: peerType, PeerID: peerID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load star gift message projection: %w", err)
|
|
}
|
|
logicalKey := fmt.Sprintf("%d:%d", target.MessageSenderID, target.PrivateMessageID)
|
|
if _, duplicate := seenPrivateMessages[logicalKey]; duplicate {
|
|
continue
|
|
}
|
|
seenPrivateMessages[logicalKey] = struct{}{}
|
|
|
|
boxes, err := q.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
|
|
OwnerUserIds: privateMessageOwnerIDs(source.Owner.ID, peerID),
|
|
MessageSenderID: target.MessageSenderID, PrivateMessageID: target.PrivateMessageID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list visible star gift message projections: %w", err)
|
|
}
|
|
var privateMediaJSON []byte
|
|
matched := false
|
|
for _, box := range boxes {
|
|
media, err := decodeMessageMedia(box.MediaJson)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode star gift message projection: %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 != current.ID {
|
|
continue
|
|
}
|
|
matched = true
|
|
action := media.ServiceAction.StarGiftUnique
|
|
retiredGift := current
|
|
retiredGift.CraftChancePermille = 0
|
|
retiredGift.ResellAmount = nil
|
|
action.Gift = retiredGift
|
|
action.Peer = domain.Peer{}
|
|
action.SavedID = 0
|
|
action.Saved = false
|
|
if validLifecyclePeer(current.Owner) && current.Owner != source.Owner {
|
|
action.Transferred = true
|
|
}
|
|
action.CanExportAt = 0
|
|
action.TransferStars = 0
|
|
action.ResaleAmount = nil
|
|
action.CanTransferAt = 0
|
|
action.CanResellAt = 0
|
|
action.DropOriginalDetailsStars = 0
|
|
action.CanCraftAt = 0
|
|
|
|
mediaJSON, err := encodeMessageMedia(media)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode retired star gift projection: %w", err)
|
|
}
|
|
pts, err := s.messages.reservePts(ctx, tx, box.OwnerUserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("allocate retired star gift 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, fmt.Errorf("update retired star gift projection: %w", err)
|
|
}
|
|
if tag.RowsAffected() != 1 {
|
|
return nil, fmt.Errorf("update retired star gift projection lost row")
|
|
}
|
|
msg, err := messageFromVisibleBoxRow(box)
|
|
if err != nil {
|
|
return nil, 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, err
|
|
}
|
|
event := domain.UpdateEvent{UserID: msg.OwnerUserID, Type: domain.UpdateEventEditMessage,
|
|
Pts: pts, PtsCount: 1, Date: date, Message: msg}
|
|
if err := appendUserUpdateEvent(ctx, tx, q, msg.OwnerUserID, event); err != nil {
|
|
return nil, fmt.Errorf("append retired star gift edit event: %w", err)
|
|
}
|
|
if err := enqueueDispatch(ctx, q, sqlcgen.EnqueueDispatchParams{
|
|
TargetUserID: msg.OwnerUserID, Pts: int32(pts), EventType: string(domain.UpdateEventEditMessage),
|
|
ExcludeAuthKeyID: 0, ExcludeSessionID: 0,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("enqueue retired star gift edit: %w", err)
|
|
}
|
|
if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 {
|
|
privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event})
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
if len(privateMediaJSON) == 0 {
|
|
return nil, fmt.Errorf("retired star gift projection missing shared media")
|
|
}
|
|
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, fmt.Errorf("update retired star gift private media: %w", err)
|
|
}
|
|
}
|
|
return edits, nil
|
|
}
|