feat: sync official star gift lifecycle tooling

Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling).

Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
This commit is contained in:
A 2026-07-19 03:11:35 +08:00
parent f2c2fd0236
commit 14bf7d1e20
92 changed files with 14768 additions and 727 deletions

View file

@ -14,6 +14,34 @@ import (
"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)
@ -38,13 +66,15 @@ SELECT COALESCE(MAX(revision), 0) + 1 FROM star_gift_collectible_revisions WHERE
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)
VALUES ($1,$2,$3,$4,$5,'draft',$6,$7)
RETURNING id`, write.GiftID, revision, write.UpgradeStars, write.SupplyTotal, write.SlugPrefix, write.Actor, write.CommandID).Scan(&revisionID); err != nil {
(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) error {
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)
@ -53,35 +83,53 @@ RETURNING id`, write.GiftID, revision, write.UpgradeStars, write.SupplyTotal, wr
return fmt.Errorf("put collectible %s blob: %w", attribute.Kind, err)
}
animation := attribute.Animation
query := fmt.Sprintf(`
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_permille, sort_order)
VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, 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,
attribute.RarityPermille, attribute.SortOrder); err != nil {
return fmt.Errorf("insert collectible %s attribute: %w", attribute.Kind, err)
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); err != nil {
if err := insertAnimated("star_gift_collectible_models", write.Models, true); err != nil {
return err
}
if err := insertAnimated("star_gift_collectible_patterns", write.Patterns); err != nil {
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_permille, sort_order)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, revisionID, strings.TrimSpace(attribute.Name), attribute.BackdropID,
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,
attribute.RarityPermille, attribute.SortOrder); err != nil {
string(attribute.RarityKind), nullablePermille(attribute), attribute.SortOrder); err != nil {
return fmt.Errorf("insert collectible backdrop: %w", err)
}
}
@ -152,10 +200,11 @@ func collectibleRevisionByID(ctx context.Context, db sqlcgen.DBTX, revisionID in
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
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)
}
@ -184,13 +233,20 @@ func listAnimatedCollectibleAttributes(ctx context.Context, db sqlcgen.DBTX, rev
return nil, domain.ErrStarGiftCollectibleInvalid
}
rows, err := db.Query(ctx, fmt.Sprintf(`
SELECT a.id, a.collectible_revision_id, a.name, a.rarity_permille, a.sort_order,
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,
a.animation_json::text, 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 a JOIN documents d ON d.id=a.document_id
WHERE a.collectible_revision_id=$1 ORDER BY a.sort_order, a.id`, table), revisionID)
WHERE a.collectible_revision_id=$1 ORDER BY a.sort_order, a.id`,
func() string {
if kind == domain.StarGiftCollectibleModel {
return "a.crafted"
}
return "false"
}(), table), revisionID)
if err != nil {
return nil, fmt.Errorf("list collectible %s attributes: %w", kind, err)
}
@ -199,7 +255,8 @@ WHERE a.collectible_revision_id=$1 ORDER BY a.sort_order, a.id`, table), revisio
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.RarityPermille, &attribute.SortOrder,
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,
@ -221,7 +278,7 @@ WHERE a.collectible_revision_id=$1 ORDER BY a.sort_order, a.id`, table), revisio
func listCollectibleBackdrops(ctx context.Context, db sqlcgen.DBTX, revisionID int64) ([]domain.StarGiftCollectibleAttribute, error) {
rows, err := db.Query(ctx, `
SELECT id, collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color,
text_color, rarity_permille, sort_order
text_color, rarity_kind, COALESCE(rarity_permille,0), sort_order
FROM star_gift_collectible_backdrops WHERE collectible_revision_id=$1 ORDER BY sort_order, id`, revisionID)
if err != nil {
return nil, fmt.Errorf("list collectible backdrops: %w", err)
@ -232,7 +289,7 @@ FROM star_gift_collectible_backdrops WHERE collectible_revision_id=$1 ORDER BY s
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.RarityPermille, &attribute.SortOrder); err != nil {
&attribute.RarityKind, &attribute.RarityPermille, &attribute.SortOrder); err != nil {
return nil, err
}
out = append(out, attribute)
@ -307,14 +364,24 @@ func (s *StarGiftStore) uniqueByPredicate(ctx context.Context, predicate string,
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,
u.owner_peer_type, u.owner_peer_id, u.keep_original_details, u.created_at,
r.issued, r.supply_total, sg.from_user_id, sg.owner_peer_type, sg.owner_peer_id,
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_permille, md.id, md.access_hash, md.file_reference, md.date,
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_permille, pd.id, pd.access_hash, pd.file_reference, pd.date,
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_permille
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
@ -323,12 +390,14 @@ 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 string
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
@ -337,23 +406,40 @@ func scanUniqueStarGift(row rowScanner) (domain.UniqueStarGift, error) {
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.AvailabilityIssued, &unique.AvailabilityTotal,
&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.RarityPermille,
&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.RarityPermille,
&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.RarityPermille); err != nil {
&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
@ -603,7 +689,7 @@ func validatePostgresCollectionGiftIDs(ctx context.Context, db sqlcgen.DBTX, own
}
rows, err := db.Query(ctx, `
SELECT id FROM peer_star_gifts
WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND NOT converted AND id=ANY($3::bigint[])
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