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.
This commit is contained in:
parent
f2c2fd0236
commit
14bf7d1e20
92 changed files with 14768 additions and 727 deletions
227
internal/store/postgres/star_gift_craft_projection.go
Normal file
227
internal/store/postgres/star_gift_craft_projection.go
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue