951 lines
38 KiB
Go
951 lines
38 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store/postgres/sqlcgen"
|
||
)
|
||
|
||
// StarGiftStore 用 PostgreSQL 实现 store.StarGiftStore(peer 收到的 Star 礼物实例)。
|
||
type StarGiftStore struct {
|
||
db sqlcgen.DBTX
|
||
}
|
||
|
||
// NewStarGiftStore 基于 pgx 连接池(或事务)创建 StarGiftStore。
|
||
func NewStarGiftStore(db sqlcgen.DBTX) *StarGiftStore {
|
||
return &StarGiftStore{db: db}
|
||
}
|
||
|
||
const starGiftCatalogSelect = `
|
||
SELECT c.gift_id, r.id, r.stars, r.convert_stars, r.title,
|
||
r.limited, r.sold_out, r.birthday, r.require_premium,
|
||
r.limited_per_user, r.peer_color_available, r.auction,
|
||
c.availability_remains, r.availability_total, c.availability_resale,
|
||
c.first_sale_date, c.last_sale_date, c.resell_min_stars,
|
||
COALESCE(r.released_by_peer_type, ''), COALESCE(r.released_by_peer_id, 0),
|
||
r.per_user_total, r.locked_until_date, r.auction_slug, r.gifts_per_round,
|
||
r.auction_start_date, r.upgrade_variants,
|
||
r.background_center_color IS NOT NULL,
|
||
COALESCE(r.background_center_color, 0), COALESCE(r.background_edge_color, 0),
|
||
COALESCE(r.background_text_color, 0),
|
||
COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0),
|
||
d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id,
|
||
d.attributes::text, d.thumbs::text
|
||
FROM star_gift_catalog c
|
||
JOIN star_gift_catalog_revisions r ON r.id = c.active_revision_id
|
||
LEFT JOIN star_gift_collectible_revisions cr ON cr.id = c.collectible_revision_id AND cr.status = 'published'
|
||
JOIN documents d ON d.id = r.document_id`
|
||
|
||
func (s *StarGiftStore) Catalog(ctx context.Context) ([]domain.StarGift, error) {
|
||
rows, err := s.db.Query(ctx, starGiftCatalogSelect+`
|
||
WHERE c.enabled
|
||
ORDER BY c.sort_order, c.gift_id`)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list star gift catalog: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
out := make([]domain.StarGift, 0)
|
||
for rows.Next() {
|
||
gift, err := scanCatalogGift(rows)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, gift)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("iterate star gift catalog: %w", err)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) CatalogGift(ctx context.Context, giftID int64) (domain.StarGift, bool, error) {
|
||
if giftID <= 0 {
|
||
return domain.StarGift{}, false, nil
|
||
}
|
||
gift, err := scanCatalogGift(s.db.QueryRow(ctx, starGiftCatalogSelect+`
|
||
WHERE c.enabled AND c.gift_id = $1`, giftID))
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.StarGift{}, false, nil
|
||
}
|
||
if err != nil {
|
||
return domain.StarGift{}, false, err
|
||
}
|
||
return gift, true, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) CatalogRevision(ctx context.Context, revisionID int64) (domain.StarGift, bool, error) {
|
||
if revisionID <= 0 {
|
||
return domain.StarGift{}, false, nil
|
||
}
|
||
gift, err := scanCatalogGift(s.db.QueryRow(ctx, `
|
||
SELECT r.gift_id, r.id, r.stars, r.convert_stars, r.title,
|
||
r.limited, r.sold_out, r.birthday, r.require_premium,
|
||
r.limited_per_user, r.peer_color_available, r.auction,
|
||
c.availability_remains, r.availability_total, c.availability_resale,
|
||
c.first_sale_date, c.last_sale_date, c.resell_min_stars,
|
||
COALESCE(r.released_by_peer_type, ''), COALESCE(r.released_by_peer_id, 0),
|
||
r.per_user_total, r.locked_until_date, r.auction_slug, r.gifts_per_round,
|
||
r.auction_start_date, r.upgrade_variants,
|
||
r.background_center_color IS NOT NULL,
|
||
COALESCE(r.background_center_color, 0), COALESCE(r.background_edge_color, 0),
|
||
COALESCE(r.background_text_color, 0),
|
||
COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0),
|
||
d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id,
|
||
d.attributes::text, d.thumbs::text
|
||
FROM star_gift_catalog_revisions r
|
||
JOIN star_gift_catalog c ON c.gift_id = r.gift_id
|
||
LEFT JOIN star_gift_collectible_revisions cr ON cr.id = c.collectible_revision_id AND cr.status = 'published'
|
||
JOIN documents d ON d.id = r.document_id
|
||
WHERE r.id = $1`, revisionID))
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.StarGift{}, false, nil
|
||
}
|
||
if err != nil {
|
||
return domain.StarGift{}, false, err
|
||
}
|
||
return gift, true, nil
|
||
}
|
||
|
||
func scanCatalogGift(row rowScanner) (domain.StarGift, error) {
|
||
var gift domain.StarGift
|
||
var attrsJSON, thumbsJSON string
|
||
var releasedByType string
|
||
var releasedByID int64
|
||
var hasBackground bool
|
||
var background domain.StarGiftBackground
|
||
if err := row.Scan(
|
||
&gift.ID, &gift.RevisionID, &gift.Stars, &gift.ConvertStars, &gift.Title,
|
||
&gift.Limited, &gift.SoldOut, &gift.Birthday, &gift.RequirePremium,
|
||
&gift.LimitedPerUser, &gift.PeerColorAvailable, &gift.Auction,
|
||
&gift.AvailabilityRemains, &gift.AvailabilityTotal, &gift.AvailabilityResale,
|
||
&gift.FirstSaleDate, &gift.LastSaleDate, &gift.ResellMinStars,
|
||
&releasedByType, &releasedByID, &gift.PerUserTotal, &gift.LockedUntilDate,
|
||
&gift.AuctionSlug, &gift.GiftsPerRound, &gift.AuctionStartDate, &gift.UpgradeVariants,
|
||
&hasBackground, &background.CenterColor, &background.EdgeColor, &background.TextColor,
|
||
&gift.UpgradeStars, &gift.UpgradeTotal, &gift.UpgradeIssued,
|
||
&gift.Sticker.ID, &gift.Sticker.AccessHash, &gift.Sticker.FileReference, &gift.Sticker.Date,
|
||
&gift.Sticker.MimeType, &gift.Sticker.Size, &gift.Sticker.DCID, &attrsJSON, &thumbsJSON,
|
||
); err != nil {
|
||
return domain.StarGift{}, err
|
||
}
|
||
if releasedByType != "" && releasedByID > 0 {
|
||
gift.ReleasedBy = domain.Peer{Type: domain.PeerType(releasedByType), ID: releasedByID}
|
||
}
|
||
if hasBackground {
|
||
gift.Background = &background
|
||
}
|
||
if gift.LimitedPerUser {
|
||
gift.PerUserRemains = gift.PerUserTotal
|
||
}
|
||
attrs, err := decodeDocumentAttributes(attrsJSON)
|
||
if err != nil {
|
||
return domain.StarGift{}, fmt.Errorf("decode star gift document attributes: %w", err)
|
||
}
|
||
thumbs, err := decodePhotoSizes(thumbsJSON)
|
||
if err != nil {
|
||
return domain.StarGift{}, fmt.Errorf("decode star gift document thumbs: %w", err)
|
||
}
|
||
gift.Sticker.Attributes = attrs
|
||
gift.Sticker.Thumbs = thumbs
|
||
if !gift.Sticker.IsSticker() || gift.Sticker.MimeType != "application/x-tgsticker" {
|
||
return domain.StarGift{}, fmt.Errorf("invalid star gift revision %d document %d", gift.RevisionID, gift.Sticker.ID)
|
||
}
|
||
return gift, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) {
|
||
if write.Stars <= 0 || write.ConvertStars < 0 || write.ConvertStars > write.Stars ||
|
||
write.Document.ID <= 0 || !write.Document.IsSticker() || write.Document.MimeType != "application/x-tgsticker" ||
|
||
len(write.Animation.JSON) == 0 || len(write.Animation.SHA256) != 32 {
|
||
return domain.StarGiftCatalogEntry{}, domain.ErrStarGiftInvalid
|
||
}
|
||
var entry domain.StarGiftCatalogEntry
|
||
err := withTx(ctx, s.db, "create star gift catalog revision", func(tx pgx.Tx) error {
|
||
giftID := write.GiftID
|
||
var revisionID int64
|
||
if err := tx.QueryRow(ctx, `SELECT nextval('star_gift_catalog_revision_id_seq')`).Scan(&revisionID); err != nil {
|
||
return fmt.Errorf("allocate star gift revision id: %w", err)
|
||
}
|
||
revision := 1
|
||
if giftID == 0 {
|
||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended('star_gift_catalog', 0))`); err != nil {
|
||
return fmt.Errorf("lock star gift catalog capacity: %w", err)
|
||
}
|
||
var catalogCount int
|
||
if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_catalog`).Scan(&catalogCount); err != nil {
|
||
return fmt.Errorf("count star gift catalog: %w", err)
|
||
}
|
||
if catalogCount >= domain.MaxStarGiftCatalogSize {
|
||
return domain.ErrStarGiftCatalogFull
|
||
}
|
||
if err := tx.QueryRow(ctx, `SELECT nextval('star_gift_catalog_gift_id_seq')`).Scan(&giftID); err != nil {
|
||
return fmt.Errorf("allocate star gift id: %w", err)
|
||
}
|
||
if _, err := tx.Exec(ctx, `
|
||
INSERT INTO star_gift_catalog (
|
||
gift_id, active_revision_id, enabled, sort_order, availability_remains,
|
||
availability_resale, resell_min_stars, first_sale_date, last_sale_date
|
||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, giftID, revisionID, write.Enabled, write.SortOrder,
|
||
write.AvailabilityRemains, write.AvailabilityResale, write.ResellMinStars,
|
||
write.FirstSaleDate, write.LastSaleDate); err != nil {
|
||
return fmt.Errorf("insert star gift catalog: %w", err)
|
||
}
|
||
} else {
|
||
var ignored int64
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT active_revision_id FROM star_gift_catalog WHERE gift_id=$1 FOR UPDATE`, giftID).Scan(&ignored); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.ErrStarGiftNotFound
|
||
}
|
||
return fmt.Errorf("lock star gift catalog: %w", err)
|
||
}
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT COALESCE(MAX(revision), 0) + 1
|
||
FROM star_gift_catalog_revisions
|
||
WHERE gift_id = $1`, giftID).Scan(&revision); err != nil {
|
||
return fmt.Errorf("lock star gift catalog: %w", err)
|
||
}
|
||
}
|
||
|
||
media := NewMediaStore(tx)
|
||
if err := media.PutDocument(ctx, write.Document); err != nil {
|
||
return fmt.Errorf("put star gift document: %w", err)
|
||
}
|
||
if err := media.PutFileBlob(ctx, write.Blob); err != nil {
|
||
return fmt.Errorf("put star gift blob: %w", err)
|
||
}
|
||
if _, err := tx.Exec(ctx, `
|
||
INSERT INTO star_gift_catalog_revisions (
|
||
id, gift_id, revision, title, stars, convert_stars, document_id,
|
||
animation_json, animation_sha256, source_name, source_format,
|
||
width, height, frame_rate, in_point, out_point, created_by, command_id,
|
||
official_gift_id, source_manifest_sha256, official_source,
|
||
limited, sold_out, birthday, require_premium, limited_per_user,
|
||
peer_color_available, auction, availability_total,
|
||
released_by_peer_type, released_by_peer_id, per_user_total, locked_until_date,
|
||
auction_slug, gifts_per_round, auction_start_date, upgrade_variants,
|
||
background_center_color, background_edge_color, background_text_color
|
||
) VALUES (
|
||
$1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,
|
||
NULLIF($19::bigint,0),$20,$21::jsonb,$22,$23,$24,$25,$26,$27,$28,$29,
|
||
$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40
|
||
)`,
|
||
revisionID, giftID, revision, write.Title, write.Stars, write.ConvertStars, write.Document.ID,
|
||
string(write.Animation.JSON), write.Animation.SHA256, write.Animation.SourceName, string(write.Animation.SourceFormat),
|
||
write.Animation.Width, write.Animation.Height, write.Animation.FrameRate, write.Animation.InPoint, write.Animation.OutPoint,
|
||
write.Actor, write.CommandID, write.OfficialGiftID, nullableSHA256(write.SourceManifestSHA256), nullableOfficialGiftJSON(write.OfficialSourceJSON),
|
||
write.Limited, write.SoldOut, write.Birthday, write.RequirePremium, write.LimitedPerUser,
|
||
write.PeerColorAvailable, write.Auction, write.AvailabilityTotal,
|
||
nullableStarGiftPeerType(write.ReleasedBy), nullableStarGiftPeerID(write.ReleasedBy), write.PerUserTotal,
|
||
write.LockedUntilDate, write.AuctionSlug, write.GiftsPerRound, write.AuctionStartDate,
|
||
write.UpgradeVariants, nullableBackgroundColor(write.Background, "center"),
|
||
nullableBackgroundColor(write.Background, "edge"), nullableBackgroundColor(write.Background, "text"),
|
||
); err != nil {
|
||
return fmt.Errorf("insert star gift revision: %w", err)
|
||
}
|
||
if write.GiftID != 0 {
|
||
if _, err := tx.Exec(ctx, `
|
||
UPDATE star_gift_catalog
|
||
SET active_revision_id=$2, enabled=$3, sort_order=$4, availability_remains=$5,
|
||
availability_resale=$6, resell_min_stars=$7, first_sale_date=$8, last_sale_date=$9, updated_at=now()
|
||
WHERE gift_id=$1`, giftID, revisionID, write.Enabled, write.SortOrder, write.AvailabilityRemains,
|
||
write.AvailabilityResale, write.ResellMinStars, write.FirstSaleDate, write.LastSaleDate); err != nil {
|
||
return fmt.Errorf("activate star gift revision: %w", err)
|
||
}
|
||
}
|
||
write.GiftID = giftID
|
||
var err error
|
||
entry, err = catalogEntryByID(ctx, tx, giftID)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return domain.StarGiftCatalogEntry{}, err
|
||
}
|
||
return entry, nil
|
||
}
|
||
|
||
func nullableStarGiftPeerType(peer domain.Peer) any {
|
||
if peer.ID <= 0 || (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) {
|
||
return nil
|
||
}
|
||
return string(peer.Type)
|
||
}
|
||
|
||
func nullableStarGiftPeerID(peer domain.Peer) any {
|
||
if nullableStarGiftPeerType(peer) == nil {
|
||
return nil
|
||
}
|
||
return peer.ID
|
||
}
|
||
|
||
func nullableBackgroundColor(background *domain.StarGiftBackground, component string) any {
|
||
if background == nil {
|
||
return nil
|
||
}
|
||
switch component {
|
||
case "center":
|
||
return background.CenterColor
|
||
case "edge":
|
||
return background.EdgeColor
|
||
default:
|
||
return background.TextColor
|
||
}
|
||
}
|
||
|
||
func (s *StarGiftStore) CreateCatalogBundle(ctx context.Context, write domain.StarGiftCatalogBundleWrite) (domain.StarGiftCatalogBundleResult, error) {
|
||
var result domain.StarGiftCatalogBundleResult
|
||
err := withTx(ctx, s.db, "create star gift catalog bundle", func(tx pgx.Tx) error {
|
||
nested := NewStarGiftStore(tx)
|
||
entry, err := nested.CreateCatalogRevision(ctx, write.Catalog)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result.Catalog = entry
|
||
if write.Collectible != nil {
|
||
collectibleWrite := *write.Collectible
|
||
collectibleWrite.GiftID = entry.Gift.ID
|
||
revision, err := nested.PublishCollectibleRevision(ctx, collectibleWrite)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result.Collectible = &revision
|
||
entry, err = catalogEntryByID(ctx, tx, entry.Gift.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
result.Catalog = entry
|
||
}
|
||
return nil
|
||
})
|
||
return result, err
|
||
}
|
||
|
||
func (s *StarGiftStore) SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error) {
|
||
tag, err := s.db.Exec(ctx, `
|
||
UPDATE star_gift_catalog SET enabled=$2, updated_at=now()
|
||
WHERE gift_id=$1 AND enabled IS DISTINCT FROM $2`, giftID, enabled)
|
||
if err != nil {
|
||
return false, fmt.Errorf("set star gift enabled: %w", err)
|
||
}
|
||
if tag.RowsAffected() > 0 {
|
||
return true, nil
|
||
}
|
||
var exists bool
|
||
if err := s.db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM star_gift_catalog WHERE gift_id=$1)`, giftID).Scan(&exists); err != nil {
|
||
return false, fmt.Errorf("check star gift enabled target: %w", err)
|
||
}
|
||
if !exists {
|
||
return false, domain.ErrStarGiftNotFound
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) SetCatalogSortOrder(ctx context.Context, giftID int64, sortOrder int) (bool, error) {
|
||
tag, err := s.db.Exec(ctx, `
|
||
UPDATE star_gift_catalog SET sort_order=$2, updated_at=now()
|
||
WHERE gift_id=$1 AND sort_order IS DISTINCT FROM $2`, giftID, sortOrder)
|
||
if err != nil {
|
||
return false, fmt.Errorf("set star gift sort order: %w", err)
|
||
}
|
||
if tag.RowsAffected() > 0 {
|
||
return true, nil
|
||
}
|
||
var exists bool
|
||
if err := s.db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM star_gift_catalog WHERE gift_id=$1)`, giftID).Scan(&exists); err != nil {
|
||
return false, fmt.Errorf("check star gift sort target: %w", err)
|
||
}
|
||
if !exists {
|
||
return false, domain.ErrStarGiftNotFound
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) AnimationJSON(ctx context.Context, giftID int64) ([]byte, bool, error) {
|
||
var raw []byte
|
||
err := s.db.QueryRow(ctx, `
|
||
SELECT r.animation_json::text
|
||
FROM star_gift_catalog c
|
||
JOIN star_gift_catalog_revisions r ON r.id = c.active_revision_id
|
||
WHERE c.gift_id=$1`, giftID).Scan(&raw)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, false, nil
|
||
}
|
||
if err != nil {
|
||
return nil, false, fmt.Errorf("get star gift animation: %w", err)
|
||
}
|
||
return raw, true, nil
|
||
}
|
||
|
||
func catalogEntryByID(ctx context.Context, db sqlcgen.DBTX, giftID int64) (domain.StarGiftCatalogEntry, error) {
|
||
row := db.QueryRow(ctx, `
|
||
SELECT c.gift_id, r.id, r.stars, r.convert_stars, r.title,
|
||
r.limited, r.sold_out, r.birthday, r.require_premium,
|
||
r.limited_per_user, r.peer_color_available, r.auction,
|
||
c.availability_remains, r.availability_total, c.availability_resale,
|
||
c.first_sale_date, c.last_sale_date, c.resell_min_stars,
|
||
COALESCE(r.released_by_peer_type, ''), COALESCE(r.released_by_peer_id, 0),
|
||
r.per_user_total, r.locked_until_date, r.auction_slug, r.gifts_per_round,
|
||
r.auction_start_date, r.upgrade_variants,
|
||
r.background_center_color IS NOT NULL,
|
||
COALESCE(r.background_center_color, 0), COALESCE(r.background_edge_color, 0),
|
||
COALESCE(r.background_text_color, 0),
|
||
COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0),
|
||
d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id,
|
||
d.attributes::text, d.thumbs::text,
|
||
c.enabled, c.sort_order, r.revision, r.source_name, r.source_format,
|
||
r.animation_sha256, r.width, r.height, r.frame_rate, r.created_by, c.updated_at,
|
||
(SELECT COUNT(*) FROM peer_star_gifts p WHERE p.gift_id=c.gift_id)
|
||
FROM star_gift_catalog c
|
||
JOIN star_gift_catalog_revisions r ON r.id=c.active_revision_id
|
||
LEFT JOIN star_gift_collectible_revisions cr ON cr.id=c.collectible_revision_id AND cr.status='published'
|
||
JOIN documents d ON d.id=r.document_id
|
||
WHERE c.gift_id=$1`, giftID)
|
||
var entry domain.StarGiftCatalogEntry
|
||
var attrsJSON, thumbsJSON, sourceFormat string
|
||
var releasedByType string
|
||
var releasedByID int64
|
||
var hasBackground bool
|
||
var background domain.StarGiftBackground
|
||
if err := row.Scan(
|
||
&entry.Gift.ID, &entry.Gift.RevisionID, &entry.Gift.Stars, &entry.Gift.ConvertStars, &entry.Gift.Title,
|
||
&entry.Gift.Limited, &entry.Gift.SoldOut, &entry.Gift.Birthday, &entry.Gift.RequirePremium,
|
||
&entry.Gift.LimitedPerUser, &entry.Gift.PeerColorAvailable, &entry.Gift.Auction,
|
||
&entry.Gift.AvailabilityRemains, &entry.Gift.AvailabilityTotal, &entry.Gift.AvailabilityResale,
|
||
&entry.Gift.FirstSaleDate, &entry.Gift.LastSaleDate, &entry.Gift.ResellMinStars,
|
||
&releasedByType, &releasedByID, &entry.Gift.PerUserTotal, &entry.Gift.LockedUntilDate,
|
||
&entry.Gift.AuctionSlug, &entry.Gift.GiftsPerRound, &entry.Gift.AuctionStartDate,
|
||
&entry.Gift.UpgradeVariants, &hasBackground, &background.CenterColor, &background.EdgeColor,
|
||
&background.TextColor,
|
||
&entry.Gift.UpgradeStars, &entry.Gift.UpgradeTotal, &entry.Gift.UpgradeIssued,
|
||
&entry.Gift.Sticker.ID, &entry.Gift.Sticker.AccessHash, &entry.Gift.Sticker.FileReference, &entry.Gift.Sticker.Date,
|
||
&entry.Gift.Sticker.MimeType, &entry.Gift.Sticker.Size, &entry.Gift.Sticker.DCID, &attrsJSON, &thumbsJSON,
|
||
&entry.Enabled, &entry.SortOrder, &entry.Revision, &entry.SourceName, &sourceFormat,
|
||
&entry.AnimationSHA, &entry.Width, &entry.Height, &entry.FrameRate, &entry.CreatedBy, &entry.UpdatedAt,
|
||
&entry.ReceivedCount,
|
||
); err != nil {
|
||
return domain.StarGiftCatalogEntry{}, err
|
||
}
|
||
if releasedByType != "" && releasedByID > 0 {
|
||
entry.Gift.ReleasedBy = domain.Peer{Type: domain.PeerType(releasedByType), ID: releasedByID}
|
||
}
|
||
if hasBackground {
|
||
entry.Gift.Background = &background
|
||
}
|
||
if entry.Gift.LimitedPerUser {
|
||
entry.Gift.PerUserRemains = entry.Gift.PerUserTotal
|
||
}
|
||
attrs, err := decodeDocumentAttributes(attrsJSON)
|
||
if err != nil {
|
||
return domain.StarGiftCatalogEntry{}, err
|
||
}
|
||
thumbs, err := decodePhotoSizes(thumbsJSON)
|
||
if err != nil {
|
||
return domain.StarGiftCatalogEntry{}, err
|
||
}
|
||
entry.Gift.Sticker.Attributes = attrs
|
||
entry.Gift.Sticker.Thumbs = thumbs
|
||
entry.SourceFormat = domain.StarGiftAnimationFormat(sourceFormat)
|
||
entry.AnimationSize = entry.Gift.Sticker.Size
|
||
return entry, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) Create(ctx context.Context, gift domain.SavedStarGift) (int64, error) {
|
||
if !validSavedStarGift(gift) {
|
||
return 0, domain.ErrStarGiftInvalid
|
||
}
|
||
var id int64
|
||
err := s.db.QueryRow(ctx, `
|
||
WITH next_id AS (
|
||
SELECT nextval(pg_get_serial_sequence('public.peer_star_gifts', 'id'))::bigint AS id
|
||
)
|
||
INSERT INTO peer_star_gifts (id, owner_peer_type, owner_peer_id, from_user_id, gift_id, catalog_revision_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, prepaid_upgrade_stars, prepaid_upgrade_hash, gift_num, message)
|
||
SELECT next_id.id, $1,$2,$3,$4,$5,$6,
|
||
CASE WHEN $1 = 'channel' AND $7::bigint = 0 THEN next_id.id ELSE $7::bigint END,
|
||
$8,$9,$10,false,$11,$12,$13,$14,$15
|
||
FROM next_id
|
||
RETURNING id`,
|
||
string(gift.Owner.Type), gift.Owner.ID, gift.FromUserID, gift.GiftID, gift.RevisionID, gift.MsgID, gift.SavedID, gift.Date,
|
||
gift.NameHidden, gift.Unsaved, gift.ConvertStars, gift.PrepaidUpgradeStars, gift.PrepaidUpgradeHash, gift.GiftNum, gift.Message).Scan(&id)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("create star gift: %w", err)
|
||
}
|
||
return id, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) ListByOwner(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) {
|
||
return s.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{
|
||
Owner: owner, ExcludeUnsaved: excludeUnsaved, Offset: offset, Limit: limit,
|
||
})
|
||
}
|
||
|
||
func (s *StarGiftStore) ListByOwnerFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) {
|
||
owner, offset, limit := filter.Owner, filter.Offset, filter.Limit
|
||
if !validStarGiftOwner(owner) {
|
||
return domain.SavedStarGiftPage{}, nil
|
||
}
|
||
if limit <= 0 || limit > domain.MaxSavedStarGiftsLimit {
|
||
limit = domain.MaxSavedStarGiftsLimit
|
||
}
|
||
joins := `
|
||
JOIN star_gift_catalog c ON c.gift_id = p.gift_id
|
||
LEFT JOIN star_gift_collectible_revisions acr
|
||
ON acr.id = c.collectible_revision_id AND acr.status = 'published'`
|
||
conditions := []string{"p.owner_peer_type = $1", "p.owner_peer_id = $2", "p.lifecycle_status = 'active'"}
|
||
args := []any{string(owner.Type), owner.ID}
|
||
if filter.ExcludeUnsaved {
|
||
conditions = append(conditions, "NOT p.unsaved")
|
||
}
|
||
if filter.ExcludeSaved {
|
||
conditions = append(conditions, "p.unsaved")
|
||
}
|
||
if filter.ExcludeUnique {
|
||
conditions = append(conditions, "p.unique_gift_id IS NULL")
|
||
}
|
||
// telesrv ordinary catalog gifts are currently unlimited. Unique gifts are
|
||
// collectibles and therefore survive exclude_unlimited.
|
||
if filter.ExcludeUnlimited {
|
||
conditions = append(conditions, "p.unique_gift_id IS NOT NULL")
|
||
}
|
||
upgradable := `(p.unique_gift_id IS NULL AND acr.id IS NOT NULL AND acr.upgrade_stars > 0 AND acr.issued < acr.supply_total)`
|
||
if filter.ExcludeUpgradable {
|
||
conditions = append(conditions, "NOT "+upgradable)
|
||
}
|
||
if filter.ExcludeUnupgradable {
|
||
conditions = append(conditions, upgradable)
|
||
}
|
||
if filter.CollectionID > 0 {
|
||
args = append(args, filter.CollectionID)
|
||
conditions = append(conditions, fmt.Sprintf(`EXISTS (
|
||
SELECT 1 FROM star_gift_collection_items ci
|
||
JOIN star_gift_collections cc ON cc.collection_id = ci.collection_id
|
||
WHERE ci.saved_gift_id = p.id AND ci.collection_id = $%d
|
||
AND cc.owner_peer_type = p.owner_peer_type AND cc.owner_peer_id = p.owner_peer_id)`, len(args)))
|
||
}
|
||
where := strings.Join(conditions, " AND ")
|
||
countQuery := `SELECT COUNT(*) FROM peer_star_gifts p ` + joins + ` WHERE ` + where
|
||
var total int
|
||
if err := s.db.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
||
return domain.SavedStarGiftPage{}, fmt.Errorf("count star gifts: %w", err)
|
||
}
|
||
page := domain.SavedStarGiftPage{Count: total}
|
||
|
||
profileOrder := filter.CollectionID == 0
|
||
if cursor, ok := domain.DecodeSavedStarGiftListCursor(offset); ok {
|
||
if profileOrder && cursor.PinnedOrder > 0 {
|
||
args = append(args, cursor.PinnedOrder, cursor.ID)
|
||
where += fmt.Sprintf(` AND (
|
||
p.pinned_order = 0
|
||
OR p.pinned_order > $%d
|
||
OR (p.pinned_order = $%d AND p.id < $%d)
|
||
)`, len(args)-1, len(args)-1, len(args))
|
||
} else {
|
||
args = append(args, cursor.ID)
|
||
if profileOrder {
|
||
where += fmt.Sprintf(" AND p.pinned_order = 0 AND p.id < $%d", len(args))
|
||
} else {
|
||
where += fmt.Sprintf(" AND p.id < $%d", len(args))
|
||
}
|
||
}
|
||
}
|
||
orderBy := "ORDER BY p.id DESC"
|
||
if profileOrder {
|
||
orderBy = "ORDER BY (p.pinned_order = 0), p.pinned_order, p.id DESC"
|
||
}
|
||
args = append(args, limit+1)
|
||
limitPlaceholder := len(args)
|
||
rows, err := s.db.Query(ctx, `
|
||
SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id,
|
||
p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, p.prepaid_upgrade_hash, p.gift_num,
|
||
p.lifecycle_status, p.transfer_stars, p.can_export_at, p.can_transfer_at, p.can_resell_at,
|
||
p.drop_original_details_stars, p.can_craft_at,
|
||
p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order,
|
||
COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id)
|
||
FROM star_gift_collection_items i
|
||
JOIN star_gift_collections c ON c.collection_id=i.collection_id
|
||
WHERE i.saved_gift_id=p.id), ARRAY[]::integer[])
|
||
FROM peer_star_gifts p `+joins+`
|
||
WHERE `+where+`
|
||
`+orderBy+`
|
||
LIMIT $`+fmt.Sprint(limitPlaceholder), args...)
|
||
if err != nil {
|
||
return domain.SavedStarGiftPage{}, fmt.Errorf("list star gifts: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
gifts := make([]domain.SavedStarGift, 0, limit)
|
||
for rows.Next() {
|
||
g, err := scanSavedStarGift(rows)
|
||
if err != nil {
|
||
return domain.SavedStarGiftPage{}, err
|
||
}
|
||
gifts = append(gifts, g)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return domain.SavedStarGiftPage{}, fmt.Errorf("iterate star gifts: %w", err)
|
||
}
|
||
if len(gifts) > limit {
|
||
gifts = gifts[:limit]
|
||
last := gifts[len(gifts)-1]
|
||
pinnedOrder := 0
|
||
if profileOrder {
|
||
pinnedOrder = last.PinnedOrder
|
||
}
|
||
page.NextOffset = domain.EncodeSavedStarGiftListCursor(pinnedOrder, last.ID)
|
||
}
|
||
page.Gifts = gifts
|
||
return page, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) {
|
||
if !validStarGiftOwner(owner) || len(refs) > domain.MaxStarGiftCollectionItems {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
if len(refs) == 0 {
|
||
return []int64{}, nil
|
||
}
|
||
type resolveKey struct {
|
||
value int64
|
||
slug string
|
||
}
|
||
keys := make([]resolveKey, 0, len(refs))
|
||
values := make([]int64, 0, len(refs))
|
||
slugs := make([]string, 0, len(refs))
|
||
seenKeys := make(map[string]struct{}, len(refs))
|
||
for _, ref := range refs {
|
||
if ref.Owner != owner || !ref.Valid() {
|
||
return nil, domain.ErrStarGiftNotFound
|
||
}
|
||
if ref.Slug != "" {
|
||
slug := strings.ToLower(strings.TrimSpace(ref.Slug))
|
||
key := "slug:" + slug
|
||
if _, duplicate := seenKeys[key]; duplicate {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
seenKeys[key] = struct{}{}
|
||
keys = append(keys, resolveKey{slug: slug})
|
||
slugs = append(slugs, slug)
|
||
continue
|
||
}
|
||
value := int64(ref.MsgID)
|
||
if owner.Type == domain.PeerTypeChannel {
|
||
value = ref.SavedID
|
||
}
|
||
key := fmt.Sprintf("id:%d", value)
|
||
if _, duplicate := seenKeys[key]; duplicate {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
seenKeys[key] = struct{}{}
|
||
keys = append(keys, resolveKey{value: value})
|
||
values = append(values, value)
|
||
}
|
||
query := `SELECT p.saved_id::bigint, COALESCE(u.slug, ''), p.id
|
||
FROM peer_star_gifts p
|
||
LEFT JOIN unique_star_gifts u ON u.id=p.unique_gift_id
|
||
WHERE p.owner_peer_type=$1 AND p.owner_peer_id=$2 AND p.lifecycle_status='active'
|
||
AND (p.saved_id::bigint=ANY($3::bigint[]) OR u.slug=ANY($4::text[]))`
|
||
if owner.Type == domain.PeerTypeUser {
|
||
query = `SELECT ref.msg_id, COALESCE(u.slug, ''), p.id
|
||
FROM peer_star_gifts p
|
||
LEFT JOIN unique_star_gifts u ON u.id=p.unique_gift_id
|
||
CROSS JOIN LATERAL (
|
||
SELECT p.msg_id::bigint AS msg_id
|
||
UNION ALL
|
||
SELECT r.msg_id::bigint FROM star_gift_user_message_refs r
|
||
WHERE r.saved_gift_id=p.id AND r.owner_user_id=p.owner_peer_id
|
||
) ref
|
||
WHERE p.owner_peer_type=$1 AND p.owner_peer_id=$2 AND p.lifecycle_status='active'
|
||
AND (ref.msg_id=ANY($3::bigint[])
|
||
OR u.slug=ANY($4::text[]))`
|
||
}
|
||
rows, err := s.db.Query(ctx, query, string(owner.Type), owner.ID, values, slugs)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("resolve saved star gifts: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
resolvedValues := make(map[int64]int64, len(values))
|
||
resolvedSlugs := make(map[string]int64, len(slugs))
|
||
for rows.Next() {
|
||
var primaryValue, id int64
|
||
var slug string
|
||
if err := rows.Scan(&primaryValue, &slug, &id); err != nil {
|
||
return nil, fmt.Errorf("scan resolved saved star gift: %w", err)
|
||
}
|
||
if existing := resolvedValues[primaryValue]; existing != 0 && existing != id {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
resolvedValues[primaryValue] = id
|
||
if slug != "" {
|
||
if existing := resolvedSlugs[slug]; existing != 0 && existing != id {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
resolvedSlugs[slug] = id
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("iterate resolved saved star gifts: %w", err)
|
||
}
|
||
out := make([]int64, 0, len(keys))
|
||
seenIDs := make(map[int64]struct{}, len(keys))
|
||
for _, key := range keys {
|
||
id := resolvedValues[key.value]
|
||
if key.slug != "" {
|
||
id = resolvedSlugs[key.slug]
|
||
}
|
||
if id == 0 {
|
||
return nil, domain.ErrStarGiftNotFound
|
||
}
|
||
if _, duplicate := seenIDs[id]; duplicate {
|
||
return nil, domain.ErrStarGiftCollectibleInvalid
|
||
}
|
||
seenIDs[id] = struct{}{}
|
||
out = append(out, id)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) GetByRef(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) {
|
||
if !ref.Valid() {
|
||
return domain.SavedStarGift{}, false, nil
|
||
}
|
||
where, args := savedStarGiftRefWhere(ref)
|
||
row := s.db.QueryRow(ctx, `
|
||
SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id,
|
||
p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, p.prepaid_upgrade_hash, p.gift_num,
|
||
p.lifecycle_status, p.transfer_stars, p.can_export_at, p.can_transfer_at, p.can_resell_at,
|
||
p.drop_original_details_stars, p.can_craft_at,
|
||
p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order,
|
||
COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id)
|
||
FROM star_gift_collection_items i
|
||
JOIN star_gift_collections c ON c.collection_id=i.collection_id
|
||
WHERE i.saved_gift_id=p.id), ARRAY[]::integer[])
|
||
FROM peer_star_gifts p
|
||
WHERE `+where, args...)
|
||
g, err := scanSavedStarGift(row)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.SavedStarGift{}, false, nil
|
||
}
|
||
return domain.SavedStarGift{}, false, err
|
||
}
|
||
return g, true, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) ResolveUserMessageRef(ctx context.Context, viewerUserID int64, msgID int) (domain.SavedStarGiftRef, bool, error) {
|
||
if s == nil || s.db == nil || viewerUserID <= 0 || msgID <= 0 {
|
||
return domain.SavedStarGiftRef{}, false, nil
|
||
}
|
||
var ownerType string
|
||
var ownerID, savedID int64
|
||
err := s.db.QueryRow(ctx, `
|
||
SELECT gift.owner_peer_type,gift.owner_peer_id,gift.saved_id
|
||
FROM star_gift_user_message_refs ref
|
||
JOIN peer_star_gifts gift ON gift.id=ref.saved_gift_id
|
||
JOIN message_boxes box
|
||
ON box.owner_user_id=ref.owner_user_id AND box.box_id=ref.msg_id
|
||
WHERE ref.owner_user_id=$1 AND ref.msg_id=$2
|
||
AND NOT box.deleted
|
||
AND gift.lifecycle_status='active'
|
||
AND (
|
||
(gift.owner_peer_type='user' AND gift.owner_peer_id=$1)
|
||
OR (
|
||
gift.owner_peer_type='channel'
|
||
AND (
|
||
(
|
||
box.media #>> '{service_action,kind}'='star_gift'
|
||
AND box.media #>> '{service_action,star_gift,peer_channel_id}'=gift.owner_peer_id::text
|
||
AND box.media #>> '{service_action,star_gift,saved_id}'=gift.saved_id::text
|
||
)
|
||
OR (
|
||
box.media #>> '{service_action,kind}'='star_gift_unique'
|
||
AND box.media #>> '{service_action,star_gift_unique,peer,Type}'='channel'
|
||
AND box.media #>> '{service_action,star_gift_unique,peer,ID}'=gift.owner_peer_id::text
|
||
AND box.media #>> '{service_action,star_gift_unique,saved_id}'=gift.saved_id::text
|
||
)
|
||
)
|
||
)
|
||
)`,
|
||
viewerUserID, msgID).Scan(&ownerType, &ownerID, &savedID)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.SavedStarGiftRef{}, false, nil
|
||
}
|
||
if err != nil {
|
||
return domain.SavedStarGiftRef{}, false, fmt.Errorf("resolve star gift user message ref: %w", err)
|
||
}
|
||
owner := domain.Peer{Type: domain.PeerType(ownerType), ID: ownerID}
|
||
switch owner.Type {
|
||
case domain.PeerTypeUser:
|
||
return domain.SavedStarGiftRef{Owner: owner, MsgID: msgID}, true, nil
|
||
case domain.PeerTypeChannel:
|
||
if savedID <= 0 {
|
||
return domain.SavedStarGiftRef{}, false, domain.ErrStarGiftOwnerInvalid
|
||
}
|
||
return domain.SavedStarGiftRef{Owner: owner, SavedID: savedID}, true, nil
|
||
default:
|
||
return domain.SavedStarGiftRef{}, false, domain.ErrStarGiftOwnerInvalid
|
||
}
|
||
}
|
||
|
||
func (s *StarGiftStore) CountByOwner(ctx context.Context, owner domain.Peer) (int, error) {
|
||
if !validStarGiftOwner(owner) {
|
||
return 0, nil
|
||
}
|
||
var n int
|
||
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM peer_star_gifts WHERE owner_peer_type = $1 AND owner_peer_id = $2 AND lifecycle_status='active' AND NOT unsaved`, string(owner.Type), owner.ID).Scan(&n); err != nil {
|
||
return 0, fmt.Errorf("count star gifts: %w", err)
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) SetUnsaved(ctx context.Context, ref domain.SavedStarGiftRef, unsaved bool) (bool, error) {
|
||
if !ref.Valid() {
|
||
return false, domain.ErrStarGiftNotFound
|
||
}
|
||
changed := false
|
||
err := withTx(ctx, s.db, "set star gift unsaved", func(tx pgx.Tx) error {
|
||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(ref.Owner)); err != nil {
|
||
return err
|
||
}
|
||
where, args := savedStarGiftRefWhere(ref)
|
||
var savedID int64
|
||
var pinnedOrder int
|
||
err := tx.QueryRow(ctx, `SELECT id,pinned_order FROM peer_star_gifts WHERE `+where+` AND lifecycle_status='active' FOR UPDATE`, args...).Scan(&savedID, &pinnedOrder)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET unsaved=$2,pinned_order=CASE WHEN $2 THEN 0 ELSE pinned_order END WHERE id=$1`, savedID, unsaved); err != nil {
|
||
return err
|
||
}
|
||
if unsaved && pinnedOrder > 0 {
|
||
// The positive-order unique index is immediate. Move the bounded
|
||
// vector one vacant slot at a time so no transient duplicate order
|
||
// can be observed by PostgreSQL.
|
||
for order := pinnedOrder + 1; order <= domain.MaxPinnedStarGifts; order++ {
|
||
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=$4
|
||
WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND pinned_order=$3`, string(ref.Owner.Type), ref.Owner.ID, order, order-1); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
changed = true
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return false, fmt.Errorf("set star gift unsaved: %w", err)
|
||
}
|
||
return changed, nil
|
||
}
|
||
|
||
func (s *StarGiftStore) MarkConverted(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) {
|
||
if !ref.Valid() {
|
||
return domain.SavedStarGift{}, domain.ErrStarGiftNotFound
|
||
}
|
||
out := domain.SavedStarGift{}
|
||
err := withTx(ctx, s.db, "convert star gift", func(tx pgx.Tx) error {
|
||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(ref.Owner)); err != nil {
|
||
return fmt.Errorf("lock star gift owner collections: %w", err)
|
||
}
|
||
where, args := savedStarGiftRefWhere(ref)
|
||
row := tx.QueryRow(ctx, `
|
||
SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id,
|
||
p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, p.prepaid_upgrade_hash, p.gift_num,
|
||
p.lifecycle_status, p.transfer_stars, p.can_export_at, p.can_transfer_at, p.can_resell_at,
|
||
p.drop_original_details_stars, p.can_craft_at,
|
||
p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order,
|
||
COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id)
|
||
FROM star_gift_collection_items i
|
||
JOIN star_gift_collections c ON c.collection_id=i.collection_id
|
||
WHERE i.saved_gift_id=p.id), ARRAY[]::integer[])
|
||
FROM peer_star_gifts p
|
||
WHERE `+where+` FOR UPDATE`, args...)
|
||
g, err := scanSavedStarGift(row)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.ErrStarGiftNotFound
|
||
}
|
||
return err
|
||
}
|
||
if g.Converted {
|
||
return domain.ErrStarGiftAlreadyConverted
|
||
}
|
||
if g.UniqueGiftID != 0 {
|
||
return domain.ErrStarGiftAlreadyUpgraded
|
||
}
|
||
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET converted = true, lifecycle_status='converted', unsaved = true, pinned_order = 0 WHERE id = $1`, g.ID); err != nil {
|
||
return fmt.Errorf("mark star gift converted: %w", err)
|
||
}
|
||
if err := removeSavedGiftFromCollections(ctx, tx, g.Owner, g.ID); err != nil {
|
||
return err
|
||
}
|
||
g.Converted = true
|
||
g.LifecycleStatus = domain.StarGiftLifecycleConverted
|
||
g.Unsaved = true
|
||
g.PinnedOrder = 0
|
||
g.CollectionIDs = nil
|
||
out = g
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return domain.SavedStarGift{}, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func scanSavedStarGift(row rowScanner) (domain.SavedStarGift, error) {
|
||
var g domain.SavedStarGift
|
||
var ownerType string
|
||
if err := row.Scan(&g.ID, &ownerType, &g.Owner.ID, &g.FromUserID, &g.GiftID, &g.RevisionID, &g.MsgID, &g.SavedID, &g.Date,
|
||
&g.NameHidden, &g.Unsaved, &g.Converted, &g.ConvertStars, &g.PrepaidUpgradeStars, &g.PrepaidUpgradeHash, &g.GiftNum,
|
||
&g.LifecycleStatus, &g.TransferStars, &g.CanExportAt, &g.CanTransferAt, &g.CanResellAt,
|
||
&g.DropOriginalDetailsStars, &g.CanCraftAt, &g.Message, &g.UniqueGiftID,
|
||
&g.UpgradeMsgID, &g.PinnedOrder, &g.CollectionIDs); err != nil {
|
||
return domain.SavedStarGift{}, err
|
||
}
|
||
g.Owner.Type = domain.PeerType(ownerType)
|
||
return g, nil
|
||
}
|
||
|
||
func savedStarGiftRefWhere(ref domain.SavedStarGiftRef) (string, []any) {
|
||
args := []any{string(ref.Owner.Type), ref.Owner.ID}
|
||
if ref.Slug != "" {
|
||
args = append(args, strings.ToLower(strings.TrimSpace(ref.Slug)))
|
||
return "owner_peer_type = $1 AND owner_peer_id = $2 AND unique_gift_id = (SELECT id FROM unique_star_gifts WHERE slug = $3)", args
|
||
}
|
||
switch ref.Owner.Type {
|
||
case domain.PeerTypeChannel:
|
||
args = append(args, ref.SavedID)
|
||
return "owner_peer_type = $1 AND owner_peer_id = $2 AND saved_id = $3", args
|
||
default:
|
||
args = append(args, ref.MsgID)
|
||
return `owner_peer_type = $1 AND owner_peer_id = $2 AND (
|
||
msg_id = $3 OR EXISTS (
|
||
SELECT 1 FROM star_gift_user_message_refs r
|
||
WHERE r.saved_gift_id = id AND r.owner_user_id = $2 AND r.msg_id = $3
|
||
))`, args
|
||
}
|
||
}
|
||
|
||
func validSavedStarGift(g domain.SavedStarGift) bool {
|
||
if g.GiftID == 0 || g.RevisionID == 0 || !validStarGiftOwner(g.Owner) {
|
||
return false
|
||
}
|
||
switch g.Owner.Type {
|
||
case domain.PeerTypeUser:
|
||
return g.MsgID > 0 && g.SavedID == 0
|
||
case domain.PeerTypeChannel:
|
||
return g.MsgID == 0 && g.SavedID >= 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func validStarGiftOwner(owner domain.Peer) bool {
|
||
return owner.ID != 0 && (owner.Type == domain.PeerTypeUser || owner.Type == domain.PeerTypeChannel)
|
||
}
|