fix(admin): close PR review blockers

Keep bot credentials out of durable command results, fail bot deletion closed when session revocation fails, reject invalid scam/fake states at every write boundary, and make direct collectible grants a single replayable PostgreSQL aggregate.

Also lock admin gift sender/message limits and add regression coverage for rollback, replay, moderation constraints, and credential redaction.
This commit is contained in:
iamxvbaba 2026-07-23 13:29:04 +08:00
parent 90792cdfab
commit 234061ef83
30 changed files with 859 additions and 93 deletions

View file

@ -289,6 +289,9 @@ func (s *ChannelStore) SetChannelScamFake(ctx context.Context, channelID int64,
if channelID == 0 {
return domain.Channel{}, domain.ErrChannelInvalid
}
if scam && fake {
return domain.Channel{}, domain.ErrPeerModerationFlagsInvalid
}
channel, err := s.channelByID(ctx, s.db, channelID)
if err != nil {
return domain.Channel{}, err

View file

@ -119,7 +119,12 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva
type privateSendTxHooks struct {
before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error
projectMedia func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) (privateSendMediaProjection, error)
after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error
// afterAllocate runs after the immutable logical message and both box IDs
// exist, but before either box, update event or replay snapshot is written.
// It may finalize req.Media using those IDs; all of its writes remain in the
// same private-send transaction.
afterAllocate func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest, int, int) error
after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error
}
// privateSendMediaProjection separates the logical private-message payload
@ -325,6 +330,44 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP
return domain.SendPrivateTextResult{}, fmt.Errorf("allocate recipient pts: %w", err)
}
}
if hooks.afterAllocate != nil {
// A callback may replace the media after the ordinary request
// fingerprint was computed. Requiring a complete caller-owned
// fingerprint keeps random_id replay bound to the final aggregate intent.
if err := store.ValidateSendFingerprint(req.IdempotencyFingerprint, "after-allocate private send"); err != nil {
return domain.SendPrivateTextResult{}, err
}
if err := hooks.afterAllocate(ctx, tx, &req, senderBoxID, recipientBoxID); err != nil {
return domain.SendPrivateTextResult{}, err
}
media = privateSendMediaProjection{Shared: req.Media, Sender: req.Media, Recipient: req.Media}
if hooks.projectMedia != nil {
media, err = hooks.projectMedia(ctx, tx, &req)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
}
sharedMediaJSON, err = encodeMessageMedia(media.Shared)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
senderMediaJSON, err = encodeMessageMedia(media.Sender)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
recipientMediaJSON, err = encodeMessageMedia(media.Recipient)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
tag, err := tx.Exec(ctx, `UPDATE private_messages SET media=$3
WHERE sender_user_id=$1 AND id=$2`, req.SenderUserID, pm.ID, sharedMediaJSON)
if err != nil {
return domain.SendPrivateTextResult{}, fmt.Errorf("finalize private message media: %w", err)
}
if tag.RowsAffected() != 1 {
return domain.SendPrivateTextResult{}, fmt.Errorf("finalize private message media: logical message disappeared")
}
}
senderArg := sqlcgen.CreateMessageBoxParams{
OwnerUserID: req.SenderUserID,

View file

@ -0,0 +1,50 @@
package postgres
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
)
func TestModerationFlagsRejectImpossibleStateAtPostgresBoundary(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
user := createTestUser(t, ctx, users, "+1781"+suffix+"71", "ModerationFlags", "")
if _, err := users.SetScamFake(ctx, user.ID, true, true); !errors.Is(err, domain.ErrPeerModerationFlagsInvalid) {
t.Fatalf("user store error=%v", err)
}
if _, err := pool.Exec(ctx, `UPDATE users SET scam=true,fake=true WHERE id=$1`, user.ID); err == nil {
t.Fatal("users CHECK constraint accepted scam=true,fake=true")
}
channels := NewChannelStore(pool)
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: user.ID,
Title: "Moderation " + suffix,
Megagroup: true,
Date: 1700002000,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
if _, err := channels.SetChannelScamFake(ctx, created.Channel.ID, true, true); !errors.Is(err, domain.ErrPeerModerationFlagsInvalid) {
t.Fatalf("channel store error=%v", err)
}
if _, err := pool.Exec(ctx, `UPDATE channels SET scam=true,fake=true WHERE id=$1`, created.Channel.ID); err == nil {
t.Fatal("channels CHECK constraint accepted scam=true,fake=true")
}
gotUser, found, err := users.ByID(ctx, user.ID)
if err != nil || !found || gotUser.Scam || gotUser.Fake {
t.Fatalf("user after rejected writes=%+v found=%v err=%v", gotUser, found, err)
}
gotChannel, err := channels.GetChannelByID(ctx, created.Channel.ID)
if err != nil || gotChannel.Scam || gotChannel.Fake {
t.Fatalf("channel after rejected writes=%+v err=%v", gotChannel, err)
}
}

