removed default telegram gifts
This commit is contained in:
parent
139967399d
commit
b1836c78e8
25109 changed files with 1040 additions and 757922 deletions
|
|
@ -1,83 +0,0 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
stargiftapp "telesrv/internal/app/stargifts"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/officialgifts"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
// This opt-in regression uses the exact official snapshot reported by the
|
||||
// Party Sparkler import issue. It crosses official catalog verification,
|
||||
// admin mapping, animation materialization and the complete store validator.
|
||||
func TestConfiguredOfficialPartySparklerImport(t *testing.T) {
|
||||
root := os.Getenv("TELESRV_TEST_OFFICIAL_GIFTS_DIR")
|
||||
if root == "" {
|
||||
t.Skip("TELESRV_TEST_OFFICIAL_GIFTS_DIR is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
giftService := stargiftapp.NewService(memory.NewStarGiftStore(), &adminGiftBlob{data: map[string][]byte{}}, 2)
|
||||
svc := NewService(Dependencies{
|
||||
Commands: newMemoryCommandRepo(),
|
||||
Gifts: giftService,
|
||||
OfficialGifts: officialgifts.New(root),
|
||||
Now: fixedNow,
|
||||
})
|
||||
result, err := svc.ImportOfficialStarGift(ctx, ImportOfficialStarGiftRequest{
|
||||
CommandMeta: CommandMeta{
|
||||
CommandID: "exec-party-sparkler-snapshot-regression",
|
||||
Actor: "test",
|
||||
Reason: "verify official collectible import",
|
||||
},
|
||||
SourceGiftID: "6003643167683903930",
|
||||
Enabled: true,
|
||||
IncludeCollectible: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("import Party Sparkler: result=%+v err=%v", result, err)
|
||||
}
|
||||
if result.Details["models"] != 100 || result.Details["patterns"] != 136 || result.Details["backdrops"] != 60 {
|
||||
t.Fatalf("Party Sparkler details = %+v", result.Details)
|
||||
}
|
||||
catalog, err := giftService.Catalog(ctx)
|
||||
if err != nil || len(catalog) != 1 {
|
||||
t.Fatalf("catalog=%+v err=%v, want one gift", catalog, err)
|
||||
}
|
||||
preview, ok, err := giftService.CollectiblePreview(ctx, catalog[0].ID)
|
||||
if err != nil || !ok || len(preview.Models) != 100 || len(preview.Patterns) != 136 || len(preview.Backdrops) != 60 {
|
||||
t.Fatalf("preview counts=%d/%d/%d ok=%v err=%v", len(preview.Models), len(preview.Patterns), len(preview.Backdrops), ok, err)
|
||||
}
|
||||
for _, model := range preview.Models {
|
||||
if model.Document == nil || !model.Document.IsSticker() || model.Document.IsCustomEmoji() {
|
||||
t.Fatalf("model %q document=%+v, want ordinary sticker", model.Name, model.Document)
|
||||
}
|
||||
}
|
||||
for _, pattern := range preview.Patterns {
|
||||
if pattern.Document == nil || pattern.Document.IsSticker() || !pattern.Document.IsCustomEmoji() ||
|
||||
!hasTextColorCustomEmoji(pattern.Document.Attributes) || !hasInlinePathThumb(pattern.Document.Thumbs) {
|
||||
t.Fatalf("pattern %q document=%+v, want text-color custom emoji with inline path", pattern.Name, pattern.Document)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasTextColorCustomEmoji(attributes []domain.DocumentAttribute) bool {
|
||||
for _, attribute := range attributes {
|
||||
if attribute.Kind == domain.DocAttrCustomEmoji && attribute.TextColor {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasInlinePathThumb(thumbs []domain.PhotoSize) bool {
|
||||
for _, thumb := range thumbs {
|
||||
if thumb.Kind == domain.PhotoSizeKindPath && thumb.Type != "" && len(thumb.Bytes) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
|
@ -19,31 +18,31 @@ import (
|
|||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/officialgifts"
|
||||
"telesrv/internal/seed/giftdemo"
|
||||
)
|
||||
|
||||
const (
|
||||
ActionSetAccountFrozen = "account.set_frozen"
|
||||
ActionGrantPremium = "account.grant_premium"
|
||||
ActionGrantStars = "account.grant_stars"
|
||||
ActionSetVerified = "account.set_verified"
|
||||
ActionSetChannelVerified = "channel.set_verified"
|
||||
ActionRevokeSessions = "account.revoke_sessions"
|
||||
ActionDeletePrivateMessages = "messages.delete_private_messages"
|
||||
ActionDeletePrivateHistory = "messages.delete_private_history"
|
||||
ActionImportStarGift = "gifts.import"
|
||||
ActionImportOfficialStarGift = "gifts.official.import"
|
||||
ActionImportAllOfficialStarGifts = "gifts.official.import_all"
|
||||
ActionPublishGiftCollectibles = "gifts.collectibles.publish"
|
||||
ActionSetStarGiftEnabled = "gifts.set_enabled"
|
||||
ActionSetStarGiftSortOrder = "gifts.set_sort_order"
|
||||
ActionSetStickerSetArchived = "stickers.set_archived"
|
||||
ActionSetStickerSetSortOrder = "stickers.set_sort_order"
|
||||
ActionRenameStickerSet = "stickers.rename"
|
||||
ActionDeleteStickerSet = "stickers.delete"
|
||||
ActionCreateStickerSet = "stickers.create"
|
||||
ActionAddStickerToSet = "stickers.add_sticker"
|
||||
ActionRemoveStickerFromSet = "stickers.remove_sticker"
|
||||
ActionSetAccountFrozen = "account.set_frozen"
|
||||
ActionGrantPremium = "account.grant_premium"
|
||||
ActionGrantStars = "account.grant_stars"
|
||||
ActionSetVerified = "account.set_verified"
|
||||
ActionSetChannelVerified = "channel.set_verified"
|
||||
ActionRevokeSessions = "account.revoke_sessions"
|
||||
ActionDeletePrivateMessages = "messages.delete_private_messages"
|
||||
ActionDeletePrivateHistory = "messages.delete_private_history"
|
||||
ActionImportStarGift = "gifts.import"
|
||||
ActionImportDefaultStarGift = "gifts.default.import"
|
||||
ActionImportAllDefaultStarGifts = "gifts.default.import_all"
|
||||
ActionPublishGiftCollectibles = "gifts.collectibles.publish"
|
||||
ActionSetStarGiftEnabled = "gifts.set_enabled"
|
||||
ActionSetStarGiftSortOrder = "gifts.set_sort_order"
|
||||
ActionSetStickerSetArchived = "stickers.set_archived"
|
||||
ActionSetStickerSetSortOrder = "stickers.set_sort_order"
|
||||
ActionRenameStickerSet = "stickers.rename"
|
||||
ActionDeleteStickerSet = "stickers.delete"
|
||||
ActionCreateStickerSet = "stickers.create"
|
||||
ActionAddStickerToSet = "stickers.add_sticker"
|
||||
ActionRemoveStickerFromSet = "stickers.remove_sticker"
|
||||
|
||||
maxCommandIDLength = 128
|
||||
maxActorLength = 128
|
||||
|
|
@ -110,7 +109,7 @@ type MessagesService interface {
|
|||
|
||||
type GiftsService interface {
|
||||
PrepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error)
|
||||
PrepareOfficialAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error)
|
||||
Catalog(ctx context.Context) ([]domain.StarGift, error)
|
||||
CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error)
|
||||
CreateCatalogBundle(ctx context.Context, write domain.StarGiftCatalogBundleWrite) (domain.StarGiftCatalogBundleResult, error)
|
||||
SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error)
|
||||
|
|
@ -121,11 +120,6 @@ type GiftsService interface {
|
|||
CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error)
|
||||
}
|
||||
|
||||
type OfficialGiftsSource interface {
|
||||
List(ctx context.Context) ([]officialgifts.GiftSummary, error)
|
||||
Bundle(ctx context.Context, giftID int64, includeCollectible bool) (officialgifts.Bundle, error)
|
||||
}
|
||||
|
||||
// AvatarResolver is the same shape as internal/web's ProfilePhotoResolver, kept as its
|
||||
// own local interface (rather than importing internal/web) since only this narrow slice
|
||||
// is needed to serve an account's current profile photo in the admin console.
|
||||
|
|
@ -164,7 +158,6 @@ type Dependencies struct {
|
|||
ChannelNotifier ChannelNotifier
|
||||
Messages MessagesService
|
||||
Gifts GiftsService
|
||||
OfficialGifts OfficialGiftsSource
|
||||
Photos AvatarResolver
|
||||
StickerSets StickerSetsService
|
||||
Now func() time.Time
|
||||
|
|
@ -183,7 +176,6 @@ type Service struct {
|
|||
channelNotifier ChannelNotifier
|
||||
messages MessagesService
|
||||
gifts GiftsService
|
||||
officialGifts OfficialGiftsSource
|
||||
photos AvatarResolver
|
||||
stickerSets StickerSetsService
|
||||
now func() time.Time
|
||||
|
|
@ -231,9 +223,6 @@ func (s *Service) Configure(deps Dependencies) *Service {
|
|||
if deps.Gifts != nil {
|
||||
s.gifts = deps.Gifts
|
||||
}
|
||||
if deps.OfficialGifts != nil {
|
||||
s.officialGifts = deps.OfficialGifts
|
||||
}
|
||||
if deps.Photos != nil {
|
||||
s.photos = deps.Photos
|
||||
}
|
||||
|
|
@ -282,21 +271,13 @@ type ImportStarGiftRequest struct {
|
|||
Data []byte `json:"-"`
|
||||
}
|
||||
|
||||
type ImportOfficialStarGiftRequest struct {
|
||||
type ImportDefaultStarGiftRequest struct {
|
||||
CommandMeta
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type ImportAllDefaultStarGiftsRequest struct {
|
||||
CommandMeta
|
||||
SourceGiftID string `json:"source_gift_id"`
|
||||
GiftID int64 `json:"gift_id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Stars int64 `json:"stars"`
|
||||
ConvertStars int64 `json:"convert_stars"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IncludeCollectible bool `json:"include_collectible"`
|
||||
UpgradeStars int64 `json:"upgrade_stars,omitempty"`
|
||||
SupplyTotal int `json:"supply_total,omitempty"`
|
||||
SlugPrefix string `json:"slug_prefix,omitempty"`
|
||||
ManifestSHA256 string `json:"manifest_sha256,omitempty"`
|
||||
AssetSHA256 []string `json:"asset_sha256,omitempty"`
|
||||
}
|
||||
|
||||
type SetStarGiftEnabledRequest struct {
|
||||
|
|
@ -950,11 +931,9 @@ func (s *Service) ImportStarGift(ctx context.Context, req ImportStarGiftRequest)
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) OfficialStarGifts(ctx context.Context) ([]officialgifts.GiftSummary, error) {
|
||||
if s == nil || s.officialGifts == nil {
|
||||
return nil, officialgifts.ErrUnavailable
|
||||
}
|
||||
return s.officialGifts.List(ctx)
|
||||
// DefaultStarGifts lists the built-in original demo gifts available to import.
|
||||
func (s *Service) DefaultStarGifts() []giftdemo.GiftInfo {
|
||||
return giftdemo.List()
|
||||
}
|
||||
|
||||
const maxAccountAvatarBytes = 4 << 20
|
||||
|
|
@ -1057,261 +1036,91 @@ func safeAccountImageType(value string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) OfficialStarGiftAnimation(ctx context.Context, sourceGiftID string) ([]byte, bool, error) {
|
||||
if s == nil || s.officialGifts == nil || s.gifts == nil {
|
||||
return nil, false, officialgifts.ErrUnavailable
|
||||
// DefaultStarGiftAnimation returns the base sticker animation JSON for a
|
||||
// built-in demo gift, used by the admin preview player.
|
||||
func (s *Service) DefaultStarGiftAnimation(_ context.Context, id int) ([]byte, bool, error) {
|
||||
if s == nil || s.gifts == nil {
|
||||
return nil, false, fmt.Errorf("gift service is not configured")
|
||||
}
|
||||
id, err := strconv.ParseInt(strings.TrimSpace(sourceGiftID), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, false, officialgifts.ErrNotFound
|
||||
}
|
||||
bundle, err := s.officialGifts.Bundle(ctx, id, false)
|
||||
if errors.Is(err, officialgifts.ErrNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
animation, err := s.gifts.PrepareOfficialAnimation(bundle.BaseDocument.FileName, bundle.BaseDocument.Data)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return animation.JSON, true, nil
|
||||
return giftdemo.BaseAnimationJSON(s.gifts, id)
|
||||
}
|
||||
|
||||
func (s *Service) ImportOfficialStarGift(ctx context.Context, req ImportOfficialStarGiftRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifts == nil || s.officialGifts == nil {
|
||||
return CommandResult{}, fmt.Errorf("official star gift importer is not configured")
|
||||
// ImportDefaultStarGift imports one built-in original demo gift (complete with
|
||||
// its collectible pool when upgradeable). Idempotent: a gift whose title is
|
||||
// already in the catalog is skipped rather than duplicated.
|
||||
func (s *Service) ImportDefaultStarGift(ctx context.Context, req ImportDefaultStarGiftRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifts == nil {
|
||||
return CommandResult{}, fmt.Errorf("gift service is not configured")
|
||||
}
|
||||
sourceID, err := strconv.ParseInt(strings.TrimSpace(req.SourceGiftID), 10, 64)
|
||||
if err != nil || sourceID <= 0 || req.GiftID < 0 || req.SortOrder < math.MinInt32 || req.SortOrder > math.MaxInt32 {
|
||||
write, title, err := giftdemo.BuildBundle(s.gifts, req.ID, req.ID)
|
||||
if err != nil {
|
||||
return CommandResult{}, domain.ErrStarGiftInvalid
|
||||
}
|
||||
bundle, err := s.officialGifts.Bundle(ctx, sourceID, req.IncludeCollectible)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
if req.Title = strings.TrimSpace(req.Title); req.Title == "" {
|
||||
req.Title = strings.TrimSpace(bundle.Gift.Title)
|
||||
if req.Title == "" {
|
||||
req.Title = "Official gift " + req.SourceGiftID
|
||||
}
|
||||
}
|
||||
if req.Stars <= 0 {
|
||||
req.Stars = bundle.Gift.Stars
|
||||
}
|
||||
if req.ConvertStars < 0 || req.ConvertStars > req.Stars || len([]rune(req.Title)) > domain.MaxStarGiftTitleRunes {
|
||||
return CommandResult{}, domain.ErrStarGiftInvalid
|
||||
}
|
||||
if req.UpgradeStars <= 0 {
|
||||
req.UpgradeStars = bundle.Gift.UpgradeStars
|
||||
}
|
||||
if req.SupplyTotal <= 0 {
|
||||
req.SupplyTotal = bundle.Gift.AvailabilityTotal
|
||||
}
|
||||
if req.SlugPrefix = strings.ToLower(strings.TrimSpace(req.SlugPrefix)); req.SlugPrefix == "" {
|
||||
req.SlugPrefix = "official-" + req.SourceGiftID
|
||||
}
|
||||
|
||||
baseAnimation, err := s.gifts.PrepareOfficialAnimation(bundle.BaseDocument.FileName, bundle.BaseDocument.Data)
|
||||
if err != nil {
|
||||
return CommandResult{}, fmt.Errorf("prepare official gift animation: %w", err)
|
||||
}
|
||||
assetHashes := []string{bundle.BaseDocument.SHA256}
|
||||
rarityCounts := map[string]int{}
|
||||
var background *domain.StarGiftBackground
|
||||
if bundle.Gift.Background != nil {
|
||||
background = &domain.StarGiftBackground{
|
||||
CenterColor: bundle.Gift.Background.CenterColor,
|
||||
EdgeColor: bundle.Gift.Background.EdgeColor,
|
||||
TextColor: bundle.Gift.Background.TextColor,
|
||||
}
|
||||
}
|
||||
var collectible *domain.StarGiftCollectibleWrite
|
||||
if req.IncludeCollectible {
|
||||
if bundle.Collectible == nil {
|
||||
return CommandResult{}, domain.ErrStarGiftCollectibleInvalid
|
||||
}
|
||||
modelNames := map[string]int{}
|
||||
models := make([]domain.StarGiftCollectibleAttribute, 0, len(bundle.Collectible.Models))
|
||||
for index, value := range bundle.Collectible.Models {
|
||||
animation, err := s.gifts.PrepareOfficialAnimation(value.Document.FileName, value.Document.Data)
|
||||
if err != nil {
|
||||
return CommandResult{}, fmt.Errorf("prepare official model %q: %w", value.Name, err)
|
||||
}
|
||||
rarityKind, permille, err := officialRarity(value.Rarity)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
models = append(models, domain.StarGiftCollectibleAttribute{Kind: domain.StarGiftCollectibleModel,
|
||||
Name: dedupeCollectibleAttributeName(modelNames, strings.TrimSpace(value.Name)), RarityKind: rarityKind, RarityPermille: permille,
|
||||
Crafted: value.Crafted, OfficialDocumentID: value.DocumentID, SortOrder: index, Animation: &animation})
|
||||
assetHashes = append(assetHashes, value.Document.SHA256)
|
||||
rarityCounts[string(rarityKind)]++
|
||||
}
|
||||
patternNames := map[string]int{}
|
||||
patterns := make([]domain.StarGiftCollectibleAttribute, 0, len(bundle.Collectible.Patterns))
|
||||
for index, value := range bundle.Collectible.Patterns {
|
||||
animation, err := s.gifts.PrepareOfficialAnimation(value.Document.FileName, value.Document.Data)
|
||||
if err != nil {
|
||||
return CommandResult{}, fmt.Errorf("prepare official pattern %q: %w", value.Name, err)
|
||||
}
|
||||
rarityKind, permille, err := officialRarity(value.Rarity)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
patterns = append(patterns, domain.StarGiftCollectibleAttribute{Kind: domain.StarGiftCollectiblePattern,
|
||||
Name: dedupeCollectibleAttributeName(patternNames, strings.TrimSpace(value.Name)), RarityKind: rarityKind, RarityPermille: permille,
|
||||
OfficialDocumentID: value.DocumentID, SortOrder: index, Animation: &animation})
|
||||
assetHashes = append(assetHashes, value.Document.SHA256)
|
||||
rarityCounts[string(rarityKind)]++
|
||||
}
|
||||
backdropNames := map[string]int{}
|
||||
backdrops := make([]domain.StarGiftCollectibleAttribute, 0, len(bundle.Collectible.Backdrops))
|
||||
for index, value := range bundle.Collectible.Backdrops {
|
||||
rarityKind, permille, err := officialRarity(value.Rarity)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
backdrops = append(backdrops, domain.StarGiftCollectibleAttribute{Kind: domain.StarGiftCollectibleBackdrop,
|
||||
Name: dedupeCollectibleAttributeName(backdropNames, strings.TrimSpace(value.Name)), BackdropID: value.BackdropID, CenterColor: value.CenterColor,
|
||||
EdgeColor: value.EdgeColor, PatternColor: value.PatternColor, TextColor: value.TextColor,
|
||||
RarityKind: rarityKind, RarityPermille: permille, SortOrder: index})
|
||||
rarityCounts[string(rarityKind)]++
|
||||
}
|
||||
collectible = &domain.StarGiftCollectibleWrite{GiftID: req.GiftID, UpgradeStars: req.UpgradeStars,
|
||||
SupplyTotal: req.SupplyTotal, SlugPrefix: req.SlugPrefix, Models: models, Patterns: patterns, Backdrops: backdrops,
|
||||
Actor: req.Actor, CommandID: req.CommandID, OfficialGiftID: sourceID,
|
||||
SourceManifestSHA256: append([]byte(nil), bundle.ManifestSHA256...)}
|
||||
validation := *collectible
|
||||
if validation.GiftID == 0 {
|
||||
validation.GiftID = 1
|
||||
}
|
||||
if err := domain.ValidateStarGiftCollectibleDraft(validation); err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
}
|
||||
req.ManifestSHA256 = hex.EncodeToString(bundle.ManifestSHA256)
|
||||
sort.Strings(assetHashes)
|
||||
req.AssetSHA256 = assetHashes
|
||||
write := domain.StarGiftCatalogBundleWrite{Catalog: domain.StarGiftCatalogWrite{
|
||||
GiftID: req.GiftID, Title: req.Title, Stars: req.Stars, ConvertStars: req.ConvertStars,
|
||||
Enabled: req.Enabled, SortOrder: req.SortOrder, Animation: baseAnimation, Actor: req.Actor, CommandID: req.CommandID,
|
||||
OfficialGiftID: sourceID, SourceManifestSHA256: append([]byte(nil), bundle.ManifestSHA256...),
|
||||
OfficialSourceJSON: append([]byte(nil), bundle.SourceJSON...),
|
||||
// The snapshot describes Telegram's global market, not this deployment's
|
||||
// inventory. Keep the complete source JSON as provenance, while publishing
|
||||
// regular official imports as a fresh, locally purchasable catalog entry.
|
||||
// Local resale counters and sale dates are derived by lifecycle writes.
|
||||
// Auction gifts are the one exception: star_gift_catalog_revision_auction_check
|
||||
// requires limited=true whenever auction=true, so it can't be forced false here.
|
||||
Limited: bundle.Gift.Auction, SoldOut: false, Birthday: bundle.Gift.Birthday,
|
||||
RequirePremium: bundle.Gift.RequirePremium, LimitedPerUser: bundle.Gift.LimitedPerUser,
|
||||
PeerColorAvailable: bundle.Gift.PeerColorAvailable, Auction: bundle.Gift.Auction,
|
||||
AvailabilityRemains: 0, AvailabilityTotal: 0,
|
||||
AvailabilityResale: 0, FirstSaleDate: 0,
|
||||
LastSaleDate: 0, ResellMinStars: 0,
|
||||
PerUserTotal: bundle.Gift.PerUserTotal, LockedUntilDate: bundle.Gift.LockedUntilDate,
|
||||
AuctionSlug: bundle.Gift.AuctionSlug, GiftsPerRound: bundle.Gift.GiftsPerRound,
|
||||
AuctionStartDate: bundle.Gift.AuctionStartDate, UpgradeVariants: bundle.Gift.UpgradeVariants,
|
||||
Background: background,
|
||||
}, Collectible: collectible}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionImportOfficialStarGift, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{"source_gift_id": req.SourceGiftID, "gift_id": strconv.FormatInt(req.GiftID, 10),
|
||||
"manifest_sha256": req.ManifestSHA256, "title": req.Title, "stars": strconv.FormatInt(req.Stars, 10),
|
||||
"convert_stars": strconv.FormatInt(req.ConvertStars, 10), "include_collectible": req.IncludeCollectible,
|
||||
"verified_asset_count": len(assetHashes), "rarity_counts": rarityCounts,
|
||||
"official_limited": bundle.Gift.Limited, "official_sold_out": bundle.Gift.SoldOut,
|
||||
"official_auction": bundle.Gift.Auction, "official_birthday": bundle.Gift.Birthday,
|
||||
"official_require_premium": bundle.Gift.RequirePremium,
|
||||
"official_availability_remains": bundle.Gift.AvailabilityRemains,
|
||||
"official_availability_total": bundle.Gift.AvailabilityTotal,
|
||||
"official_availability_resale": bundle.Gift.AvailabilityResale,
|
||||
}
|
||||
if bundle.Collectible != nil {
|
||||
details["models"] = len(bundle.Collectible.Models)
|
||||
details["patterns"] = len(bundle.Collectible.Patterns)
|
||||
details["backdrops"] = len(bundle.Collectible.Backdrops)
|
||||
crafted := 0
|
||||
for _, model := range bundle.Collectible.Models {
|
||||
if model.Crafted {
|
||||
crafted++
|
||||
}
|
||||
}
|
||||
details["crafted_models"] = crafted
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionImportDefaultStarGift, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{
|
||||
"id": req.ID, "title": title,
|
||||
"stars": strconv.FormatInt(write.Catalog.Stars, 10),
|
||||
"upgradeable": write.Collectible != nil,
|
||||
}
|
||||
if req.DryRun {
|
||||
return CommandResult{Message: "official star gift bundle validated", Details: details}, nil
|
||||
return CommandResult{Message: "default gift validated", Details: details}, nil
|
||||
}
|
||||
present, err := giftdemo.PresentTitles(ctx, s.gifts)
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
if _, ok := present[title]; ok {
|
||||
details["skipped"] = true
|
||||
return CommandResult{Message: "default gift already present", Details: details}, nil
|
||||
}
|
||||
result, err := s.gifts.CreateCatalogBundle(ctx, write)
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
details["gift_id"] = strconv.FormatInt(result.Catalog.Gift.ID, 10)
|
||||
details["catalog_revision_id"] = strconv.FormatInt(result.Catalog.Gift.RevisionID, 10)
|
||||
if result.Collectible != nil {
|
||||
details["collectible_revision_id"] = strconv.FormatInt(result.Collectible.ID, 10)
|
||||
details["collectible_revision"] = result.Collectible.Revision
|
||||
}
|
||||
return CommandResult{Message: "official star gift bundle imported", Details: details}, nil
|
||||
return CommandResult{Message: "default gift imported", Details: details}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type ImportAllOfficialStarGiftsRequest struct {
|
||||
CommandMeta
|
||||
}
|
||||
|
||||
// ImportAllOfficialStarGifts imports every gift in the official snapshot, one
|
||||
// ImportOfficialStarGift call each, all inside the single admin command this
|
||||
// request itself represents (so it gets the same dry-run/confirm handling
|
||||
// and audit trail as every other admin action; DryRun previews only report
|
||||
// the candidate count and do not touch the catalog). Each per-gift call gets
|
||||
// a CommandID stable across separate confirmed runs of this action
|
||||
// (bulk-official-gift-<source id>), so re-running the batch later is safe:
|
||||
// gifts already imported by a prior run replay their cached result
|
||||
// (CommandResult.AlreadyExecuted) instead of writing a duplicate catalog
|
||||
// entry — the catalog table has no unique constraint on official_gift_id, so
|
||||
// without this the same gift could otherwise be imported twice.
|
||||
func (s *Service) ImportAllOfficialStarGifts(ctx context.Context, req ImportAllOfficialStarGiftsRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifts == nil || s.officialGifts == nil {
|
||||
return CommandResult{}, fmt.Errorf("official star gift importer is not configured")
|
||||
// ImportAllDefaultStarGifts imports every built-in demo gift, one
|
||||
// ImportDefaultStarGift call each, inside this single admin command. Each
|
||||
// per-gift call uses a CommandID stable across confirmed runs
|
||||
// (bulk-default-gift-<id>), so re-running replays cached results instead of
|
||||
// duplicating catalog entries; already-present gifts are counted as skipped.
|
||||
func (s *Service) ImportAllDefaultStarGifts(ctx context.Context, req ImportAllDefaultStarGiftsRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifts == nil {
|
||||
return CommandResult{}, fmt.Errorf("gift service is not configured")
|
||||
}
|
||||
items, err := s.officialGifts.List(ctx)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionImportAllOfficialStarGifts, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
items := giftdemo.List()
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionImportAllDefaultStarGifts, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{"total": len(items)}
|
||||
if req.DryRun {
|
||||
details["note"] = "dry run does not import; confirming imports all, skipping gifts already imported by a prior run"
|
||||
return CommandResult{
|
||||
Message: fmt.Sprintf("%d official gifts available to import", len(items)),
|
||||
Details: details,
|
||||
}, nil
|
||||
details["note"] = "dry run does not import; confirming imports all, skipping gifts already present"
|
||||
return CommandResult{Message: fmt.Sprintf("%d default gifts available to import", len(items)), Details: details}, nil
|
||||
}
|
||||
imported, skipped, failed := 0, 0, 0
|
||||
perGift := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
perReq := ImportOfficialStarGiftRequest{
|
||||
perReq := ImportDefaultStarGiftRequest{
|
||||
CommandMeta: CommandMeta{
|
||||
CommandID: fmt.Sprintf("bulk-official-gift-%d", item.ID),
|
||||
CommandID: fmt.Sprintf("bulk-default-gift-%d", item.ID),
|
||||
Actor: req.Actor,
|
||||
Reason: req.Reason,
|
||||
},
|
||||
SourceGiftID: strconv.FormatInt(item.ID, 10),
|
||||
// Every attribute the snapshot has for an upgradeable gift is
|
||||
// worth importing; CanUpgrade() is exactly the precondition
|
||||
// ImportOfficialStarGift enforces for IncludeCollectible.
|
||||
IncludeCollectible: item.CanUpgrade(),
|
||||
ID: item.ID,
|
||||
}
|
||||
result, opErr := s.ImportOfficialStarGift(ctx, perReq)
|
||||
entry := map[string]any{"source_gift_id": perReq.SourceGiftID, "status": result.Status}
|
||||
if opErr != nil {
|
||||
result, opErr := s.ImportDefaultStarGift(ctx, perReq)
|
||||
entry := map[string]any{"id": item.ID, "title": item.Title, "status": result.Status}
|
||||
switch {
|
||||
case opErr != nil:
|
||||
failed++
|
||||
entry["error"] = opErr.Error()
|
||||
} else if result.AlreadyExecuted {
|
||||
case result.AlreadyExecuted, result.Details["skipped"] == true:
|
||||
skipped++
|
||||
} else {
|
||||
default:
|
||||
imported++
|
||||
entry["gift_id"] = result.Details["gift_id"]
|
||||
}
|
||||
|
|
@ -1322,44 +1131,12 @@ func (s *Service) ImportAllOfficialStarGifts(ctx context.Context, req ImportAllO
|
|||
details["failed"] = failed
|
||||
details["gifts"] = perGift
|
||||
return CommandResult{
|
||||
Message: fmt.Sprintf("imported %d, skipped %d, failed %d of %d official gifts", imported, skipped, failed, len(items)),
|
||||
Message: fmt.Sprintf("imported %d, skipped %d, failed %d of %d default gifts", imported, skipped, failed, len(items)),
|
||||
Details: details,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// dedupeCollectibleAttributeName disambiguates attribute names within one kind (models,
|
||||
// patterns, or backdrops each need distinct names per collectible_revision — see the
|
||||
// star_gift_collectible_{model,pattern,backdrop}_name_uniq constraints). Official Telegram
|
||||
// data legitimately reuses a display name across two distinct attributes of the same kind
|
||||
// (seen in practice: two different "Strawberry" models on one gift), which the DB would
|
||||
// otherwise reject outright on insert.
|
||||
func dedupeCollectibleAttributeName(seen map[string]int, name string) string {
|
||||
key := strings.ToLower(name)
|
||||
seen[key]++
|
||||
if seen[key] == 1 {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%s (%d)", name, seen[key])
|
||||
}
|
||||
|
||||
func officialRarity(value officialgifts.Rarity) (domain.StarGiftAttributeRarityKind, int, error) {
|
||||
kind := domain.StarGiftAttributeRarityKind(strings.ToLower(strings.TrimSpace(value.Kind)))
|
||||
if !kind.Valid() {
|
||||
return "", 0, domain.ErrStarGiftCollectibleInvalid
|
||||
}
|
||||
if kind == domain.StarGiftRarityPermille {
|
||||
if value.Permille == nil || *value.Permille <= 0 || *value.Permille > 1000 {
|
||||
return "", 0, domain.ErrStarGiftCollectibleInvalid
|
||||
}
|
||||
return kind, *value.Permille, nil
|
||||
}
|
||||
if value.Permille != nil {
|
||||
return "", 0, domain.ErrStarGiftCollectibleInvalid
|
||||
}
|
||||
return kind, 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) PublishStarGiftCollectibles(ctx context.Context, req PublishStarGiftCollectiblesRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifts == nil {
|
||||
return CommandResult{}, fmt.Errorf("star gift service is not configured")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
|
@ -13,7 +12,6 @@ import (
|
|||
|
||||
stargiftapp "telesrv/internal/app/stargifts"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/officialgifts"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
|
|
@ -757,112 +755,72 @@ func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testing.T) {
|
||||
permille := 922
|
||||
source := &fakeOfficialGiftsSource{bundle: officialgifts.Bundle{
|
||||
ManifestSHA256: bytesOf(0x42, 32),
|
||||
SourceJSON: []byte(`{"id":5170145012310081615,"limited":true,"sold_out":true,"availability_total":10,"availability_resale":4}`),
|
||||
Gift: officialgifts.Gift{
|
||||
ID: 5170145012310081615, Stars: 50, ConvertStars: 25, UpgradeStars: 100, DocumentID: 1,
|
||||
Limited: true, SoldOut: true, AvailabilityTotal: 10, AvailabilityRemains: 0,
|
||||
AvailabilityResale: 4, FirstSaleDate: 100, LastSaleDate: 200, ResellMinStars: 75,
|
||||
},
|
||||
BaseDocument: officialgifts.Document{ID: 1, FileName: "gift.tgs", SHA256: strings.Repeat("a", 64), Data: []byte("gift")},
|
||||
Collectible: &officialgifts.CollectibleSet{
|
||||
Models: []officialgifts.Model{
|
||||
{Name: "Regular", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 2, FileName: "regular.tgs", SHA256: strings.Repeat("b", 64), Data: []byte("regular")}},
|
||||
{Name: "Crafted", DocumentID: 3, Crafted: true, Rarity: officialgifts.Rarity{Kind: "legendary"}, Document: officialgifts.Document{ID: 3, FileName: "crafted.tgs", SHA256: strings.Repeat("c", 64), Data: []byte("crafted")}},
|
||||
},
|
||||
Patterns: []officialgifts.Pattern{{Name: "Pattern", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 4, FileName: "pattern.tgs", SHA256: strings.Repeat("d", 64), Data: []byte("pattern")}}},
|
||||
Backdrops: []officialgifts.Backdrop{{Name: "Black", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}},
|
||||
},
|
||||
}}
|
||||
gifts := &fakeGiftsService{}
|
||||
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: gifts, OfficialGifts: source, Now: fixedNow})
|
||||
req := ImportOfficialStarGiftRequest{SourceGiftID: "5170145012310081615", Enabled: true, IncludeCollectible: true}
|
||||
req.CommandMeta = CommandMeta{CommandID: "dry-official", Actor: "ops", Reason: "official snapshot", DryRun: true}
|
||||
preview, err := svc.ImportOfficialStarGift(context.Background(), req)
|
||||
if err != nil || gifts.createCalls != 0 || preview.Details["crafted_models"] != 1 {
|
||||
t.Fatalf("preview=%+v err=%v create=%d", preview, err, gifts.createCalls)
|
||||
}
|
||||
req.CommandMeta = CommandMeta{CommandID: "exec-official", Actor: "ops", Reason: "official snapshot", DryRun: false}
|
||||
result, err := svc.ImportOfficialStarGift(context.Background(), req)
|
||||
if err != nil || gifts.createCalls != 1 || result.Details["collectible_revision_id"] != "33" {
|
||||
t.Fatalf("result=%+v err=%v create=%d", result, err, gifts.createCalls)
|
||||
}
|
||||
models := gifts.lastBundle.Collectible.Models
|
||||
if len(models) != 2 || !models[1].Crafted || models[1].RarityKind != domain.StarGiftRarityLegendary || models[1].RarityPermille != 0 ||
|
||||
models[0].RarityPermille != 922 || gifts.lastBundle.Collectible.Backdrops[0].BackdropID != 0 {
|
||||
t.Fatalf("imported models=%+v backdrops=%+v", models, gifts.lastBundle.Collectible.Backdrops)
|
||||
}
|
||||
catalog := gifts.lastBundle.Catalog
|
||||
if catalog.Limited || catalog.SoldOut || catalog.AvailabilityTotal != 0 || catalog.AvailabilityRemains != 0 ||
|
||||
catalog.AvailabilityResale != 0 || catalog.FirstSaleDate != 0 || catalog.LastSaleDate != 0 || catalog.ResellMinStars != 0 {
|
||||
t.Fatalf("official global market state leaked into local catalog: %+v", catalog)
|
||||
}
|
||||
if catalog.OfficialGiftID != source.bundle.Gift.ID || !bytes.Equal(catalog.OfficialSourceJSON, source.bundle.SourceJSON) ||
|
||||
!bytes.Equal(catalog.SourceManifestSHA256, source.bundle.ManifestSHA256) {
|
||||
t.Fatalf("official provenance was not preserved: %+v", catalog)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportOfficialStarGiftPublishesThroughRealGiftService(t *testing.T) {
|
||||
const lottie = `{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"layers":[{"ty":4}],"assets":[]}`
|
||||
document := func(id int64, name string) officialgifts.Document {
|
||||
raw := []byte(lottie)
|
||||
sum := sha256.Sum256(raw)
|
||||
return officialgifts.Document{ID: id, FileName: name, SHA256: hex.EncodeToString(sum[:]), Data: raw}
|
||||
}
|
||||
permille := 1000
|
||||
source := &fakeOfficialGiftsSource{bundle: officialgifts.Bundle{
|
||||
ManifestSHA256: bytesOf(0x24, sha256.Size),
|
||||
SourceJSON: []byte(`{"id":6003643167683903930,"title":"Party Sparkler"}`),
|
||||
Gift: officialgifts.Gift{
|
||||
ID: 6003643167683903930, Title: "Party Sparkler", Stars: 15, ConvertStars: 13,
|
||||
UpgradeStars: 25, AvailabilityTotal: 400000, DocumentID: 1,
|
||||
},
|
||||
BaseDocument: document(1, "gift.json"),
|
||||
Collectible: &officialgifts.CollectibleSet{
|
||||
Models: []officialgifts.Model{{
|
||||
Name: "Model", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
|
||||
Document: document(2, "model.json"),
|
||||
}},
|
||||
Patterns: []officialgifts.Pattern{{
|
||||
Name: "Pattern", DocumentID: 3, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
|
||||
Document: document(3, "pattern.json"),
|
||||
}},
|
||||
Backdrops: []officialgifts.Backdrop{{
|
||||
Name: "Backdrop", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
|
||||
}},
|
||||
},
|
||||
}}
|
||||
func TestImportDefaultStarGiftPublishesThroughRealService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
giftService := stargiftapp.NewService(memory.NewStarGiftStore(), &adminGiftBlob{data: map[string][]byte{}}, 2)
|
||||
svc := NewService(Dependencies{
|
||||
Commands: newMemoryCommandRepo(), Gifts: giftService, OfficialGifts: source, Now: fixedNow,
|
||||
})
|
||||
req := ImportOfficialStarGiftRequest{
|
||||
SourceGiftID: "6003643167683903930", Enabled: true, IncludeCollectible: true,
|
||||
CommandMeta: CommandMeta{CommandID: "exec-official-real-service", Actor: "ops", Reason: "regression", DryRun: false},
|
||||
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: giftService, Now: fixedNow})
|
||||
|
||||
if list := svc.DefaultStarGifts(); len(list) != 5 {
|
||||
t.Fatalf("default gifts = %d, want 5", len(list))
|
||||
}
|
||||
result, err := svc.ImportOfficialStarGift(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("import official collectible through real service: result=%+v err=%v", result, err)
|
||||
|
||||
// Dry-run must not write to the catalog.
|
||||
dry := ImportDefaultStarGiftRequest{ID: 3, CommandMeta: CommandMeta{CommandID: "dry-default-3", Actor: "ops", Reason: "demo", DryRun: true}}
|
||||
if _, err := svc.ImportDefaultStarGift(ctx, dry); err != nil {
|
||||
t.Fatalf("dry run: %v", err)
|
||||
}
|
||||
if cat, _ := giftService.Catalog(ctx); len(cat) != 0 {
|
||||
t.Fatalf("dry run wrote %d gifts", len(cat))
|
||||
}
|
||||
|
||||
// Confirmed import-all creates the whole demo set.
|
||||
all := ImportAllDefaultStarGiftsRequest{CommandMeta: CommandMeta{CommandID: "exec-default-all", Actor: "ops", Reason: "demo"}}
|
||||
result, err := svc.ImportAllDefaultStarGifts(ctx, all)
|
||||
if err != nil || result.Details["imported"] != 5 {
|
||||
t.Fatalf("import all: result=%+v err=%v", result, err)
|
||||
}
|
||||
catalog, err := giftService.Catalog(ctx)
|
||||
if err != nil || len(catalog) != 1 {
|
||||
t.Fatalf("catalog=%+v err=%v, want one imported gift", catalog, err)
|
||||
if err != nil || len(catalog) != 5 {
|
||||
t.Fatalf("catalog=%d err=%v, want 5", len(catalog), err)
|
||||
}
|
||||
preview, ok, err := giftService.CollectiblePreview(ctx, catalog[0].ID)
|
||||
if err != nil || !ok || len(preview.Models) != 1 || len(preview.Patterns) != 1 {
|
||||
t.Fatalf("preview=%+v ok=%v err=%v", preview, ok, err)
|
||||
limited, premium, upgradeable := 0, 0, 0
|
||||
var craftGiftID int64
|
||||
for _, g := range catalog {
|
||||
if g.Limited {
|
||||
limited++
|
||||
}
|
||||
if g.RequirePremium {
|
||||
premium++
|
||||
}
|
||||
if g.UpgradeStars > 0 {
|
||||
upgradeable++
|
||||
}
|
||||
if g.Title == "OwpenGram Coin" {
|
||||
craftGiftID = g.ID
|
||||
}
|
||||
}
|
||||
model := preview.Models[0].Document
|
||||
pattern := preview.Patterns[0].Document
|
||||
if model == nil || !model.IsSticker() || model.IsCustomEmoji() || pattern == nil ||
|
||||
pattern.IsSticker() || !pattern.IsCustomEmoji() || len(pattern.Thumbs) != 1 ||
|
||||
pattern.Thumbs[0].Kind != domain.PhotoSizeKindPath || len(pattern.Thumbs[0].Bytes) == 0 {
|
||||
t.Fatalf("materialized model=%+v pattern=%+v", model, pattern)
|
||||
if limited != 2 || premium != 1 || upgradeable != 4 {
|
||||
t.Fatalf("stored flags limited=%d premium=%d upgradeable=%d", limited, premium, upgradeable)
|
||||
}
|
||||
// The craftable gift must carry a craft-only (named-rarity) model.
|
||||
preview, ok, err := giftService.CollectiblePreview(ctx, craftGiftID)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("coin preview ok=%v err=%v", ok, err)
|
||||
}
|
||||
crafted := 0
|
||||
for _, m := range preview.Models {
|
||||
if m.Crafted {
|
||||
crafted++
|
||||
}
|
||||
}
|
||||
if crafted == 0 {
|
||||
t.Fatalf("coin has no craft-only model: %+v", preview.Models)
|
||||
}
|
||||
|
||||
// Re-running import-all is idempotent: everything already present -> skipped.
|
||||
result, err = svc.ImportAllDefaultStarGifts(ctx, ImportAllDefaultStarGiftsRequest{CommandMeta: CommandMeta{CommandID: "exec-default-all-2", Actor: "ops", Reason: "demo"}})
|
||||
if err != nil || result.Details["imported"] != 0 || result.Details["skipped"] != 5 {
|
||||
t.Fatalf("re-import: result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -879,30 +837,6 @@ func (b *adminGiftBlob) Get(_ context.Context, key string) ([]byte, error) {
|
|||
return append([]byte(nil), b.data[key]...), nil
|
||||
}
|
||||
|
||||
func bytesOf(value byte, count int) []byte {
|
||||
out := make([]byte, count)
|
||||
for i := range out {
|
||||
out[i] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type fakeOfficialGiftsSource struct{ bundle officialgifts.Bundle }
|
||||
|
||||
func (f *fakeOfficialGiftsSource) List(context.Context) ([]officialgifts.GiftSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeOfficialGiftsSource) Bundle(_ context.Context, giftID int64, include bool) (officialgifts.Bundle, error) {
|
||||
if giftID != f.bundle.Gift.ID {
|
||||
return officialgifts.Bundle{}, officialgifts.ErrNotFound
|
||||
}
|
||||
out := f.bundle
|
||||
if !include {
|
||||
out.Collectible = nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type fakeGiftsService struct {
|
||||
createCalls int
|
||||
lastBundle domain.StarGiftCatalogBundleWrite
|
||||
|
|
@ -915,6 +849,10 @@ func (f *fakeGiftsService) PrepareAnimation(name string, data []byte) (domain.St
|
|||
JSON: []byte(`{"v":"5.7"}`), TGS: []byte("tgs"), SHA256: sum[:], Width: 512, Height: 512, FrameRate: 30,
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeGiftsService) Catalog(context.Context) ([]domain.StarGift, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeGiftsService) PrepareOfficialAnimation(name string, data []byte) (domain.StarGiftAnimation, error) {
|
||||
return f.PrepareAnimation(name, data)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue