982 lines
39 KiB
Go
982 lines
39 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/postgres/sqlcgen"
|
|
)
|
|
|
|
func nullablePermille(attribute domain.StarGiftCollectibleAttribute) any {
|
|
if attribute.RarityKind != domain.StarGiftRarityPermille {
|
|
return nil
|
|
}
|
|
return attribute.RarityPermille
|
|
}
|
|
|
|
func nullableSHA256(value []byte) any {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func nullablePositiveInt64(value int64) any {
|
|
if value <= 0 {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func nullableOfficialGiftJSON(value []byte) any {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
return string(value)
|
|
}
|
|
|
|
func (s *StarGiftStore) PublishCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) {
|
|
write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix))
|
|
write.Actor = strings.TrimSpace(write.Actor)
|
|
write.CommandID = strings.TrimSpace(write.CommandID)
|
|
if err := domain.ValidateStarGiftCollectibleWrite(write); err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, err
|
|
}
|
|
var result domain.StarGiftCollectibleRevision
|
|
err := withTx(ctx, s.db, "publish collectible star gift revision", func(tx pgx.Tx) error {
|
|
var ignored int64
|
|
if err := tx.QueryRow(ctx, `SELECT gift_id FROM star_gift_catalog WHERE gift_id=$1 FOR UPDATE`, write.GiftID).Scan(&ignored); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrStarGiftNotFound
|
|
}
|
|
return fmt.Errorf("lock collectible catalog gift: %w", err)
|
|
}
|
|
var revision int
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT COALESCE(MAX(revision), 0) + 1 FROM star_gift_collectible_revisions WHERE gift_id=$1`, write.GiftID).Scan(&revision); err != nil {
|
|
return fmt.Errorf("allocate collectible revision: %w", err)
|
|
}
|
|
var revisionID int64
|
|
if err := tx.QueryRow(ctx, `
|
|
INSERT INTO star_gift_collectible_revisions
|
|
(gift_id, revision, upgrade_stars, supply_total, slug_prefix, status, created_by, command_id,
|
|
official_gift_id, source_manifest_sha256)
|
|
VALUES ($1,$2,$3,$4,$5,'draft',$6,$7,NULLIF($8::bigint,0),$9)
|
|
RETURNING id`, write.GiftID, revision, write.UpgradeStars, write.SupplyTotal, write.SlugPrefix, write.Actor, write.CommandID,
|
|
write.OfficialGiftID, nullableSHA256(write.SourceManifestSHA256)).Scan(&revisionID); err != nil {
|
|
return fmt.Errorf("insert collectible revision: %w", err)
|
|
}
|
|
media := NewMediaStore(tx)
|
|
insertAnimated := func(table string, attributes []domain.StarGiftCollectibleAttribute, models bool) error {
|
|
for _, attribute := range attributes {
|
|
if err := media.PutDocument(ctx, *attribute.Document); err != nil {
|
|
return fmt.Errorf("put collectible %s document: %w", attribute.Kind, err)
|
|
}
|
|
if err := media.PutFileBlob(ctx, *attribute.Blob); err != nil {
|
|
return fmt.Errorf("put collectible %s blob: %w", attribute.Kind, err)
|
|
}
|
|
animation := attribute.Animation
|
|
var query string
|
|
if models {
|
|
query = fmt.Sprintf(`
|
|
INSERT INTO %s
|
|
(collectible_revision_id, name, document_id, animation_json, animation_sha256,
|
|
source_name, source_format, width, height, frame_rate, in_point, out_point,
|
|
rarity_kind, rarity_permille, crafted, official_document_id, sort_order)
|
|
VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)`, table)
|
|
if _, err := tx.Exec(ctx, query, revisionID, strings.TrimSpace(attribute.Name), attribute.Document.ID,
|
|
string(animation.JSON), animation.SHA256, animation.SourceName, string(animation.SourceFormat),
|
|
animation.Width, animation.Height, animation.FrameRate, animation.InPoint, animation.OutPoint,
|
|
string(attribute.RarityKind), nullablePermille(attribute), attribute.Crafted,
|
|
nullablePositiveInt64(attribute.OfficialDocumentID), attribute.SortOrder); err != nil {
|
|
return fmt.Errorf("insert collectible %s attribute: %w", attribute.Kind, err)
|
|
}
|
|
} else {
|
|
query = fmt.Sprintf(`
|
|
INSERT INTO %s
|
|
(collectible_revision_id, name, document_id, animation_json, animation_sha256,
|
|
source_name, source_format, width, height, frame_rate, in_point, out_point,
|
|
rarity_kind, rarity_permille, official_document_id, sort_order)
|
|
VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`, table)
|
|
if _, err := tx.Exec(ctx, query, revisionID, strings.TrimSpace(attribute.Name), attribute.Document.ID,
|
|
string(animation.JSON), animation.SHA256, animation.SourceName, string(animation.SourceFormat),
|
|
animation.Width, animation.Height, animation.FrameRate, animation.InPoint, animation.OutPoint,
|
|
string(attribute.RarityKind), nullablePermille(attribute), nullablePositiveInt64(attribute.OfficialDocumentID),
|
|
attribute.SortOrder); err != nil {
|
|
return fmt.Errorf("insert collectible %s attribute: %w", attribute.Kind, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := insertAnimated("star_gift_collectible_models", write.Models, true); err != nil {
|
|
return err
|
|
}
|
|
if err := insertAnimated("star_gift_collectible_patterns", write.Patterns, false); err != nil {
|
|
return err
|
|
}
|
|
for _, attribute := range write.Backdrops {
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO star_gift_collectible_backdrops
|
|
(collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color,
|
|
text_color, rarity_kind, rarity_permille, sort_order)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, revisionID, strings.TrimSpace(attribute.Name), attribute.BackdropID,
|
|
attribute.CenterColor, attribute.EdgeColor, attribute.PatternColor, attribute.TextColor,
|
|
string(attribute.RarityKind), nullablePermille(attribute), attribute.SortOrder); err != nil {
|
|
return fmt.Errorf("insert collectible backdrop: %w", err)
|
|
}
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE star_gift_collectible_revisions SET status='published', published_at=now() WHERE id=$1`, revisionID); err != nil {
|
|
return fmt.Errorf("publish collectible revision: %w", err)
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE star_gift_catalog SET collectible_revision_id=$2, updated_at=now() WHERE gift_id=$1`, write.GiftID, revisionID); err != nil {
|
|
return fmt.Errorf("activate collectible revision: %w", err)
|
|
}
|
|
var err error
|
|
result, err = collectibleRevisionByID(ctx, tx, revisionID)
|
|
return err
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func (s *StarGiftStore) ActiveCollectibleRevision(ctx context.Context, giftID int64) (domain.StarGiftCollectibleRevision, bool, error) {
|
|
revisionID, ok, err := activeCollectibleRevisionID(ctx, s.db, giftID)
|
|
if err != nil || !ok {
|
|
return domain.StarGiftCollectibleRevision{}, ok, err
|
|
}
|
|
revision, err := collectibleRevisionByID(ctx, s.db, revisionID)
|
|
if err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, false, err
|
|
}
|
|
return revision, true, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) ActiveCollectibleProjection(ctx context.Context, giftID int64, samplePerKind int) (domain.StarGiftCollectibleRevision, bool, error) {
|
|
revisionID, ok, err := activeCollectibleRevisionID(ctx, s.db, giftID)
|
|
if err != nil || !ok {
|
|
return domain.StarGiftCollectibleRevision{}, ok, err
|
|
}
|
|
revision, err := collectibleRevisionProjectionByID(ctx, s.db, revisionID, samplePerKind)
|
|
if err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, false, err
|
|
}
|
|
return revision, true, nil
|
|
}
|
|
|
|
func activeCollectibleRevisionID(ctx context.Context, db sqlcgen.DBTX, giftID int64) (int64, bool, error) {
|
|
var revisionID int64
|
|
err := db.QueryRow(ctx, `
|
|
SELECT collectible_revision_id FROM star_gift_catalog
|
|
WHERE gift_id=$1 AND collectible_revision_id IS NOT NULL`, giftID).Scan(&revisionID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return 0, false, nil
|
|
}
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("get active collectible revision: %w", err)
|
|
}
|
|
return revisionID, true, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) {
|
|
out := make(map[int64]domain.StarGiftCollectibleAvailability, len(giftIDs))
|
|
if len(giftIDs) == 0 {
|
|
return out, nil
|
|
}
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT c.gift_id, r.upgrade_stars, r.supply_total, r.issued
|
|
FROM star_gift_catalog c
|
|
JOIN star_gift_collectible_revisions r ON r.id=c.collectible_revision_id
|
|
WHERE c.gift_id=ANY($1) AND r.status='published'`, giftIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list collectible availability: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var giftID int64
|
|
var availability domain.StarGiftCollectibleAvailability
|
|
if err := rows.Scan(&giftID, &availability.UpgradeStars, &availability.SupplyTotal, &availability.Issued); err != nil {
|
|
return nil, fmt.Errorf("scan collectible availability: %w", err)
|
|
}
|
|
out[giftID] = availability
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("list collectible availability rows: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func collectibleRevisionByID(ctx context.Context, db sqlcgen.DBTX, revisionID int64) (domain.StarGiftCollectibleRevision, error) {
|
|
return readCollectibleRevisionByID(ctx, db, revisionID, collectibleRevisionReadOptions{includeAnimationJSON: true})
|
|
}
|
|
|
|
func collectibleRevisionProjectionByID(ctx context.Context, db sqlcgen.DBTX, revisionID int64, samplePerKind int) (domain.StarGiftCollectibleRevision, error) {
|
|
return readCollectibleRevisionByID(ctx, db, revisionID, collectibleRevisionReadOptions{samplePerKind: samplePerKind})
|
|
}
|
|
|
|
type collectibleRevisionReadOptions struct {
|
|
includeAnimationJSON bool
|
|
samplePerKind int
|
|
}
|
|
|
|
func readCollectibleRevisionByID(ctx context.Context, db sqlcgen.DBTX, revisionID int64, options collectibleRevisionReadOptions) (domain.StarGiftCollectibleRevision, error) {
|
|
var revision domain.StarGiftCollectibleRevision
|
|
var status string
|
|
var publishedAt pgtype.Timestamptz
|
|
if err := db.QueryRow(ctx, `
|
|
SELECT id, gift_id, revision, upgrade_stars, supply_total, issued, slug_prefix, status,
|
|
created_by, created_at, published_at, COALESCE(official_gift_id,0), source_manifest_sha256
|
|
FROM star_gift_collectible_revisions WHERE id=$1`, revisionID).Scan(
|
|
&revision.ID, &revision.GiftID, &revision.Revision, &revision.UpgradeStars, &revision.SupplyTotal,
|
|
&revision.Issued, &revision.SlugPrefix, &status, &revision.CreatedBy, &revision.CreatedAt, &publishedAt,
|
|
&revision.OfficialGiftID, &revision.SourceManifestSHA256,
|
|
); err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, fmt.Errorf("get collectible revision: %w", err)
|
|
}
|
|
revision.Published = status == "published"
|
|
if publishedAt.Valid {
|
|
revision.PublishedAt = publishedAt.Time
|
|
}
|
|
var err error
|
|
if revision.Models, err = listAnimatedCollectibleAttributes(ctx, db, revisionID, domain.StarGiftCollectibleModel, options); err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, err
|
|
}
|
|
if revision.Patterns, err = listAnimatedCollectibleAttributes(ctx, db, revisionID, domain.StarGiftCollectiblePattern, options); err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, err
|
|
}
|
|
if revision.Backdrops, err = listCollectibleBackdrops(ctx, db, revisionID, options); err != nil {
|
|
return domain.StarGiftCollectibleRevision{}, err
|
|
}
|
|
return revision, nil
|
|
}
|
|
|
|
func listAnimatedCollectibleAttributes(ctx context.Context, db sqlcgen.DBTX, revisionID int64, kind domain.StarGiftCollectibleAttributeKind, options collectibleRevisionReadOptions) ([]domain.StarGiftCollectibleAttribute, error) {
|
|
table := "star_gift_collectible_models"
|
|
if kind == domain.StarGiftCollectiblePattern {
|
|
table = "star_gift_collectible_patterns"
|
|
} else if kind != domain.StarGiftCollectibleModel {
|
|
return nil, domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
craftedExpression := "false"
|
|
if kind == domain.StarGiftCollectibleModel {
|
|
craftedExpression = "a.crafted"
|
|
}
|
|
animationJSONExpression := "''::text"
|
|
if options.includeAnimationJSON {
|
|
animationJSONExpression = "a.animation_json::text"
|
|
}
|
|
prefix := ""
|
|
from := fmt.Sprintf("%s a JOIN documents d ON d.id=a.document_id", table)
|
|
args := []any{revisionID}
|
|
if options.samplePerKind > 0 {
|
|
extra := ""
|
|
if kind == domain.StarGiftCollectibleModel {
|
|
extra = " AND NOT crafted"
|
|
}
|
|
prefix = fmt.Sprintf(`WITH picked AS MATERIALIZED (
|
|
SELECT id FROM %s
|
|
WHERE collectible_revision_id=$1 AND rarity_kind='permille' AND rarity_permille > 0%s
|
|
ORDER BY random()
|
|
LIMIT $2
|
|
)`, table, extra)
|
|
from = fmt.Sprintf("picked p JOIN %s a ON a.id=p.id JOIN documents d ON d.id=a.document_id", table)
|
|
args = append(args, options.samplePerKind)
|
|
}
|
|
rows, err := db.Query(ctx, fmt.Sprintf(`
|
|
%s
|
|
SELECT a.id, a.collectible_revision_id, a.name, a.rarity_kind, COALESCE(a.rarity_permille,0),
|
|
%s, COALESCE(a.official_document_id,0), a.sort_order,
|
|
%s, a.animation_sha256, a.source_name, a.source_format,
|
|
a.width, a.height, a.frame_rate, a.in_point, a.out_point,
|
|
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 %s
|
|
%s
|
|
ORDER BY a.sort_order, a.id`, prefix, craftedExpression, animationJSONExpression, from,
|
|
func() string {
|
|
if options.samplePerKind > 0 {
|
|
return ""
|
|
}
|
|
return "WHERE a.collectible_revision_id=$1"
|
|
}()), args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list collectible %s attributes: %w", kind, err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.StarGiftCollectibleAttribute, 0)
|
|
for rows.Next() {
|
|
attribute := domain.StarGiftCollectibleAttribute{Kind: kind, Document: &domain.Document{}, Animation: &domain.StarGiftAnimation{}}
|
|
var attrsJSON, thumbsJSON, sourceFormat string
|
|
if err := rows.Scan(&attribute.ID, &attribute.CollectibleRevisionID, &attribute.Name, &attribute.RarityKind,
|
|
&attribute.RarityPermille, &attribute.Crafted, &attribute.OfficialDocumentID, &attribute.SortOrder,
|
|
&attribute.Animation.JSON, &attribute.Animation.SHA256, &attribute.Animation.SourceName, &sourceFormat,
|
|
&attribute.Animation.Width, &attribute.Animation.Height, &attribute.Animation.FrameRate, &attribute.Animation.InPoint, &attribute.Animation.OutPoint,
|
|
&attribute.Document.ID, &attribute.Document.AccessHash, &attribute.Document.FileReference, &attribute.Document.Date,
|
|
&attribute.Document.MimeType, &attribute.Document.Size, &attribute.Document.DCID, &attrsJSON, &thumbsJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
attribute.Animation.SourceFormat = domain.StarGiftAnimationFormat(sourceFormat)
|
|
if attribute.Document.Attributes, err = decodeDocumentAttributes(attrsJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
if attribute.Document.Thumbs, err = decodePhotoSizes(thumbsJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, attribute)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func listCollectibleBackdrops(ctx context.Context, db sqlcgen.DBTX, revisionID int64, options collectibleRevisionReadOptions) ([]domain.StarGiftCollectibleAttribute, error) {
|
|
prefix := ""
|
|
from := "star_gift_collectible_backdrops a"
|
|
where := "WHERE a.collectible_revision_id=$1"
|
|
args := []any{revisionID}
|
|
if options.samplePerKind > 0 {
|
|
prefix = `WITH picked AS MATERIALIZED (
|
|
SELECT id FROM star_gift_collectible_backdrops
|
|
WHERE collectible_revision_id=$1 AND rarity_kind='permille' AND rarity_permille > 0
|
|
ORDER BY random()
|
|
LIMIT $2
|
|
)`
|
|
from = "picked p JOIN star_gift_collectible_backdrops a ON a.id=p.id"
|
|
where = ""
|
|
args = append(args, options.samplePerKind)
|
|
}
|
|
rows, err := db.Query(ctx, fmt.Sprintf(`
|
|
%s
|
|
SELECT a.id, a.collectible_revision_id, a.name, a.backdrop_id, a.center_color, a.edge_color, a.pattern_color,
|
|
a.text_color, a.rarity_kind, COALESCE(a.rarity_permille,0), a.sort_order
|
|
FROM %s
|
|
%s
|
|
ORDER BY a.sort_order, a.id`, prefix, from, where), args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list collectible backdrops: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.StarGiftCollectibleAttribute, 0)
|
|
for rows.Next() {
|
|
attribute := domain.StarGiftCollectibleAttribute{Kind: domain.StarGiftCollectibleBackdrop}
|
|
if err := rows.Scan(&attribute.ID, &attribute.CollectibleRevisionID, &attribute.Name, &attribute.BackdropID,
|
|
&attribute.CenterColor, &attribute.EdgeColor, &attribute.PatternColor, &attribute.TextColor,
|
|
&attribute.RarityKind, &attribute.RarityPermille, &attribute.SortOrder); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, attribute)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *StarGiftStore) CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) {
|
|
table := "star_gift_collectible_models"
|
|
if kind == domain.StarGiftCollectiblePattern {
|
|
table = "star_gift_collectible_patterns"
|
|
} else if kind != domain.StarGiftCollectibleModel {
|
|
return nil, false, nil
|
|
}
|
|
var raw []byte
|
|
err := s.db.QueryRow(ctx, fmt.Sprintf(`
|
|
SELECT a.animation_json::text FROM %s a
|
|
JOIN star_gift_catalog c ON c.collectible_revision_id=a.collectible_revision_id
|
|
WHERE c.gift_id=$1 AND a.id=$2`, table), giftID, attributeID).Scan(&raw)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("get collectible animation: %w", err)
|
|
}
|
|
return raw, true, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) {
|
|
return s.uniqueByPredicate(ctx, "u.slug=$1", strings.ToLower(strings.TrimSpace(slug)))
|
|
}
|
|
|
|
func (s *StarGiftStore) UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) {
|
|
return s.uniqueByPredicate(ctx, "u.id=$1", uniqueGiftID)
|
|
}
|
|
|
|
func (s *StarGiftStore) UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) {
|
|
out := make(map[int64]domain.UniqueStarGift, len(uniqueGiftIDs))
|
|
if len(uniqueGiftIDs) == 0 {
|
|
return out, nil
|
|
}
|
|
rows, err := s.db.Query(ctx, uniqueStarGiftQuery("u.id=ANY($1::bigint[])"), uniqueGiftIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list unique star gifts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
unique, err := scanUniqueStarGift(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[unique.ID] = unique
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate unique star gifts: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) ListUniqueByOwner(ctx context.Context, owner domain.Peer, limit int) ([]domain.UniqueStarGift, error) {
|
|
if owner.ID <= 0 || limit <= 0 {
|
|
return []domain.UniqueStarGift{}, nil
|
|
}
|
|
if limit > domain.MaxSavedStarGiftsLimit {
|
|
limit = domain.MaxSavedStarGiftsLimit
|
|
}
|
|
rows, err := s.db.Query(ctx, uniqueStarGiftQuery(`
|
|
u.owner_peer_type=$1 AND u.owner_peer_id=$2
|
|
AND NOT u.burned AND u.owner_address=''
|
|
AND sg.lifecycle_status='active'`)+`
|
|
ORDER BY u.id DESC
|
|
LIMIT $3`, string(owner.Type), owner.ID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list unique star gifts by owner: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.UniqueStarGift, 0, limit)
|
|
for rows.Next() {
|
|
gift, err := scanUniqueStarGift(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, gift)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate unique star gifts by owner: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) uniqueByPredicate(ctx context.Context, predicate string, value any) (domain.UniqueStarGift, bool, error) {
|
|
row := s.db.QueryRow(ctx, uniqueStarGiftQuery(predicate), value)
|
|
unique, err := scanUniqueStarGift(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.UniqueStarGift{}, false, nil
|
|
}
|
|
return domain.UniqueStarGift{}, false, err
|
|
}
|
|
return unique, true, nil
|
|
}
|
|
|
|
func uniqueStarGiftQuery(predicate string) string {
|
|
return fmt.Sprintf(`
|
|
SELECT u.id, u.gift_id, u.collectible_revision_id, u.source_saved_gift_id, u.title, u.slug, u.num,
|
|
COALESCE(u.owner_peer_type,''), COALESCE(u.owner_peer_id,0), u.keep_original_details, u.created_at,
|
|
u.require_premium, u.resale_ton_only, u.theme_available, u.burned, u.crafted,
|
|
u.owner_name, u.owner_address, u.gift_address,
|
|
COALESCE(l.currency,''), COALESCE(l.amount,0), COALESCE(l.version,0),
|
|
COALESCE(u.released_by_peer_type,''), COALESCE(u.released_by_peer_id,0),
|
|
u.value_amount, u.value_currency, u.value_usd_amount,
|
|
COALESCE(u.theme_peer_type,''), COALESCE(u.theme_peer_id,0),
|
|
COALESCE(u.host_peer_type,''), COALESCE(u.host_peer_id,0),
|
|
u.offer_min_stars, u.craft_chance_permille, u.last_sale_date,
|
|
u.last_sale_currency, u.last_sale_amount,
|
|
r.issued, r.supply_total, sg.from_user_id, u.original_owner_peer_type, u.original_owner_peer_id,
|
|
sg.gift_date, sg.message, sg.name_hidden,
|
|
m.id, m.name, m.rarity_kind, COALESCE(m.rarity_permille,0), m.crafted, md.id, md.access_hash, md.file_reference, md.date,
|
|
md.mime_type, md.size, md.dc_id, md.attributes::text, md.thumbs::text,
|
|
p.id, p.name, p.rarity_kind, COALESCE(p.rarity_permille,0), pd.id, pd.access_hash, pd.file_reference, pd.date,
|
|
pd.mime_type, pd.size, pd.dc_id, pd.attributes::text, pd.thumbs::text,
|
|
b.id, b.name, b.backdrop_id, b.center_color, b.edge_color, b.pattern_color, b.text_color,
|
|
b.rarity_kind, COALESCE(b.rarity_permille,0)
|
|
FROM unique_star_gifts u
|
|
JOIN star_gift_collectible_revisions r ON r.id=u.collectible_revision_id
|
|
JOIN star_gift_collectible_models m ON m.id=u.model_attribute_id
|
|
JOIN documents md ON md.id=m.document_id
|
|
JOIN star_gift_collectible_patterns p ON p.id=u.pattern_attribute_id
|
|
JOIN documents pd ON pd.id=p.document_id
|
|
JOIN star_gift_collectible_backdrops b ON b.id=u.backdrop_attribute_id
|
|
JOIN peer_star_gifts sg ON sg.id=u.source_saved_gift_id
|
|
LEFT JOIN star_gift_listings l ON l.unique_gift_id=u.id
|
|
WHERE %s`, predicate)
|
|
}
|
|
|
|
func scanUniqueStarGift(row rowScanner) (domain.UniqueStarGift, error) {
|
|
var unique domain.UniqueStarGift
|
|
var ownerType, originalOwnerType, listingCurrency, releasedByType, themePeerType, hostPeerType, lastSaleCurrency string
|
|
var listingAmount, lastSaleAmount int64
|
|
unique.Model.Kind = domain.StarGiftCollectibleModel
|
|
unique.Pattern.Kind = domain.StarGiftCollectiblePattern
|
|
unique.Backdrop.Kind = domain.StarGiftCollectibleBackdrop
|
|
unique.Model.Document = &domain.Document{}
|
|
unique.Pattern.Document = &domain.Document{}
|
|
var modelAttrs, modelThumbs, patternAttrs, patternThumbs string
|
|
if err := row.Scan(&unique.ID, &unique.GiftID, &unique.CollectibleRevisionID, &unique.SourceSavedGiftID,
|
|
&unique.Title, &unique.Slug, &unique.Num, &ownerType, &unique.Owner.ID, &unique.KeepOriginalDetails,
|
|
&unique.CreatedAt, &unique.RequirePremium, &unique.ResaleTonOnly, &unique.ThemeAvailable,
|
|
&unique.Burned, &unique.Crafted, &unique.OwnerName, &unique.OwnerAddress, &unique.GiftAddress,
|
|
&listingCurrency, &listingAmount, &unique.ResellVersion, &releasedByType, &unique.ReleasedBy.ID,
|
|
&unique.ValueAmount, &unique.ValueCurrency, &unique.ValueUSD,
|
|
&themePeerType, &unique.ThemePeer.ID, &hostPeerType, &unique.Host.ID,
|
|
&unique.OfferMinStars, &unique.CraftChancePermille, &unique.LastSaleDate,
|
|
&lastSaleCurrency, &lastSaleAmount,
|
|
&unique.AvailabilityIssued, &unique.AvailabilityTotal,
|
|
&unique.OriginalFromUserID, &originalOwnerType, &unique.OriginalOwner.ID, &unique.OriginalDate,
|
|
&unique.OriginalMessage, &unique.OriginalNameHidden,
|
|
&unique.Model.ID, &unique.Model.Name, &unique.Model.RarityKind, &unique.Model.RarityPermille, &unique.Model.Crafted,
|
|
&unique.Model.Document.ID, &unique.Model.Document.AccessHash, &unique.Model.Document.FileReference,
|
|
&unique.Model.Document.Date, &unique.Model.Document.MimeType, &unique.Model.Document.Size,
|
|
&unique.Model.Document.DCID, &modelAttrs, &modelThumbs,
|
|
&unique.Pattern.ID, &unique.Pattern.Name, &unique.Pattern.RarityKind, &unique.Pattern.RarityPermille,
|
|
&unique.Pattern.Document.ID, &unique.Pattern.Document.AccessHash, &unique.Pattern.Document.FileReference,
|
|
&unique.Pattern.Document.Date, &unique.Pattern.Document.MimeType, &unique.Pattern.Document.Size,
|
|
&unique.Pattern.Document.DCID, &patternAttrs, &patternThumbs,
|
|
&unique.Backdrop.ID, &unique.Backdrop.Name, &unique.Backdrop.BackdropID, &unique.Backdrop.CenterColor,
|
|
&unique.Backdrop.EdgeColor, &unique.Backdrop.PatternColor, &unique.Backdrop.TextColor,
|
|
&unique.Backdrop.RarityKind, &unique.Backdrop.RarityPermille); err != nil {
|
|
return domain.UniqueStarGift{}, fmt.Errorf("get unique star gift: %w", err)
|
|
}
|
|
unique.Owner.Type = domain.PeerType(ownerType)
|
|
unique.OriginalOwner.Type = domain.PeerType(originalOwnerType)
|
|
unique.ReleasedBy.Type = domain.PeerType(releasedByType)
|
|
unique.ThemePeer.Type = domain.PeerType(themePeerType)
|
|
unique.Host.Type = domain.PeerType(hostPeerType)
|
|
if listingCurrency != "" && listingAmount > 0 {
|
|
unique.ResellAmount = &domain.StarGiftAmount{Currency: domain.StarGiftCurrency(listingCurrency), Amount: listingAmount}
|
|
}
|
|
if lastSaleCurrency != "" && unique.LastSaleDate > 0 {
|
|
unique.LastSaleAmount = &domain.StarGiftAmount{Currency: domain.StarGiftCurrency(lastSaleCurrency), Amount: lastSaleAmount}
|
|
}
|
|
unique.Model.CollectibleRevisionID = unique.CollectibleRevisionID
|
|
unique.Pattern.CollectibleRevisionID = unique.CollectibleRevisionID
|
|
unique.Backdrop.CollectibleRevisionID = unique.CollectibleRevisionID
|
|
var err error
|
|
if unique.Model.Document.Attributes, err = decodeDocumentAttributes(modelAttrs); err != nil {
|
|
return domain.UniqueStarGift{}, err
|
|
}
|
|
if unique.Model.Document.Thumbs, err = decodePhotoSizes(modelThumbs); err != nil {
|
|
return domain.UniqueStarGift{}, err
|
|
}
|
|
if unique.Pattern.Document.Attributes, err = decodeDocumentAttributes(patternAttrs); err != nil {
|
|
return domain.UniqueStarGift{}, err
|
|
}
|
|
if unique.Pattern.Document.Thumbs, err = decodePhotoSizes(patternThumbs); err != nil {
|
|
return domain.UniqueStarGift{}, err
|
|
}
|
|
return unique, nil
|
|
}
|
|
|
|
func (s *StarGiftStore) ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT c.collection_id, c.title, c.hash, c.sort_order, c.created_at, c.updated_at, i.saved_gift_id
|
|
FROM star_gift_collections c
|
|
LEFT JOIN star_gift_collection_items i ON i.collection_id=c.collection_id
|
|
WHERE c.owner_peer_type=$1 AND c.owner_peer_id=$2
|
|
ORDER BY c.sort_order, c.collection_id, i.sort_order, i.saved_gift_id`, string(owner.Type), owner.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list star gift collections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.StarGiftCollection, 0)
|
|
index := make(map[int]int)
|
|
for rows.Next() {
|
|
var collection domain.StarGiftCollection
|
|
var giftID pgtype.Int8
|
|
if err := rows.Scan(&collection.CollectionID, &collection.Title, &collection.Hash, &collection.SortOrder,
|
|
&collection.CreatedAt, &collection.UpdatedAt, &giftID); err != nil {
|
|
return nil, err
|
|
}
|
|
position, ok := index[collection.CollectionID]
|
|
if !ok {
|
|
collection.Owner = owner
|
|
position = len(out)
|
|
index[collection.CollectionID] = position
|
|
out = append(out, collection)
|
|
}
|
|
if giftID.Valid {
|
|
out[position].GiftIDs = append(out[position].GiftIDs, giftID.Int64)
|
|
}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *StarGiftStore) CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) {
|
|
title = strings.TrimSpace(title)
|
|
if !validPostgresStarGiftOwner(owner) || title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes {
|
|
return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
var result domain.StarGiftCollection
|
|
err := withTx(ctx, s.db, "create star gift collection", func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil {
|
|
return err
|
|
}
|
|
var count int
|
|
if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2`, string(owner.Type), owner.ID).Scan(&count); err != nil {
|
|
return err
|
|
}
|
|
if count >= domain.MaxStarGiftCollectionsPerPeer {
|
|
return domain.ErrStarGiftCollectionsFull
|
|
}
|
|
ids, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, savedGiftIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result = domain.StarGiftCollection{Owner: owner, Title: title, GiftIDs: ids, SortOrder: count}
|
|
result.Hash = domain.StarGiftCollectionHash(title, ids)
|
|
if err := tx.QueryRow(ctx, `
|
|
INSERT INTO star_gift_collections(owner_peer_type, owner_peer_id, title, sort_order, hash)
|
|
VALUES ($1,$2,$3,$4,$5) RETURNING collection_id, created_at, updated_at`, string(owner.Type), owner.ID,
|
|
title, count, result.Hash).Scan(&result.CollectionID, &result.CreatedAt, &result.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
return replaceCollectionItems(ctx, tx, result.CollectionID, ids)
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func (s *StarGiftStore) UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) {
|
|
var result domain.StarGiftCollection
|
|
err := withTx(ctx, s.db, "update star gift collection", func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT title, hash, sort_order, created_at, updated_at FROM star_gift_collections
|
|
WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3 FOR UPDATE`, string(owner.Type), owner.ID, collectionID).Scan(
|
|
&result.Title, &result.Hash, &result.SortOrder, &result.CreatedAt, &result.UpdatedAt); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrStarGiftCollectionNotFound
|
|
}
|
|
return err
|
|
}
|
|
result.Owner = owner
|
|
result.CollectionID = collectionID
|
|
rows, err := tx.Query(ctx, `SELECT saved_gift_id FROM star_gift_collection_items WHERE collection_id=$1 ORDER BY sort_order, saved_gift_id`, collectionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
result.GiftIDs = append(result.GiftIDs, id)
|
|
}
|
|
rows.Close()
|
|
if patch.Title != nil {
|
|
title := strings.TrimSpace(*patch.Title)
|
|
if title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
result.Title = title
|
|
}
|
|
deleted := make(map[int64]struct{}, len(patch.DeleteIDs))
|
|
for _, id := range patch.DeleteIDs {
|
|
deleted[id] = struct{}{}
|
|
}
|
|
next := make([]int64, 0, len(result.GiftIDs)+len(patch.AddIDs))
|
|
for _, id := range result.GiftIDs {
|
|
if _, ok := deleted[id]; !ok {
|
|
next = append(next, id)
|
|
}
|
|
}
|
|
add, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, patch.AddIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
next = appendUniquePostgresIDs(next, add...)
|
|
if patch.Order != nil {
|
|
order, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, patch.Order)
|
|
if err != nil || !samePostgresIDSet(order, next) {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
next = order
|
|
}
|
|
if len(next) > domain.MaxStarGiftCollectionItems {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
result.GiftIDs = next
|
|
result.Hash = domain.StarGiftCollectionHash(result.Title, result.GiftIDs)
|
|
if err := tx.QueryRow(ctx, `
|
|
UPDATE star_gift_collections SET title=$4, hash=$5, updated_at=now()
|
|
WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3 RETURNING updated_at`,
|
|
string(owner.Type), owner.ID, collectionID, result.Title, result.Hash).Scan(&result.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
return replaceCollectionItems(ctx, tx, collectionID, result.GiftIDs)
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func (s *StarGiftStore) DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error) {
|
|
var changed bool
|
|
err := withTx(ctx, s.db, "delete star gift collection", func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil {
|
|
return err
|
|
}
|
|
tag, err := tx.Exec(ctx, `DELETE FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3`, string(owner.Type), owner.ID, collectionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changed = tag.RowsAffected() > 0
|
|
if changed {
|
|
_, err = tx.Exec(ctx, `
|
|
WITH ordered AS (
|
|
SELECT collection_id, row_number() OVER (ORDER BY sort_order, collection_id) - 1 AS next_order
|
|
FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2
|
|
)
|
|
UPDATE star_gift_collections c SET sort_order=o.next_order, updated_at=now()
|
|
FROM ordered o WHERE c.collection_id=o.collection_id`, string(owner.Type), owner.ID)
|
|
}
|
|
return err
|
|
})
|
|
return changed, err
|
|
}
|
|
|
|
func (s *StarGiftStore) ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error {
|
|
return withTx(ctx, s.db, "reorder star gift collections", func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil {
|
|
return err
|
|
}
|
|
rows, err := tx.Query(ctx, `SELECT collection_id FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2 FOR UPDATE`, string(owner.Type), owner.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing := make([]int, 0)
|
|
for rows.Next() {
|
|
var id int
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
existing = append(existing, id)
|
|
}
|
|
rows.Close()
|
|
if !samePostgresIntSet(existing, collectionIDs) {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
for order, id := range collectionIDs {
|
|
if _, err := tx.Exec(ctx, `UPDATE star_gift_collections SET sort_order=$4, updated_at=now() WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3`, string(owner.Type), owner.ID, id, order); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *StarGiftStore) SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error {
|
|
if len(savedGiftIDs) > domain.MaxPinnedStarGifts {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
return withTx(ctx, s.db, "set pinned star gifts", func(tx pgx.Tx) error {
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil {
|
|
return err
|
|
}
|
|
ids, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, savedGiftIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(ids) != len(savedGiftIDs) {
|
|
return domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=0 WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND pinned_order<>0`, string(owner.Type), owner.ID); err != nil {
|
|
return err
|
|
}
|
|
for order, id := range ids {
|
|
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=$2,unsaved=false WHERE id=$1`, id, order+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func validatePostgresCollectionGiftIDs(ctx context.Context, db sqlcgen.DBTX, owner domain.Peer, ids []int64) ([]int64, error) {
|
|
ids = dedupePostgresIDs(ids)
|
|
if len(ids) > domain.MaxStarGiftCollectionItems {
|
|
return nil, domain.ErrStarGiftCollectibleInvalid
|
|
}
|
|
if len(ids) == 0 {
|
|
return []int64{}, nil
|
|
}
|
|
rows, err := db.Query(ctx, `
|
|
SELECT id FROM peer_star_gifts
|
|
WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND lifecycle_status='active' AND id=ANY($3::bigint[])
|
|
FOR UPDATE`, string(owner.Type), owner.ID, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
found := make(map[int64]struct{}, len(ids))
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
found[id] = struct{}{}
|
|
}
|
|
if len(found) != len(ids) {
|
|
return nil, domain.ErrStarGiftNotFound
|
|
}
|
|
return ids, rows.Err()
|
|
}
|
|
|
|
// removeSavedGiftFromCollections runs under the owner advisory lock. It removes
|
|
// terminal gifts and updates every affected collection hash in bounded batches,
|
|
// so getStarGiftCollections cannot return NotModified for changed membership.
|
|
func removeSavedGiftFromCollections(ctx context.Context, tx pgx.Tx, owner domain.Peer, savedGiftID int64) error {
|
|
rows, err := tx.Query(ctx, `
|
|
SELECT c.collection_id, c.title
|
|
FROM star_gift_collections c
|
|
JOIN star_gift_collection_items i ON i.collection_id=c.collection_id
|
|
WHERE c.owner_peer_type=$1 AND c.owner_peer_id=$2 AND i.saved_gift_id=$3
|
|
ORDER BY c.collection_id
|
|
FOR UPDATE OF c`, string(owner.Type), owner.ID, savedGiftID)
|
|
if err != nil {
|
|
return fmt.Errorf("lock converted gift collections: %w", err)
|
|
}
|
|
titles := make(map[int]string)
|
|
ids := make([]int, 0)
|
|
for rows.Next() {
|
|
var id int
|
|
var title string
|
|
if err := rows.Scan(&id, &title); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
ids = append(ids, id)
|
|
titles[id] = title
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
rows.Close()
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
if _, err := tx.Exec(ctx, `DELETE FROM star_gift_collection_items WHERE saved_gift_id=$1`, savedGiftID); err != nil {
|
|
return fmt.Errorf("remove converted gift collection memberships: %w", err)
|
|
}
|
|
|
|
memberships := make(map[int][]int64, len(ids))
|
|
itemRows, err := tx.Query(ctx, `
|
|
SELECT collection_id, saved_gift_id
|
|
FROM star_gift_collection_items
|
|
WHERE collection_id=ANY($1::integer[])
|
|
ORDER BY collection_id, sort_order, saved_gift_id`, ids)
|
|
if err != nil {
|
|
return fmt.Errorf("list remaining collection memberships: %w", err)
|
|
}
|
|
for itemRows.Next() {
|
|
var collectionID int
|
|
var giftID int64
|
|
if err := itemRows.Scan(&collectionID, &giftID); err != nil {
|
|
itemRows.Close()
|
|
return err
|
|
}
|
|
memberships[collectionID] = append(memberships[collectionID], giftID)
|
|
}
|
|
if err := itemRows.Err(); err != nil {
|
|
itemRows.Close()
|
|
return err
|
|
}
|
|
itemRows.Close()
|
|
|
|
hashes := make([]int64, len(ids))
|
|
for i, collectionID := range ids {
|
|
hashes[i] = domain.StarGiftCollectionHash(titles[collectionID], memberships[collectionID])
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE star_gift_collections c SET hash=x.hash, updated_at=now()
|
|
FROM unnest($1::integer[], $2::bigint[]) AS x(collection_id, hash)
|
|
WHERE c.collection_id=x.collection_id`, ids, hashes); err != nil {
|
|
return fmt.Errorf("refresh converted gift collection hashes: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func replaceCollectionItems(ctx context.Context, tx pgx.Tx, collectionID int, ids []int64) error {
|
|
if _, err := tx.Exec(ctx, `DELETE FROM star_gift_collection_items WHERE collection_id=$1`, collectionID); err != nil {
|
|
return err
|
|
}
|
|
for order, id := range ids {
|
|
if _, err := tx.Exec(ctx, `INSERT INTO star_gift_collection_items(collection_id, saved_gift_id, sort_order) VALUES ($1,$2,$3)`, collectionID, id, order); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validPostgresStarGiftOwner(owner domain.Peer) bool {
|
|
return owner.ID > 0 && (owner.Type == domain.PeerTypeUser || owner.Type == domain.PeerTypeChannel)
|
|
}
|
|
|
|
func starGiftCollectionLockKey(owner domain.Peer) string {
|
|
return fmt.Sprintf("star_gift_collection:%s:%d", owner.Type, owner.ID)
|
|
}
|
|
|
|
func dedupePostgresIDs(ids []int64) []int64 {
|
|
out := make([]int64, 0, len(ids))
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
for _, id := range ids {
|
|
if id <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; !ok {
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendUniquePostgresIDs(dst []int64, values ...int64) []int64 {
|
|
seen := make(map[int64]struct{}, len(dst)+len(values))
|
|
for _, id := range dst {
|
|
seen[id] = struct{}{}
|
|
}
|
|
for _, id := range values {
|
|
if _, ok := seen[id]; !ok {
|
|
seen[id] = struct{}{}
|
|
dst = append(dst, id)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func samePostgresIDSet(a, b []int64) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
a = append([]int64(nil), a...)
|
|
b = append([]int64(nil), b...)
|
|
sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
|
|
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func samePostgresIntSet(a, b []int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
seen := make(map[int]struct{}, len(a))
|
|
for _, id := range a {
|
|
seen[id] = struct{}{}
|
|
}
|
|
for _, id := range b {
|
|
if _, ok := seen[id]; !ok {
|
|
return false
|
|
}
|
|
delete(seen, id)
|
|
}
|
|
return len(seen) == 0
|
|
}
|