View file

@ -603,6 +603,125 @@ FROM peer_star_gifts p JOIN unique_star_gifts u ON u.id=p.unique_gift_id WHERE p
}
}
func TestAdminUniqueStarGiftGrantIsAtomicAndReplayable(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
now := int(time.Now().Unix())
recipient := createTestUser(t, ctx, NewUserStore(pool), "+1780"+suffix+"61", "AdminGiftRecipient", "")
gifts := NewStarGiftStore(pool)
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
Title: "Admin Grant " + suffix, Stars: 50, ConvertStars: 25, Enabled: true,
Document: collectibleTestDocument(baseDocumentID, "admin-grant-gift.tgs"),
Blob: collectibleTestBlob(baseDocumentID, "admin-grant-gift"), Animation: collectibleTestAnimation("admin-grant-gift.tgs"),
Actor: "integration", CommandID: "admin-grant-catalog-" + suffix,
})
if err != nil {
t.Fatalf("create admin grant catalog gift: %v", err)
}
revision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 2, SlugPrefix: "admin-grant-" + suffix,
Models: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleModel, Name: "Model One", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+1, "admin-model-one.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "admin-model-one"), Animation: collectibleTestAnimationPtr("admin-model-one.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+2, "admin-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "admin-model-two"), Animation: collectibleTestAnimationPtr("admin-model-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern One", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "admin-pattern-one.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "admin-pattern-one"), Animation: collectibleTestAnimationPtr("admin-pattern-one.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+4, "admin-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "admin-pattern-two"), Animation: collectibleTestAnimationPtr("admin-pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop One", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
},
Actor: "integration", CommandID: "admin-grant-pool-" + suffix,
})
if err != nil {
t.Fatalf("publish admin grant collectible pool: %v", err)
}
upgrades := NewStarGiftUpgradeStore(pool, NewMessageStore(pool))
invalid := domain.AdminStarGiftGrant{
SenderID: domain.OfficialSystemUserID, Recipient: domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID},
GiftID: entry.Gift.ID, Upgrade: true, CommandKey: "admin-invalid-" + suffix, Date: now,
ModelAttributeID: revision.Models[0].ID + 9_999_999,
}
if _, err := upgrades.GrantUniqueStarGift(ctx, invalid); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) {
t.Fatalf("invalid admin grant error=%v", err)
}
var issued, messageCount, savedCount, uniqueCount, commandCount int
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, revision.ID).Scan(&issued); err != nil {
t.Fatal(err)
}
invalidRandomID := lifecycleCommandRandomID("admin-collectible-grant", recipient.ID, invalid.CommandKey)
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM private_messages WHERE sender_user_id=$1 AND random_id=$2`,
domain.OfficialSystemUserID, invalidRandomID).Scan(&messageCount); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM peer_star_gifts WHERE owner_peer_type='user' AND owner_peer_id=$1 AND gift_id=$2`,
recipient.ID, entry.Gift.ID).Scan(&savedCount); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM unique_star_gifts WHERE gift_id=$1`, entry.Gift.ID).Scan(&uniqueCount); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_admin_grant_commands WHERE recipient_user_id=$1`,
recipient.ID).Scan(&commandCount); err != nil {
t.Fatal(err)
}
if issued != 0 || messageCount != 0 || savedCount != 0 || uniqueCount != 0 || commandCount != 0 {
t.Fatalf("failed grant leaked state: issued=%d messages=%d saved=%d unique=%d commands=%d",
issued, messageCount, savedCount, uniqueCount, commandCount)
}
req := invalid
req.CommandKey = "admin-success-" + suffix
req.Message = "atomic collectible"
req.ModelAttributeID = revision.Models[0].ID
req.PatternAttributeID = revision.Patterns[0].ID
req.BackdropAttributeID = revision.Backdrops[0].ID
granted, err := upgrades.GrantUniqueStarGift(ctx, req)
if err != nil {
t.Fatalf("grant admin unique gift: %v", err)
}
action := granted.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique
if granted.Duplicate || granted.Saved.MsgID <= 0 || granted.Saved.MsgID != granted.Saved.UpgradeMsgID ||
granted.Saved.UniqueGiftID != granted.Unique.ID || granted.Unique.Num != 1 ||
action == nil || !action.Assigned || !action.Saved || action.Gift.ID != granted.Unique.ID {
t.Fatalf("admin grant result=%+v action=%+v", granted, action)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, granted.Send.RecipientMessage.Pts-1, 1)
if err != nil || len(events) != 1 || events[0].Message.Media == nil ||
events[0].Message.Media.ServiceAction == nil ||
events[0].Message.Media.ServiceAction.StarGiftUnique == nil ||
events[0].Message.Media.ServiceAction.StarGiftUnique.Gift.ID != granted.Unique.ID {
t.Fatalf("admin grant durable update=%+v err=%v", events, err)
}
replay, err := upgrades.GrantUniqueStarGift(ctx, req)
if err != nil {
t.Fatalf("replay admin unique gift: %v", err)
}
if !replay.Duplicate || replay.Saved.ID != granted.Saved.ID || replay.Unique.ID != granted.Unique.ID ||
replay.Send.RecipientMessage.ID != granted.Send.RecipientMessage.ID {
t.Fatalf("admin grant replay=%+v want saved=%d unique=%d msg=%d",
replay, granted.Saved.ID, granted.Unique.ID, granted.Send.RecipientMessage.ID)
}
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, revision.ID).Scan(&issued); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_admin_grant_commands WHERE recipient_user_id=$1`,
recipient.ID).Scan(&commandCount); err != nil {
t.Fatal(err)
}
if issued != 1 || commandCount != 1 {
t.Fatalf("replay duplicated aggregate: issued=%d commands=%d", issued, commandCount)
}
}
func collectibleTestAnimation(name string) domain.StarGiftAnimation {
return domain.StarGiftAnimation{
SourceName: name, SourceFormat: domain.StarGiftAnimationTGS,

View file

@ -1,6 +1,7 @@
package postgres
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
@ -46,6 +47,296 @@ func NewStarGiftUpgradeStore(db sqlcgen.DBTX, messages *MessageStore, opts ...St
return s
}
// GrantUniqueStarGift atomically assigns a newly minted collectible from the
// official system account. The saved gift, unique issuance, service message,
// pts/outbox and immutable command receipt share MessageStore's transaction.
func (s *StarGiftUpgradeStore) GrantUniqueStarGift(ctx context.Context, req domain.AdminStarGiftGrant) (domain.AdminStarGiftGrantResult, error) {
req.CommandKey = strings.TrimSpace(req.CommandKey)
req.Message = strings.TrimSpace(req.Message)
if s == nil || s.db == nil || s.messages == nil || req.SenderID != domain.OfficialSystemUserID ||
req.Recipient.Type != domain.PeerTypeUser || req.Recipient.ID <= 0 || req.GiftID <= 0 || !req.Upgrade ||
req.CommandKey == "" || len(req.CommandKey) > 256 || req.Date <= 0 || len([]rune(req.Message)) > 128 ||
req.ModelAttributeID < 0 || req.PatternAttributeID < 0 || req.BackdropAttributeID < 0 {
return domain.AdminStarGiftGrantResult{}, domain.ErrStarGiftCollectibleInvalid
}
fingerprint := adminStarGiftGrantFingerprint(req)
if replay, found, err := s.loadAdminStarGiftGrantReplay(ctx, req, fingerprint, domain.SendPrivateTextResult{}); err != nil || found {
return replay, err
}
placeholder := &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
Kind: domain.MessageServiceActionStarGiftUnique,
StarGiftUnique: &domain.MessageStarGiftUniqueAction{
Assigned: true,
Saved: true,
},
}}
messageReq := domain.SendPrivateTextRequest{
SenderUserID: req.SenderID,
RecipientUserID: req.Recipient.ID,
RandomID: lifecycleCommandRandomID("admin-collectible-grant", req.Recipient.ID, req.CommandKey),
Date: req.Date,
OriginUserID: req.SenderID,
RecipientBlocked: req.RecipientBlocked,
IdempotencyFingerprint: fingerprint[:],
Media: placeholder,
}
var result domain.AdminStarGiftGrantResult
hooks := privateSendTxHooks{
afterAllocate: func(ctx context.Context, tx pgx.Tx, send *domain.SendPrivateTextRequest, senderBoxID, recipientBoxID int) error {
ownerMessageID := recipientBoxID
if req.SenderID == req.Recipient.ID {
ownerMessageID = senderBoxID
}
if ownerMessageID <= 0 {
return fmt.Errorf("admin collectible grant missing owner message id")
}
var revisionID int64
var enabled bool
if err := tx.QueryRow(ctx, `SELECT active_revision_id,enabled
FROM star_gift_catalog WHERE gift_id=$1 FOR UPDATE`, req.GiftID).Scan(&revisionID, &enabled); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.ErrStarGiftNotFound
}
return fmt.Errorf("lock admin collectible catalog gift: %w", err)
}
gift, found, err := NewStarGiftStore(tx).CatalogRevision(ctx, revisionID)
if err != nil {
return err
}
if !found || !enabled || gift.ID != req.GiftID {
return domain.ErrStarGiftNotFound
}
revision, err := lockActiveCollectibleRevision(ctx, tx, gift.ID)
if err != nil {
return err
}
if revision.Issued >= revision.SupplyTotal {
return domain.ErrStarGiftCollectibleSoldOut
}
modelID, err := resolveCollectibleAttribute(ctx, tx, "star_gift_collectible_models", revision.ID, req.ModelAttributeID)
if err != nil {
return err
}
patternID, err := resolveCollectibleAttribute(ctx, tx, "star_gift_collectible_patterns", revision.ID, req.PatternAttributeID)
if err != nil {
return err
}
backdropID, err := resolveCollectibleAttribute(ctx, tx, "star_gift_collectible_backdrops", revision.ID, req.BackdropAttributeID)
if err != nil {
return err
}
var craftable bool
if err := tx.QueryRow(ctx, `SELECT EXISTS (
SELECT 1 FROM star_gift_collectible_models
WHERE collectible_revision_id=$1 AND crafted
)`, revision.ID).Scan(&craftable); err != nil {
return fmt.Errorf("load admin collectible craft capability: %w", err)
}
craftChancePermille, canCraftAt := 0, 0
if craftable {
craftChancePermille = s.lifecycle.CraftChancePermille
if craftChancePermille > 0 {
canCraftAt = starGiftCraftReadyAt(req.Date, s.lifecycle.CraftDelaySeconds)
}
}
saved := domain.SavedStarGift{
Owner: req.Recipient,
FromUserID: req.SenderID,
GiftID: gift.ID,
RevisionID: gift.RevisionID,
MsgID: ownerMessageID,
Date: req.Date,
NameHidden: req.HideName,
LifecycleStatus: domain.StarGiftLifecycleActive,
Message: req.Message,
TransferStars: s.lifecycle.TransferStars,
CanExportAt: starGiftReadyAt(req.Date, s.lifecycle.ExportDelaySeconds),
CanTransferAt: starGiftReadyAt(req.Date, s.lifecycle.TransferDelaySeconds),
CanResellAt: starGiftReadyAt(req.Date, s.lifecycle.ResellDelaySeconds),
DropOriginalDetailsStars: s.lifecycle.DropOriginalDetailsStars,
CanCraftAt: canCraftAt,
UpgradeMsgID: ownerMessageID,
}
savedID, err := NewStarGiftStore(tx).Create(ctx, saved)
if err != nil {
return err
}
saved.ID = savedID
num := revision.Issued + 1
var uniqueID int64
if err := tx.QueryRow(ctx, `SELECT nextval('unique_star_gift_id_seq')`).Scan(&uniqueID); err != nil {
return fmt.Errorf("allocate admin unique star gift id: %w", err)
}
slug := fmt.Sprintf("%s-%d", revision.SlugPrefix, num)
if _, err := tx.Exec(ctx, `
INSERT INTO unique_star_gifts
(id, gift_id, collectible_revision_id, source_saved_gift_id, title, slug, num,
owner_peer_type, owner_peer_id, model_attribute_id, pattern_attribute_id,
backdrop_attribute_id, keep_original_details, original_owner_peer_type, original_owner_peer_id,
craft_chance_permille, offer_min_stars)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,true,$13,$14,$15,$16)`,
uniqueID, gift.ID, revision.ID, savedID, gift.Title, slug, num,
string(req.Recipient.Type), req.Recipient.ID, modelID, patternID, backdropID,
string(req.Recipient.Type), req.Recipient.ID, craftChancePermille, s.lifecycle.OfferMinStars); err != nil {
return fmt.Errorf("insert admin unique star gift: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE star_gift_collectible_revisions SET issued=issued+1 WHERE id=$1`, revision.ID); err != nil {
return fmt.Errorf("increment admin collectible issuance: %w", err)
}
tag, err := tx.Exec(ctx, `
UPDATE peer_star_gifts
SET unique_gift_id=$2,upgrade_msg_id=$3,convert_stars=0,prepaid_upgrade_stars=0,prepaid_upgrade_hash='',
transfer_stars=$4,can_export_at=$5,can_transfer_at=$6,can_resell_at=$7,
drop_original_details_stars=$8,can_craft_at=$9
WHERE id=$1 AND unique_gift_id IS NULL AND lifecycle_status='active'`,
savedID, uniqueID, ownerMessageID, s.lifecycle.TransferStars, saved.CanExportAt,
saved.CanTransferAt, saved.CanResellAt, s.lifecycle.DropOriginalDetailsStars, canCraftAt)
if err != nil {
return fmt.Errorf("link admin unique star gift: %w", err)
}
if tag.RowsAffected() != 1 {
return fmt.Errorf("link admin unique star gift lost aggregate row")
}
if _, err := tx.Exec(ctx, `
INSERT INTO star_gift_admin_grant_commands
(recipient_user_id,command_key,request_fingerprint,sender_user_id,gift_id,saved_gift_id,unique_gift_id,created_at)
VALUES($1,$2,$3,$4,$5,$6,$7,to_timestamp($8))`,
req.Recipient.ID, req.CommandKey, fingerprint[:], req.SenderID, gift.ID, savedID, uniqueID, req.Date); err != nil {
return fmt.Errorf("insert admin collectible grant command: %w", err)
}
unique, found, err := NewStarGiftStore(tx).UniqueByID(ctx, uniqueID)
if err != nil {
return err
}
if !found {
return fmt.Errorf("new admin unique star gift %d disappeared", uniqueID)
}
saved.UniqueGiftID = uniqueID
saved.Unique = &unique
if err := registerUserStarGiftMessageRef(ctx, tx, req.Recipient.ID, ownerMessageID, savedID, uniqueID); err != nil {
return err
}
result.Saved, result.Unique = saved, unique
send.Media = adminStarGiftUniqueMedia(saved, unique)
return nil
},
}
sent, err := s.messages.sendPrivateTextWithHooks(ctx, messageReq, hooks)
if err != nil {
if isUniqueViolation(err) {
if replay, found, replayErr := s.loadAdminStarGiftGrantReplay(ctx, req, fingerprint, sent); replayErr != nil || found {
return replay, replayErr
}
}
return domain.AdminStarGiftGrantResult{}, err
}
result.Send, result.Duplicate = sent, sent.Duplicate
if sent.Duplicate {
replay, found, replayErr := s.loadAdminStarGiftGrantReplay(ctx, req, fingerprint, sent)
if replayErr != nil {
return domain.AdminStarGiftGrantResult{}, replayErr
}
if !found {
return domain.AdminStarGiftGrantResult{}, domain.ErrStarGiftCollectibleInvalid
}
return replay, nil
}
return result, nil
}
func adminStarGiftUniqueMedia(saved domain.SavedStarGift, unique domain.UniqueStarGift) *domain.MessageMedia {
fromUserID := saved.FromUserID
if saved.NameHidden {
fromUserID = 0
}
return &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
Kind: domain.MessageServiceActionStarGiftUnique,
StarGiftUnique: &domain.MessageStarGiftUniqueAction{
Gift: unique, FromUserID: fromUserID, Assigned: true, Saved: true,
CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars,
CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt,
DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt,
},
}}
}
func adminStarGiftGrantFingerprint(req domain.AdminStarGiftGrant) [32]byte {
return sha256.Sum256([]byte(fmt.Sprintf(
"telesrv:admin-star-gift-grant:v1:%d:%s:%d:%d:%t:%q:%d:%d:%d",
req.SenderID, req.Recipient.Type, req.Recipient.ID, req.GiftID, req.HideName, req.Message,
req.ModelAttributeID, req.PatternAttributeID, req.BackdropAttributeID,
)))
}
func (s *StarGiftUpgradeStore) loadAdminStarGiftGrantReplay(
ctx context.Context,
req domain.AdminStarGiftGrant,
fingerprint [32]byte,
sent domain.SendPrivateTextResult,
) (domain.AdminStarGiftGrantResult, bool, error) {
var storedFingerprint []byte
var senderID, giftID, savedID, uniqueID int64
err := s.db.QueryRow(ctx, `
SELECT request_fingerprint,sender_user_id,gift_id,saved_gift_id,unique_gift_id
FROM star_gift_admin_grant_commands
WHERE recipient_user_id=$1 AND command_key=$2`, req.Recipient.ID, req.CommandKey).Scan(
&storedFingerprint, &senderID, &giftID, &savedID, &uniqueID)
if errors.Is(err, pgx.ErrNoRows) {
return domain.AdminStarGiftGrantResult{}, false, nil
}
if err != nil {
return domain.AdminStarGiftGrantResult{}, false, err
}
if senderID != req.SenderID || giftID != req.GiftID || !bytes.Equal(storedFingerprint, fingerprint[:]) {
return domain.AdminStarGiftGrantResult{}, false, domain.ErrStarGiftCollectibleInvalid
}
saved, found, err := savedStarGiftByID(ctx, s.db, savedID)
if err != nil || !found {
if err == nil {
err = domain.ErrStarGiftCollectibleInvalid
}
return domain.AdminStarGiftGrantResult{}, false, err
}
unique, found, err := NewStarGiftStore(s.db).UniqueByID(ctx, uniqueID)
if err != nil || !found {
if err == nil {
err = domain.ErrStarGiftCollectibleInvalid
}
return domain.AdminStarGiftGrantResult{}, false, err
}
if saved.Owner != req.Recipient || saved.FromUserID != req.SenderID || saved.GiftID != req.GiftID ||
saved.UniqueGiftID != uniqueID || saved.MsgID <= 0 || saved.UpgradeMsgID != saved.MsgID ||
unique.SourceSavedGiftID != savedID || unique.Owner != req.Recipient {
return domain.AdminStarGiftGrantResult{}, false, domain.ErrStarGiftCollectibleInvalid
}
if sent.SenderMessage.ID == 0 {
replay, replayFound, replayErr := s.messages.LookupPrivateSendReplay(ctx, domain.PrivateSendReplayRequest{
SenderUserID: req.SenderID, RecipientUserID: req.Recipient.ID,
RandomID: lifecycleCommandRandomID("admin-collectible-grant", req.Recipient.ID, req.CommandKey),
IdempotencyFingerprint: fingerprint[:],
})
if replayErr != nil {
return domain.AdminStarGiftGrantResult{}, false, replayErr
}
if !replayFound {
return domain.AdminStarGiftGrantResult{}, false, domain.ErrStarGiftCollectibleInvalid
}
sent = replay
}
uniqueCopy := unique
saved.Unique = &uniqueCopy
return domain.AdminStarGiftGrantResult{
Saved: saved, Unique: unique, Send: sent, Duplicate: true,
}, true, nil
}
func (s *StarGiftUpgradeStore) UpgradeStarGift(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) {
if s == nil || s.db == nil || s.messages == nil || req.UserID <= 0 || !req.Ref.Valid() ||
(req.Ref.Owner.Type == domain.PeerTypeUser && req.Ref.Owner.ID != req.UserID) ||

View file

@ -338,6 +338,9 @@ func (s *UserStore) SetSupport(ctx context.Context, userID int64, support bool)
// SetScamFake 设置/取消用户的 scam 与 fake 标记bot 复用同一路径)。
func (s *UserStore) SetScamFake(ctx context.Context, userID int64, scam, fake bool) (domain.User, error) {
if scam && fake {
return domain.User{}, domain.ErrPeerModerationFlagsInvalid
}
row, err := s.q.SetUserScamFake(ctx, sqlcgen.SetUserScamFakeParams{
ID: userID,
Scam: scam,