owpengram-server/internal/seed/giftdemo/source.go
2026-07-22 20:20:16 +03:00

259 lines
7.7 KiB
Go

// Package giftdemo is an in-memory catalog of small, fully original demo Star
// Gifts (geometric Lottie authored here — not Telegram's copyrighted assets).
// It backs the admin console's "Default gifts" import source: operators can
// import these to demo the complete gift surface (upgrade + craft) without any
// third-party artwork. Nothing here is enabled automatically; gifts appear in
// the catalog only once an operator imports them.
package giftdemo
import (
"context"
"fmt"
"telesrv/internal/domain"
)
// Preparer normalizes raw Lottie bytes into the canonical Star Gift animation
// pair. *stargifts.Service satisfies it.
type Preparer interface {
PrepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error)
}
const (
seedActor = "system-default-gifts"
seedCommandID = "default-gifts-v1"
)
type attrSpec struct {
name string
spec lottieSpec
permille int // >0 for a normal (drawable) attribute
crafted bool // craft-only model; permille must be 0 and rarity named
rarity domain.StarGiftAttributeRarityKind
}
type backdropSpec struct {
name string
center int
edge int
pattern int
text int
permille int
}
type upgradeSpec struct {
upgradeStars int64
supplyTotal int
slug string
models []attrSpec
patterns []attrSpec
backdrops []backdropSpec
}
type giftSpec struct {
title string
stars int64
convert int64
base lottieSpec
limited bool
availability int
requirePremium bool
birthday bool
upgrade *upgradeSpec
}
// GiftInfo is a catalog listing entry for the import picker.
type GiftInfo struct {
ID int `json:"id"`
Title string `json:"title"`
Stars int64 `json:"stars"`
ConvertStars int64 `json:"convert_stars"`
UpgradeStars int64 `json:"upgrade_stars"`
Upgradeable bool `json:"upgradeable"`
Craftable bool `json:"craftable"`
Limited bool `json:"limited"`
Availability int `json:"availability"`
RequirePremium bool `json:"require_premium"`
ModelCount int `json:"model_count"`
PatternCount int `json:"pattern_count"`
BackdropCount int `json:"backdrop_count"`
CraftedCount int `json:"crafted_count"`
}
// List returns the built-in demo gifts, ids 1..N in display order.
func List() []GiftInfo {
gifts := demoGifts()
out := make([]GiftInfo, 0, len(gifts))
for i, spec := range gifts {
info := GiftInfo{
ID: i + 1, Title: spec.title, Stars: spec.stars, ConvertStars: spec.convert,
Limited: spec.limited, Availability: spec.availability, RequirePremium: spec.requirePremium,
}
if spec.upgrade != nil {
info.Upgradeable = true
info.UpgradeStars = spec.upgrade.upgradeStars
info.ModelCount = len(spec.upgrade.models)
info.PatternCount = len(spec.upgrade.patterns)
info.BackdropCount = len(spec.upgrade.backdrops)
for _, m := range spec.upgrade.models {
if m.crafted {
info.CraftedCount++
}
}
info.Craftable = info.CraftedCount > 0
}
out = append(out, info)
}
return out
}
func specByID(id int) (giftSpec, int, bool) {
gifts := demoGifts()
if id < 1 || id > len(gifts) {
return giftSpec{}, 0, false
}
return gifts[id-1], id - 1, true
}
// BaseAnimationJSON returns the normalized Lottie JSON of a gift's base sticker,
// used by the admin preview player.
func BaseAnimationJSON(prep Preparer, id int) ([]byte, bool, error) {
spec, _, ok := specByID(id)
if !ok {
return nil, false, nil
}
anim, err := prepare(prep, spec.title, spec.base)
if err != nil {
return nil, false, err
}
return anim.JSON, true, nil
}
// BuildBundle assembles the complete catalog+collectible write for one demo
// gift. The bundle carries no official provenance (OfficialGiftID stays 0), so
// these import as ordinary locally-authored catalog entries.
func BuildBundle(prep Preparer, id, sortOrder int) (domain.StarGiftCatalogBundleWrite, string, error) {
spec, _, ok := specByID(id)
if !ok {
return domain.StarGiftCatalogBundleWrite{}, "", fmt.Errorf("unknown demo gift id %d", id)
}
write, err := buildBundle(prep, spec, sortOrder)
if err != nil {
return domain.StarGiftCatalogBundleWrite{}, "", err
}
return write, spec.title, nil
}
// CatalogReader is the read side used to skip gifts already present by title.
type CatalogReader interface {
Catalog(ctx context.Context) ([]domain.StarGift, error)
}
// PresentTitles returns the set of demo gift titles already in the catalog, so
// callers can import idempotently.
func PresentTitles(ctx context.Context, reader CatalogReader) (map[string]struct{}, error) {
existing, err := reader.Catalog(ctx)
if err != nil {
return nil, err
}
demo := map[string]struct{}{}
for _, spec := range demoGifts() {
demo[spec.title] = struct{}{}
}
present := map[string]struct{}{}
for _, gift := range existing {
if _, ok := demo[gift.Title]; ok {
present[gift.Title] = struct{}{}
}
}
return present, nil
}
func buildBundle(prep Preparer, spec giftSpec, sortOrder int) (domain.StarGiftCatalogBundleWrite, error) {
baseAnim, err := prepare(prep, spec.title, spec.base)
if err != nil {
return domain.StarGiftCatalogBundleWrite{}, err
}
catalog := domain.StarGiftCatalogWrite{
Title: spec.title,
Stars: spec.stars,
ConvertStars: spec.convert,
Enabled: true,
SortOrder: sortOrder,
Animation: baseAnim,
Actor: seedActor,
CommandID: seedCommandID,
}
if spec.limited {
catalog.Limited = true
catalog.AvailabilityTotal = spec.availability
catalog.AvailabilityRemains = spec.availability
}
catalog.RequirePremium = spec.requirePremium
catalog.Birthday = spec.birthday
write := domain.StarGiftCatalogBundleWrite{Catalog: catalog}
if spec.upgrade == nil {
return write, nil
}
up := spec.upgrade
models, err := buildAttributes(prep, up.models, domain.StarGiftCollectibleModel)
if err != nil {
return domain.StarGiftCatalogBundleWrite{}, err
}
patterns, err := buildAttributes(prep, up.patterns, domain.StarGiftCollectiblePattern)
if err != nil {
return domain.StarGiftCatalogBundleWrite{}, err
}
backdrops := make([]domain.StarGiftCollectibleAttribute, 0, len(up.backdrops))
for i, b := range up.backdrops {
backdrops = append(backdrops, domain.StarGiftCollectibleAttribute{
Kind: domain.StarGiftCollectibleBackdrop, Name: b.name, BackdropID: i + 1,
CenterColor: b.center, EdgeColor: b.edge, PatternColor: b.pattern, TextColor: b.text,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: b.permille, SortOrder: i,
})
}
write.Collectible = &domain.StarGiftCollectibleWrite{
UpgradeStars: up.upgradeStars,
SupplyTotal: up.supplyTotal,
SlugPrefix: up.slug,
Models: models,
Patterns: patterns,
Backdrops: backdrops,
Actor: seedActor,
CommandID: seedCommandID,
}
return write, nil
}
func buildAttributes(prep Preparer, specs []attrSpec, kind domain.StarGiftCollectibleAttributeKind) ([]domain.StarGiftCollectibleAttribute, error) {
out := make([]domain.StarGiftCollectibleAttribute, 0, len(specs))
for i, s := range specs {
anim, err := prepare(prep, s.name, s.spec)
if err != nil {
return nil, err
}
attr := domain.StarGiftCollectibleAttribute{
Kind: kind, Name: s.name, SortOrder: i, Animation: &anim,
}
if s.crafted {
attr.Crafted = true
attr.RarityKind = s.rarity
attr.RarityPermille = 0
} else {
attr.RarityKind = domain.StarGiftRarityPermille
attr.RarityPermille = s.permille
}
out = append(out, attr)
}
return out, nil
}
func prepare(prep Preparer, name string, spec lottieSpec) (domain.StarGiftAnimation, error) {
data, err := renderLottie(spec)
if err != nil {
return domain.StarGiftAnimation{}, err
}
return prep.PrepareAnimation(name+".json", data)
}