changed botfather avatar and fixed import

This commit is contained in:
onysd 2026-07-24 00:54:19 +03:00
parent c436c7c773
commit 0a6c05404a
18 changed files with 318 additions and 108 deletions

View file

@ -291,11 +291,13 @@ type ImportStarGiftRequest struct {
type ImportDefaultStarGiftRequest struct {
CommandMeta
ID int `json:"id"`
ID int `json:"id"`
Enabled bool `json:"enabled"`
}
type ImportAllDefaultStarGiftsRequest struct {
CommandMeta
Enabled bool `json:"enabled"`
}
type ImportOfficialStarGiftRequest struct {
@ -317,6 +319,7 @@ type ImportOfficialStarGiftRequest struct {
type ImportAllOfficialStarGiftsRequest struct {
CommandMeta
Enabled bool `json:"enabled"`
}
type SetStarGiftEnabledRequest struct {
@ -1127,6 +1130,7 @@ func (s *Service) ImportDefaultStarGift(ctx context.Context, req ImportDefaultSt
if err != nil {
return CommandResult{}, domain.ErrStarGiftInvalid
}
write.Catalog.Enabled = req.Enabled
return s.runCommand(ctx, req.CommandMeta, ActionImportDefaultStarGift, 0, domain.Peer{}, req, func() (CommandResult, error) {
details := map[string]any{
"id": req.ID, "title": title,
@ -1181,7 +1185,8 @@ func (s *Service) ImportAllDefaultStarGifts(ctx context.Context, req ImportAllDe
Actor: req.Actor,
Reason: req.Reason,
},
ID: item.ID,
ID: item.ID,
Enabled: req.Enabled,
}
result, opErr := s.ImportDefaultStarGift(ctx, perReq)
entry := map[string]any{"id": item.ID, "title": item.Title, "status": result.Status}
@ -1428,6 +1433,7 @@ func (s *Service) ImportAllOfficialStarGifts(ctx context.Context, req ImportAllO
// worth importing; CanUpgrade() is exactly the precondition
// ImportOfficialStarGift enforces for IncludeCollectible.
IncludeCollectible: item.CanUpgrade(),
Enabled: req.Enabled,
}
result, opErr := s.ImportOfficialStarGift(ctx, perReq)
entry := map[string]any{"source_gift_id": perReq.SourceGiftID, "status": result.Status}

View file

@ -760,8 +760,8 @@ func TestImportDefaultStarGiftPublishesThroughRealService(t *testing.T) {
giftService := stargiftapp.NewService(memory.NewStarGiftStore(), &adminGiftBlob{data: map[string][]byte{}}, 2)
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))
if list := svc.DefaultStarGifts(); len(list) != 3 {
t.Fatalf("default gifts = %d, want 3", len(list))
}
// Dry-run must not write to the catalog.
@ -773,15 +773,15 @@ func TestImportDefaultStarGiftPublishesThroughRealService(t *testing.T) {
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"}}
// Confirmed import-all creates the whole demo set, enabled per the request flag.
all := ImportAllDefaultStarGiftsRequest{CommandMeta: CommandMeta{CommandID: "exec-default-all", Actor: "ops", Reason: "demo"}, Enabled: true}
result, err := svc.ImportAllDefaultStarGifts(ctx, all)
if err != nil || result.Details["imported"] != 5 {
if err != nil || result.Details["imported"] != 3 {
t.Fatalf("import all: result=%+v err=%v", result, err)
}
catalog, err := giftService.Catalog(ctx)
if err != nil || len(catalog) != 5 {
t.Fatalf("catalog=%d err=%v, want 5", len(catalog), err)
if err != nil || len(catalog) != 3 {
t.Fatalf("catalog=%d err=%v, want 3", len(catalog), err)
}
limited, premium, upgradeable := 0, 0, 0
var craftGiftID int64
@ -799,7 +799,7 @@ func TestImportDefaultStarGiftPublishesThroughRealService(t *testing.T) {
craftGiftID = g.ID
}
}
if limited != 2 || premium != 1 || upgradeable != 4 {
if limited != 0 || premium != 0 || upgradeable != 2 {
t.Fatalf("stored flags limited=%d premium=%d upgradeable=%d", limited, premium, upgradeable)
}
// The craftable gift must carry a craft-only (named-rarity) model.
@ -818,12 +818,43 @@ func TestImportDefaultStarGiftPublishesThroughRealService(t *testing.T) {
}
// 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 {
// Enabled must match the first run's value (true) or the per-gift replay
// hits COMMAND_ID_CONFLICT since the payload would differ from the cached one.
result, err = svc.ImportAllDefaultStarGifts(ctx, ImportAllDefaultStarGiftsRequest{CommandMeta: CommandMeta{CommandID: "exec-default-all-2", Actor: "ops", Reason: "demo"}, Enabled: true})
if err != nil || result.Details["imported"] != 0 || result.Details["skipped"] != 3 {
t.Fatalf("re-import: result=%+v err=%v", result, err)
}
}
// TestImportDefaultStarGiftRespectsEnabledFlag is a regression test: a single
// default gift import must honor the request's Enabled flag rather than
// always landing enabled (or, before this fix, always disabled regardless of
// the admin console checkbox).
func TestImportDefaultStarGiftRespectsEnabledFlag(t *testing.T) {
ctx := context.Background()
giftService := stargiftapp.NewService(memory.NewStarGiftStore(), &adminGiftBlob{data: map[string][]byte{}}, 2)
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: giftService, Now: fixedNow})
off := ImportDefaultStarGiftRequest{ID: 1, CommandMeta: CommandMeta{CommandID: "default-1-off", Actor: "ops", Reason: "demo"}, Enabled: false}
if _, err := svc.ImportDefaultStarGift(ctx, off); err != nil {
t.Fatalf("import disabled: %v", err)
}
on := ImportDefaultStarGiftRequest{ID: 2, CommandMeta: CommandMeta{CommandID: "default-2-on", Actor: "ops", Reason: "demo"}, Enabled: true}
if _, err := svc.ImportDefaultStarGift(ctx, on); err != nil {
t.Fatalf("import enabled: %v", err)
}
// Catalog() only ever returns enabled gifts, so presence/absence here
// directly proves whether the Enabled flag was honored.
catalog, err := giftService.Catalog(ctx)
if err != nil || len(catalog) != 1 {
t.Fatalf("catalog=%d err=%v, want 1 (only the enabled gift)", len(catalog), err)
}
if catalog[0].Title != "OwpenGram Star" {
t.Fatalf("unexpected gift in catalog: %q, want %q", catalog[0].Title, "OwpenGram Star")
}
}
type adminGiftBlob struct{ data map[string][]byte }
func (b *adminGiftBlob) Name() string { return "localfs" }

View file

@ -0,0 +1,54 @@
package files
import (
"context"
_ "embed"
"fmt"
"time"
"telesrv/internal/domain"
)
//go:embed seedassets/botfather_avatar.jpg
var botFatherAvatarJPG []byte
// SeedBotFatherAvatar idempotently seeds the built-in BotFather account's
// profile photo from the bundled avatar, mirroring SeedOfficialSystemAvatar:
// writes it under the fixed domain.BotFatherUserPhotoID so the photo/blob
// layer and the pure domain.BotFatherUser() struct literal stay in sync
// across restarts, and registers it as the account's *current* profile photo
// so users.getFullUser resolves it too. Returns true if it actually wrote a
// new photo.
func (s *Service) SeedBotFatherAvatar(ctx context.Context) (bool, error) {
photoID := domain.BotFatherUserPhotoID
wrote := false
if _, found, err := s.media.GetPhoto(ctx, photoID); err != nil {
return false, err
} else if !found {
sizes, err := s.putPhotoStaticSizes(ctx, photoID, botFatherAvatarJPG, photoSizeSpecsForAvatar(botFatherAvatarJPG))
if err != nil {
return false, err
}
photo := domain.Photo{
ID: photoID,
AccessHash: domain.BotFatherUserPhotoAccessHash,
FileReference: randomFileReference(),
Date: int(time.Now().Unix()),
DCID: s.dc,
Sizes: sizes,
}
if err := s.media.PutPhoto(ctx, photo); err != nil {
return false, err
}
wrote = true
}
photo, ok, err := s.SetCurrentProfilePhoto(ctx, domain.PeerTypeUser, domain.BotFatherUserID, photoID, int(time.Now().Unix()))
if err != nil {
return false, err
}
if !ok {
return false, fmt.Errorf("botfather avatar photo %d not found after seeding", photoID)
}
domain.SetBotFatherAvatar(photo.DCID, domain.StrippedFromSizes(photo.Sizes))
return wrote, nil
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View file

@ -15,6 +15,10 @@ const (
BotFatherUserID int64 = 93372553
// BotFatherAccessHash 固定不变;与迁移 0090 的种子行双写,必须保持一致。
BotFatherAccessHash int64 = 7421896403922962293
// BotFatherUserPhotoID/AccessHash 是 BotFather 头像 photo 的固定 id
// 与 files.Service.SeedBotFatherAvatar 种子写入的行保持一致。
BotFatherUserPhotoID int64 = 933725530001
BotFatherUserPhotoAccessHash int64 = 3198475620194837201
// StickersBotUserID 是内置 @Stickers 账号。它是 server 内置 service bot
// 不走外部 Bot API 进程。
@ -43,6 +47,20 @@ func SetOfficialSystemUserAvatar(dcID int, stripped []byte) {
officialSystemUserPhotoStripped = stripped
}
// botFatherPhotoDCID/Stripped 由 files.Service.SeedBotFatherAvatar 在启动时
// 通过 SetBotFatherAvatar 写入一次;写入前 BotFatherUser() 不带头像PhotoID==0
var (
botFatherPhotoDCID int
botFatherPhotoStripped []byte
)
// SetBotFatherAvatar 记录 BotFather 头像所在的 DC 与内联缩略图字节。
// 只应在启动阶段、头像 seed 完成后调用一次。
func SetBotFatherAvatar(dcID int, stripped []byte) {
botFatherPhotoDCID = dcID
botFatherPhotoStripped = stripped
}
// OfficialSystemUser 返回第一阶段内置的官方系统账号。
func OfficialSystemUser() User {
u := User{
@ -64,7 +82,7 @@ func OfficialSystemUser() User {
// BotFatherUser 返回内置 BotFather 账号。username 不以 bot 结尾属种子例外(与官方一致)。
func BotFatherUser() User {
return User{
u := User{
ID: BotFatherUserID,
AccessHash: BotFatherAccessHash,
FirstName: "BotFather",
@ -73,6 +91,12 @@ func BotFatherUser() User {
Bot: true,
BotInfoVersion: 1,
}
if botFatherPhotoDCID != 0 {
u.PhotoID = BotFatherUserPhotoID
u.PhotoDCID = botFatherPhotoDCID
u.PhotoStripped = botFatherPhotoStripped
}
return u
}
// StickersBotUser 返回内置 @Stickers 账号。username 不以 bot 结尾属种子例外(与官方一致)。

View file

@ -29,8 +29,8 @@ func newService() *stargifts.Service {
func TestListDescribesFullGiftSurface(t *testing.T) {
list := List()
if len(list) != 5 {
t.Fatalf("List has %d gifts, want 5", len(list))
if len(list) != 3 {
t.Fatalf("List has %d gifts, want 3", len(list))
}
upgradeable, craftable, limited, premium := 0, 0, 0, 0
for _, g := range list {
@ -50,7 +50,8 @@ func TestListDescribesFullGiftSurface(t *testing.T) {
premium++
}
}
if upgradeable != 4 || craftable != 3 || limited != 2 || premium != 1 {
// One plain (Spark), one upgradeable-only (Star), one craftable (Coin).
if upgradeable != 2 || craftable != 1 || limited != 0 || premium != 0 {
t.Fatalf("surface counts upgradeable=%d craftable=%d limited=%d premium=%d", upgradeable, craftable, limited, premium)
}
}
@ -79,8 +80,8 @@ func TestBuildBundleImportsEndToEnd(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(catalog) != 5 {
t.Fatalf("catalog has %d, want 5", len(catalog))
if len(catalog) != 3 {
t.Fatalf("catalog has %d, want 3", len(catalog))
}
limited, premium, upgradeable := 0, 0, 0
for _, g := range catalog {
@ -94,7 +95,7 @@ func TestBuildBundleImportsEndToEnd(t *testing.T) {
upgradeable++
}
}
if limited != 2 || premium != 1 || upgradeable != 4 {
if limited != 0 || premium != 0 || upgradeable != 2 {
t.Fatalf("stored flags limited=%d premium=%d upgradeable=%d", limited, premium, upgradeable)
}
}

View file

@ -44,18 +44,19 @@ func demoBackdrops() []backdropSpec {
}
}
// demoGifts returns the five demo gifts in display order.
// demoGifts returns the three demo gifts in display order, one per capability
// tier: plain (not upgradeable), upgradeable (no crafting), and craftable.
func demoGifts() []giftSpec {
return []giftSpec{
{
// #1 — cheapest, plain, not upgradeable.
// #1 — Звичайний: cheapest, plain, not upgradeable.
title: "OwpenGram Spark",
stars: 15,
convert: 15,
base: burst(8, colGold, motionPulse),
},
{
// #2 — standard upgradeable, no crafting.
// #2 — Апгрейдебл: standard upgradeable, no crafting.
title: "OwpenGram Star",
stars: 50,
convert: 50,
@ -76,7 +77,7 @@ func demoGifts() []giftSpec {
},
},
{
// #3 — upgradeable + craftable.
// #3 — Крафтабл: upgradeable + craftable.
title: "OwpenGram Coin",
stars: 100,
convert: 75,
@ -97,54 +98,5 @@ func demoGifts() []giftSpec {
backdrops: demoBackdrops(),
},
},
{
// #4 — limited edition, upgradeable + craftable.
title: "OwpenGram Gem",
stars: 250,
convert: 200,
base: polygon(6, colViolet, colWhite, 10, motionSpinPulse),
limited: true,
availability: 5000,
upgrade: &upgradeSpec{
upgradeStars: 800,
supplyTotal: 3000,
slug: "owg-gem",
models: []attrSpec{
{name: "Amethyst", spec: polygon(6, colViolet, colWhite, 12, motionSpin), permille: 600},
{name: "Verdant", spec: polygon(6, colEmerald, colWhite, 12, motionSpin), permille: 400},
{name: "Prism", spec: polygon(8, colCyan, colWhite, 10, motionSpinPulse), crafted: true, rarity: domain.StarGiftRarityEpic},
},
patterns: []attrSpec{
{name: "Facet", spec: burst(12, colViolet, motionPulse), permille: 700},
{name: "Shine", spec: burst(8, colWhite, motionPulse), permille: 300},
},
backdrops: demoBackdrops(),
},
},
{
// #5 — premium-gated, limited, the full stack.
title: "OwpenGram Crown",
stars: 1000,
convert: 800,
base: star(3, colGold, colAmber, 12, motionSpinPulse),
limited: true,
availability: 500,
requirePremium: true,
upgrade: &upgradeSpec{
upgradeStars: 2000,
supplyTotal: 500,
slug: "owg-crown",
models: []attrSpec{
{name: "Regal", spec: star(3, colGold, colAmber, 14, motionSpinPulse), permille: 600},
{name: "Noble", spec: star(5, colAmber, colGold, 12, motionSpin), permille: 400},
{name: "Eternal", spec: star(6, colGold, colWhite, 12, motionSpinPulse), crafted: true, rarity: domain.StarGiftRarityLegendary},
},
patterns: []attrSpec{
{name: "Aura", spec: burst(12, colGold, motionPulse), permille: 700},
{name: "Crest", spec: burst(8, colWhite, motionPulse), permille: 300},
},
backdrops: demoBackdrops(),
},
},
}
}