owpengram-server/internal/app/files/seed.go
A 23a2b2aff7 fix: restore sticker placeholders and media history
(cherry picked from commit 488e409a1898e9c739cc0bd24cb9791636dfd6b3)
2026-06-09 15:00:19 +08:00

819 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package files
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"telesrv/internal/domain"
)
// 本文件实现从外部导出的 reaction / sticker 资源目录导入媒体种子:
// JSON 元数据(外部导出 document id/access_hash/file_reference/attributes/thumbs落 documents /
// sticker_sets / available_reactions 表,二进制 .tgs/.webp/缩略图落 blob backend。
// dc_id 统一重写为本 server 的 DC使客户端从本 DC 下载。导入幂等(已存在则跳过)。
// SeedStats 汇报一次种子导入结果。
type SeedStats struct {
Reactions int
StickerSets int
Documents int
Blobs int
Skipped bool
}
// SeedMedia 从导出根目录导入 reaction 与 sticker 资源。maxRegularSets<=0 表示不限。
func (s *Service) SeedMedia(ctx context.Context, root string, maxRegularSets int) (SeedStats, error) {
var stats SeedStats
if root == "" {
stats.Skipped = true
return stats, nil
}
if _, err := os.Stat(root); err != nil {
// 目录不存在:跳过而非失败(开发机可能未放资源)。
stats.Skipped = true
return stats, nil
}
// reactions
if n, err := s.media.CountAvailableReactions(ctx); err != nil {
return stats, err
} else if n == 0 {
if err := s.seedReactions(ctx, root, &stats); err != nil {
return stats, fmt.Errorf("seed reactions: %w", err)
}
} else if incomplete, err := s.availableReactionSeedNeedsRepair(ctx); err != nil {
return stats, err
} else if incomplete {
if err := s.seedReactions(ctx, root, &stats); err != nil {
return stats, fmt.Errorf("repair reactions: %w", err)
}
}
// sticker setsdefault 系统集 + 常规集)
if n, err := s.media.CountStickerSets(ctx); err != nil {
return stats, err
} else if n == 0 {
if err := s.seedStickerSets(ctx, root, maxRegularSets, &stats); err != nil {
return stats, fmt.Errorf("seed sticker sets: %w", err)
}
} else if stale, err := s.stickerSetDocumentThumbsNeedInlineCache(ctx); err != nil {
return stats, err
} else if stale {
if err := s.seedStickerSets(ctx, root, maxRegularSets, &stats); err != nil {
return stats, fmt.Errorf("repair sticker set thumbs: %w", err)
}
}
if stats.Reactions == 0 && stats.StickerSets == 0 {
stats.Skipped = true
}
return stats, nil
}
// ---- reactions ----
func (s *Service) seedReactions(ctx context.Context, root string, stats *SeedStats) error {
reactionsDir := filepath.Join(root, "telegram_reactions_export", "reactions")
rawPath := filepath.Join(root, "telegram_reactions_export", "global_json", "available_reactions_raw.json")
raw, err := os.ReadFile(rawPath)
if err != nil {
return nil // 没有 reaction 资源就跳过
}
var parsed struct {
Result struct {
Reactions []seedReactionJSON `json:"reactions"`
} `json:"result"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
return fmt.Errorf("parse available_reactions_raw.json: %w", err)
}
index, err := scanSeedDir(reactionsDir)
if err != nil {
return err
}
for i, rj := range parsed.Result.Reactions {
ar := domain.AvailableReaction{
Reaction: rj.Reaction,
Title: rj.Title,
Inactive: rj.Inactive,
Premium: rj.Premium,
Order: i,
}
set := func(dst *int64, d *seedDocumentJSON) error {
if d == nil || d.ID == 0 {
return nil
}
doc, err := s.importDocument(ctx, *d, reactionsDir, index, stats)
if err != nil {
return err
}
*dst = doc.ID
return nil
}
if err := set(&ar.StaticIconID, rj.StaticIcon); err != nil {
return err
}
if err := set(&ar.AppearAnimationID, rj.AppearAnimation); err != nil {
return err
}
if err := set(&ar.SelectAnimationID, rj.SelectAnimation); err != nil {
return err
}
if err := set(&ar.ActivateAnimationID, rj.ActivateAnimation); err != nil {
return err
}
if err := set(&ar.EffectAnimationID, rj.EffectAnimation); err != nil {
return err
}
if err := set(&ar.AroundAnimationID, rj.AroundAnimation); err != nil {
return err
}
if err := set(&ar.CenterIconID, rj.CenterIcon); err != nil {
return err
}
if err := s.media.PutAvailableReaction(ctx, ar); err != nil {
return err
}
stats.Reactions++
}
return nil
}
func (s *Service) availableReactionSeedNeedsRepair(ctx context.Context) (bool, error) {
reactions, err := s.media.ListAvailableReactions(ctx)
if err != nil {
return false, err
}
var docIDs []int64
for _, r := range reactions {
for _, id := range r.DocumentIDs() {
if id == 0 {
continue
}
docIDs = append(docIDs, id)
if _, ok, err := s.media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", id)); err != nil {
return false, err
} else if !ok {
return true, nil
}
}
}
if stale, err := s.documentsNeedInlineCachedThumbs(ctx, docIDs); err != nil || stale {
return stale, err
}
return false, nil
}
// ---- sticker sets ----
func (s *Service) seedStickerSets(ctx context.Context, root string, maxRegular int, stats *SeedStats) error {
// default 系统集:目录名 → system_key。
defaultDir := filepath.Join(root, "telegram_default_stickers_export")
order := 0
if entries, err := os.ReadDir(defaultDir); err == nil {
names := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
names = append(names, e.Name())
}
}
sort.Strings(names)
for _, name := range names {
systemKey := systemKeyForDefaultSet(name)
setDir := filepath.Join(defaultDir, name)
if err := s.importStickerSetDir(ctx, setDir, systemKey, order, stats); err != nil {
return fmt.Errorf("import default set %s: %w", name, err)
}
order++
}
}
// 常规贴纸集。
regularDir := filepath.Join(root, "telegram_stickers_export")
if entries, err := os.ReadDir(regularDir); err == nil {
names := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
names = append(names, e.Name())
}
}
sort.Strings(names)
imported := 0
for _, name := range names {
if maxRegular > 0 && imported >= maxRegular {
break
}
setDir := filepath.Join(regularDir, name)
if err := s.importStickerSetDir(ctx, setDir, "", order, stats); err != nil {
return fmt.Errorf("import sticker set %s: %w", name, err)
}
order++
imported++
}
}
return nil
}
func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey string, order int, stats *SeedStats) error {
infoPath := filepath.Join(setDir, "set_info.json")
raw, err := os.ReadFile(infoPath)
if err != nil {
return nil // 该目录无 set_info.json → 跳过
}
var info struct {
Result seedStickerSetResultJSON `json:"result"`
}
if err := json.Unmarshal(raw, &info); err != nil {
return fmt.Errorf("parse %s: %w", infoPath, err)
}
sj := info.Result.Set
if sj.ID == 0 {
return nil
}
stickersDir := filepath.Join(setDir, "stickers")
index, err := scanSeedDir(stickersDir)
if err != nil {
return err
}
docIDs := make([]int64, 0, len(info.Result.Documents))
docs := make([]domain.Document, 0, len(info.Result.Documents))
docIDBySource := make(map[int64]int64, len(info.Result.Documents))
for _, dj := range info.Result.Documents {
sourceID := dj.ID
doc, err := s.importDocument(ctx, dj, stickersDir, index, stats)
if err != nil {
return err
}
if doc.ID != 0 {
docIDBySource[sourceID] = doc.ID
docIDs = append(docIDs, doc.ID)
docs = append(docs, doc)
}
}
kind := stickerSetKind(sj, systemKey)
set := domain.StickerSet{
ID: sj.ID,
AccessHash: sj.AccessHash,
ShortName: sj.ShortName,
Title: sj.Title,
Count: sj.Count,
Hash: sj.Hash,
Kind: kind,
Official: sj.Official,
Animated: true, // 导出资源均为 .tgs 动画贴纸
Emojis: sj.Emojis,
Masks: sj.Masks,
Archived: sj.Archived,
Installed: seedStickerSetInstalled(kind),
ThumbDocumentID: seedDocumentStorageID(derefInt64(sj.ThumbDocumentID)),
Thumbs: seedStickerSetPhotoSizes(sj.Thumbs),
ThumbDCID: s.dc,
ThumbVersion: sj.ThumbVersion,
DocumentIDs: docIDs,
Packs: seedStickerPacks(sj.Packs, info.Result.Packs, docIDBySource),
SortOrder: order,
SystemKey: systemKey,
}
if set.Count == 0 {
set.Count = len(docIDs)
}
if err := s.media.PutStickerSet(ctx, set); err != nil {
return err
}
s.stickerSetCache.put(set, docs)
stats.StickerSets++
return nil
}
// ---- 单个 document 导入 ----
func (s *Service) importDocument(ctx context.Context, dj seedDocumentJSON, binDir string, index seedDirIndex, stats *SeedStats) (domain.Document, error) {
if dj.ID == 0 {
return domain.Document{}, nil
}
storageID := seedDocumentStorageID(dj.ID)
ref, _ := hex.DecodeString(dj.FileReference)
doc := domain.Document{
ID: storageID,
AccessHash: dj.AccessHash,
FileReference: ref,
Date: parseSeedDate(dj.Date),
MimeType: dj.MimeType,
Size: dj.Size,
DCID: s.dc,
Attributes: seedDocumentAttributes(dj.Attributes),
}
// 主体 blobdoc:<server-owned-id>
if mainPath, ok := index.main[dj.ID]; ok {
data, err := os.ReadFile(mainPath)
if err != nil {
return domain.Document{}, err
}
objectKey, err := s.blobs.Put(ctx, data)
if err != nil {
return domain.Document{}, err
}
if err := s.media.PutFileBlob(ctx, domain.FileBlob{
LocationKey: fmt.Sprintf("doc:%d", storageID),
Backend: domain.MediaBackend(s.blobs.Name()),
ObjectKey: objectKey,
Size: int64(len(data)),
MimeType: dj.MimeType,
}); err != nil {
return domain.Document{}, err
}
s.prewarmSmallBlob(objectKey, data)
stats.Blobs++
}
// 缩略图保留两类并存(与官方 sticker 一致):
// - PhotoPathSize 矢量轮廓:随 document 元数据内联下发,是 TDesktop 对 animated
// sticker 在完整 .tgs 下载完成前唯一可即时渲染的占位history_view_sticker 显式
// 禁用了 stripped 内联占位cached 字节又经 RPC 出口转成 downloadable丢掉它会让
// 打开会话时 sticker 先空白、并多触发一次缩略图 getFile。
// - 小 PhotoSize 静态图:写 blob 并暂存为 PhotoCachedSizeRPC 出口再转 downloadable
// photoSize m供 sticker 面板等需要小缩略图的场景下载。
thumbs := make([]domain.PhotoSize, 0, len(dj.Thumbs))
for _, tj := range dj.Thumbs {
ps, downloadable := seedPhotoSize(tj)
if ps.Kind == "" {
continue
}
if downloadable {
thumbPath, ok := index.thumb[dj.ID][ps.Type]
if !ok {
continue // 无可服务的缩略图文件,丢弃该尺寸(保留 PhotoPathSize 占位)
}
data, err := os.ReadFile(thumbPath)
if err != nil {
return domain.Document{}, err
}
objectKey, err := s.blobs.Put(ctx, data)
if err != nil {
return domain.Document{}, err
}
ps.Size = len(data)
if err := s.media.PutFileBlob(ctx, domain.FileBlob{
LocationKey: fmt.Sprintf("doc:%d:%s", storageID, ps.Type),
Backend: domain.MediaBackend(s.blobs.Name()),
ObjectKey: objectKey,
Size: int64(len(data)),
MimeType: seedThumbMimeType(data),
}); err != nil {
return domain.Document{}, err
}
ps = seedInlineCachedDocumentThumb(ps, data)
s.prewarmSmallBlob(objectKey, data)
stats.Blobs++
}
thumbs = append(thumbs, ps)
}
doc.Thumbs = thumbs
if err := s.media.PutDocument(ctx, doc); err != nil {
return domain.Document{}, err
}
stats.Documents++
return doc, nil
}
func (s *Service) prewarmSmallBlob(objectKey string, data []byte) {
if len(data) > 0 && len(data) <= blobBytesCacheMaxEntryBytes {
s.byteCache.put(objectKey, data)
}
}
// ---- 目录扫描docID → 主体文件 / 可下载缩略图 ----
type seedDirIndex struct {
main map[int64]string // docID -> 主体文件路径
thumb map[int64]map[string]string // docID -> thumbType -> 缩略图文件路径
}
var seedTrailingDigits = regexp.MustCompile(`(\d{6,})`)
var seedThumbMarker = regexp.MustCompile(`_thumb\d+_`)
const seedInlineCachedDocumentThumbMaxBytes = 32 * 1024
// Exported Telegram resources keep their original id in filenames/JSON, but
// telesrv owns the document catalog it serves. Imported high source ids are
// normalized once at seed time so RPC/storage use one server-owned id.
const seedExternalDocumentIDOffset int64 = 4_000_000_000_000_000_000
var seedThumbType = regexp.MustCompile(`PhotoSize_type([a-z])`)
func seedDocumentStorageID(sourceID int64) int64 {
if sourceID <= 0 {
return 0
}
if sourceID > seedExternalDocumentIDOffset {
return sourceID - seedExternalDocumentIDOffset
}
return sourceID
}
func scanSeedDir(dir string) (seedDirIndex, error) {
idx := seedDirIndex{main: map[int64]string{}, thumb: map[int64]map[string]string{}}
entries, err := os.ReadDir(dir)
if err != nil {
return idx, nil // 目录不存在 → 空 index
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
ext := strings.ToLower(filepath.Ext(name))
full := filepath.Join(dir, name)
if marker := seedThumbMarker.FindStringIndex(name); marker != nil {
// 只收可下载的 PhotoSize 缩略图jpgPhotoPathSize(svg) 内联在 JSON。
if ext != ".jpg" && ext != ".jpeg" {
continue
}
m := seedThumbType.FindStringSubmatch(name)
if m == nil {
continue
}
docID := docIDFromName(name[:marker[0]])
if docID == 0 {
continue
}
if idx.thumb[docID] == nil {
idx.thumb[docID] = map[string]string{}
}
idx.thumb[docID][m[1]] = full
continue
}
if ext == ".svg" || ext == ".json" {
continue
}
docID := docIDFromName(strings.TrimSuffix(name, filepath.Ext(name)))
if docID == 0 {
continue
}
idx.main[docID] = full
}
return idx, nil
}
// docIDFromName 取 base name 中末尾最长的数字串作为 document id。
func docIDFromName(base string) int64 {
matches := seedTrailingDigits.FindAllString(base, -1)
if len(matches) == 0 {
return 0
}
last := matches[len(matches)-1]
id, err := strconv.ParseInt(last, 10, 64)
if err != nil {
return 0
}
return id
}
func systemKeyForDefaultSet(dirName string) string {
switch dirName {
case "DefaultSet_AnimatedEmoji":
return "animated_emoji"
case "DefaultSet_AnimatedEmojiAnimations":
return "animated_emoji_animations"
case "DefaultSet_EmojiGenericAnimations":
return "emoji_generic_animations"
case "DefaultSet_Dice_Normal":
return "dice:\U0001f3b2"
case "DefaultSet_Dice_Dart":
return "dice:\U0001f3af"
case "DefaultSet_Dice_Basketball":
return "dice:\U0001f3c0"
case "DefaultSet_Dice_Football":
return "dice:⚽"
case "DefaultSet_Dice_Bowling":
return "dice:\U0001f3b3"
case "DefaultSet_Dice_Casino":
return "dice:\U0001f3b0"
default:
return ""
}
}
func stickerSetKind(sj seedStickerSetJSON, systemKey string) domain.StickerSetKind {
switch {
case systemKey != "":
return domain.StickerSetKindSystem
case sj.Emojis:
return domain.StickerSetKindEmoji
case sj.Masks:
return domain.StickerSetKindMasks
default:
return domain.StickerSetKindStickers
}
}
func seedStickerSetInstalled(kind domain.StickerSetKind) bool {
return kind != domain.StickerSetKindSystem
}
// ---- JSON → domain 转换 ----
func seedDocumentAttributes(attrs []seedAttrJSON) []domain.DocumentAttribute {
out := make([]domain.DocumentAttribute, 0, len(attrs))
for _, a := range attrs {
switch a.Type {
case "DocumentAttributeImageSize":
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrImageSize, W: a.W, H: a.H})
case "DocumentAttributeAnimated":
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrAnimated})
case "DocumentAttributeSticker":
attr := domain.DocumentAttribute{Kind: domain.DocAttrSticker, Alt: a.Alt, Mask: a.Mask}
if a.Stickerset != nil {
attr.StickerSetID = a.Stickerset.ID
attr.StickerSetAccessHash = a.Stickerset.AccessHash
}
out = append(out, attr)
case "DocumentAttributeCustomEmoji":
attr := domain.DocumentAttribute{Kind: domain.DocAttrCustomEmoji, Alt: a.Alt, Free: a.Free, TextColor: a.TextColor}
if a.Stickerset != nil {
attr.StickerSetID = a.Stickerset.ID
attr.StickerSetAccessHash = a.Stickerset.AccessHash
}
out = append(out, attr)
case "DocumentAttributeVideo":
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: a.W, H: a.H, Duration: a.Duration, RoundMessage: a.RoundMessage, SupportsStreaming: a.SupportsStreaming})
case "DocumentAttributeAudio":
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrAudio, AudioDuration: int(a.Duration), Voice: a.Voice, Title: a.Title, Performer: a.Performer})
case "DocumentAttributeFilename":
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrFilename, FileName: a.FileName})
}
}
return out
}
func seedPhotoSizes(thumbs []seedThumbJSON) []domain.PhotoSize {
out := make([]domain.PhotoSize, 0, len(thumbs))
for _, t := range thumbs {
ps, _ := seedPhotoSize(t)
if ps.Kind != "" {
out = append(out, ps)
}
}
return out
}
func seedStickerSetPhotoSizes(thumbs []seedThumbJSON) []domain.PhotoSize {
out := make([]domain.PhotoSize, 0, len(thumbs))
for _, t := range thumbs {
ps, downloadable := seedPhotoSize(t)
if ps.Kind == "" || downloadable {
continue
}
out = append(out, ps)
}
return out
}
func seedPhotoSize(t seedThumbJSON) (domain.PhotoSize, bool) {
switch t.Type {
case "PhotoSize":
return domain.PhotoSize{Kind: domain.PhotoSizeKindDefault, Type: t.SizeType, W: t.W, H: t.H, Size: t.Size}, true
case "PhotoStrippedSize":
b, _ := hex.DecodeString(t.Bytes)
return domain.PhotoSize{Kind: domain.PhotoSizeKindStripped, Type: t.SizeType, Bytes: b}, false
case "PhotoCachedSize":
b, _ := hex.DecodeString(t.Bytes)
return domain.PhotoSize{Kind: domain.PhotoSizeKindCached, Type: t.SizeType, W: t.W, H: t.H, Bytes: b}, false
case "PhotoPathSize":
b, _ := hex.DecodeString(t.Bytes)
return domain.PhotoSize{Kind: domain.PhotoSizeKindPath, Type: t.SizeType, Bytes: b}, false
case "PhotoSizeProgressive":
return domain.PhotoSize{Kind: domain.PhotoSizeKindProgressive, Type: t.SizeType, W: t.W, H: t.H, Sizes: t.Sizes}, true
default:
return domain.PhotoSize{}, false
}
}
func seedInlineCachedDocumentThumb(ps domain.PhotoSize, data []byte) domain.PhotoSize {
if ps.Kind != domain.PhotoSizeKindDefault || len(data) == 0 || len(data) > seedInlineCachedDocumentThumbMaxBytes {
return ps
}
ps.Kind = domain.PhotoSizeKindCached
ps.Size = 0
ps.Bytes = append([]byte(nil), data...)
return ps
}
func seedThumbMimeType(data []byte) string {
switch {
case len(data) >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P':
return "image/webp"
case len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF:
return "image/jpeg"
case len(data) >= 8 && data[0] == 0x89 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G':
return "image/png"
case len(data) >= 6 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F':
return "image/gif"
default:
return "application/octet-stream"
}
}
func (s *Service) stickerSetDocumentThumbsNeedInlineCache(ctx context.Context) (bool, error) {
var ids []int64
for _, kind := range []domain.StickerSetKind{
domain.StickerSetKindStickers,
domain.StickerSetKindEmoji,
domain.StickerSetKindMasks,
domain.StickerSetKindSystem,
} {
sets, err := s.media.ListStickerSets(ctx, kind)
if err != nil {
return false, err
}
for _, set := range sets {
ids = append(ids, set.DocumentIDs...)
}
}
return s.documentsNeedInlineCachedThumbs(ctx, ids)
}
func (s *Service) documentsNeedInlineCachedThumbs(ctx context.Context, ids []int64) (bool, error) {
if len(ids) == 0 {
return false, nil
}
seen := make(map[int64]struct{}, len(ids))
unique := make([]int64, 0, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
unique = append(unique, id)
}
docs, err := s.media.GetDocuments(ctx, unique)
if err != nil {
return false, err
}
for _, doc := range docs {
for _, thumb := range doc.Thumbs {
if thumb.Kind == domain.PhotoSizeKindDefault && thumb.Size > 0 && thumb.Size <= seedInlineCachedDocumentThumbMaxBytes {
return true, nil
}
if thumb.Kind == domain.PhotoSizeKindCached && len(thumb.Bytes) > 0 {
blob, ok, err := s.media.GetFileBlob(ctx, fmt.Sprintf("doc:%d:%s", doc.ID, thumb.Type))
if err != nil {
return false, err
}
if ok {
want := seedThumbMimeType(thumb.Bytes)
if want != "application/octet-stream" && blob.MimeType != want {
return true, nil
}
}
}
}
}
return false, nil
}
func seedStickerPacks(setPacks, resultPacks []seedStickerPackJSON, docIDBySource map[int64]int64) []domain.StickerPack {
packs := setPacks
if len(packs) == 0 {
packs = resultPacks
}
out := make([]domain.StickerPack, 0, len(packs))
for _, p := range packs {
documents := make([]int64, 0, len(p.Documents))
for _, sourceID := range p.Documents {
if id, ok := docIDBySource[sourceID]; ok {
documents = append(documents, id)
continue
}
if id := seedDocumentStorageID(sourceID); id != 0 {
documents = append(documents, id)
}
}
out = append(out, domain.StickerPack{Emoticon: p.Emoticon, DocumentIDs: documents})
}
return out
}
func parseSeedDate(s string) int {
if s == "" {
return 0
}
if t, err := time.Parse(time.RFC3339, s); err == nil {
return int(t.Unix())
}
return 0
}
func derefInt64(v *int64) int64 {
if v == nil {
return 0
}
return *v
}
// ---- seed JSON 结构 ----
type seedInputStickerSetJSON struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
}
type seedAttrJSON struct {
Type string `json:"_"`
W int `json:"w"`
H int `json:"h"`
Alt string `json:"alt"`
Mask bool `json:"mask"`
Duration float64 `json:"duration"`
RoundMessage bool `json:"round_message"`
SupportsStreaming bool `json:"supports_streaming"`
Voice bool `json:"voice"`
Title string `json:"title"`
Performer string `json:"performer"`
FileName string `json:"file_name"`
Free bool `json:"free"`
TextColor bool `json:"text_color"`
Stickerset *seedInputStickerSetJSON `json:"stickerset"`
}
type seedThumbJSON struct {
Type string `json:"_"`
SizeType string `json:"type"`
W int `json:"w"`
H int `json:"h"`
Size int `json:"size"`
Bytes string `json:"bytes"`
Sizes []int `json:"sizes"`
}
type seedDocumentJSON struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
FileReference string `json:"file_reference"`
Date string `json:"date"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
DCID int `json:"dc_id"`
Attributes []seedAttrJSON `json:"attributes"`
Thumbs []seedThumbJSON `json:"thumbs"`
}
type seedStickerPackJSON struct {
Emoticon string `json:"emoticon"`
Documents []int64 `json:"documents"`
}
type seedStickerSetJSON struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
Title string `json:"title"`
ShortName string `json:"short_name"`
Count int `json:"count"`
Hash int `json:"hash"`
Archived bool `json:"archived"`
Official bool `json:"official"`
Masks bool `json:"masks"`
Emojis bool `json:"emojis"`
Thumbs []seedThumbJSON `json:"thumbs"`
ThumbDCID int `json:"thumb_dc_id"`
ThumbVersion int `json:"thumb_version"`
ThumbDocumentID *int64 `json:"thumb_document_id"`
Packs []seedStickerPackJSON `json:"packs"`
}
type seedStickerSetResultJSON struct {
Set seedStickerSetJSON `json:"set"`
Packs []seedStickerPackJSON `json:"packs"`
Documents []seedDocumentJSON `json:"documents"`
}
type seedReactionJSON struct {
Reaction string `json:"reaction"`
Title string `json:"title"`
Inactive bool `json:"inactive"`
Premium bool `json:"premium"`
StaticIcon *seedDocumentJSON `json:"static_icon"`
AppearAnimation *seedDocumentJSON `json:"appear_animation"`
SelectAnimation *seedDocumentJSON `json:"select_animation"`
ActivateAnimation *seedDocumentJSON `json:"activate_animation"`
EffectAnimation *seedDocumentJSON `json:"effect_animation"`
AroundAnimation *seedDocumentJSON `json:"around_animation"`
CenterIcon *seedDocumentJSON `json:"center_icon"`
}