feat: sync built-in sticker bot
This commit is contained in:
parent
7096625e13
commit
6867d201ed
60 changed files with 7063 additions and 144 deletions
|
|
@ -1,5 +1,10 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 本文件定义媒体相关的业务值对象(文档、照片、贴纸集、可用 reaction、消息媒体)。
|
||||
// 这些类型完全不依赖 tg.*;rpc 层负责 domain↔tg 转换。
|
||||
//
|
||||
|
|
@ -238,6 +243,91 @@ func (d Document) IsSticker() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (d Document) IsCustomEmoji() bool {
|
||||
for _, attr := range d.Attributes {
|
||||
if attr.Kind == DocAttrCustomEmoji {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d Document) IsStickerLike() bool {
|
||||
return d.IsSticker() || d.IsCustomEmoji()
|
||||
}
|
||||
|
||||
// MaxStickerMaterialDocumentSize bounds uploaded documents that can be promoted
|
||||
// into user-created sticker/custom emoji sets without server-side transcoding.
|
||||
const MaxStickerMaterialDocumentSize int64 = 20 << 20
|
||||
|
||||
// IsStickerSetMaterial reports whether an existing Document can be used as an
|
||||
// input item for stickers.createStickerSet/stickers.addStickerToSet. Already
|
||||
// tagged sticker/custom emoji documents always pass. Untagged TGS/WebP/Lottie
|
||||
// JSON uploads can be shaped later from their MIME/body; video uploads must
|
||||
// already carry a video attribute because this layer does not transcode or
|
||||
// probe containers.
|
||||
func (d Document) IsStickerSetMaterial() bool {
|
||||
if d.IsStickerLike() {
|
||||
return true
|
||||
}
|
||||
if d.ID == 0 || d.AccessHash == 0 || d.Size > MaxStickerMaterialDocumentSize {
|
||||
return false
|
||||
}
|
||||
switch d.stickerMaterialMimeOrExt() {
|
||||
case "application/x-tgsticker", "image/webp", "application/json":
|
||||
return true
|
||||
case "video/webm", "video/mp4":
|
||||
return d.hasDocumentAttribute(DocAttrVideo)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (d Document) hasDocumentAttribute(kind DocumentAttributeKind) bool {
|
||||
for _, attr := range d.Attributes {
|
||||
if attr.Kind == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StickerSetMaterialMime returns the normalized sticker material MIME inferred
|
||||
// from the document MIME type or a filename attribute.
|
||||
func (d Document) StickerSetMaterialMime() string {
|
||||
return d.stickerMaterialMimeOrExt()
|
||||
}
|
||||
|
||||
func (d Document) stickerMaterialMimeOrExt() string {
|
||||
mimeType := strings.ToLower(strings.TrimSpace(d.MimeType))
|
||||
switch mimeType {
|
||||
case "application/x-tgsticker", "image/webp", "video/webm", "video/mp4",
|
||||
"application/json", "application/lottie+json", "text/json":
|
||||
if mimeType == "application/lottie+json" || mimeType == "text/json" {
|
||||
return "application/json"
|
||||
}
|
||||
return mimeType
|
||||
}
|
||||
for _, attr := range d.Attributes {
|
||||
if attr.Kind != DocAttrFilename {
|
||||
continue
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(strings.TrimSpace(attr.FileName))) {
|
||||
case ".tgs":
|
||||
return "application/x-tgsticker"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".webm":
|
||||
return "video/webm"
|
||||
case ".mp4":
|
||||
return "video/mp4"
|
||||
}
|
||||
}
|
||||
return mimeType
|
||||
}
|
||||
|
||||
// IsGif reports whether the document is a savable GIF (documentAttributeAnimated).
|
||||
func (d Document) IsGif() bool {
|
||||
for _, attr := range d.Attributes {
|
||||
|
|
@ -558,6 +648,11 @@ type StickerPack struct {
|
|||
DocumentIDs []int64 `json:"document_ids"`
|
||||
}
|
||||
|
||||
type StickerKeyword struct {
|
||||
DocumentID int64 `json:"document_id"`
|
||||
Keywords []string `json:"keywords"`
|
||||
}
|
||||
|
||||
// StickerSetSystemKeyEmojiDefaultStatuses 是 inputStickerSetEmojiDefaultStatuses
|
||||
// 对应的系统集标识:premium 用户 emoji status 选择器的"默认状态"主体集
|
||||
// (messages.getStickerSet 与 account.getDefaultEmojiStatuses 共用)。
|
||||
|
|
@ -576,32 +671,67 @@ const (
|
|||
|
||||
// StickerSet 是贴纸/自定义 emoji 集的元数据 + 有序文档 id。
|
||||
type StickerSet struct {
|
||||
ID int64 `json:"id"`
|
||||
AccessHash int64 `json:"access_hash"`
|
||||
ShortName string `json:"short_name"`
|
||||
Title string `json:"title"`
|
||||
Count int `json:"count"`
|
||||
Hash int `json:"hash"`
|
||||
Kind StickerSetKind `json:"set_kind"`
|
||||
Official bool `json:"official,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
Videos bool `json:"videos,omitempty"`
|
||||
Emojis bool `json:"emojis,omitempty"`
|
||||
Masks bool `json:"masks,omitempty"`
|
||||
Installed bool `json:"installed,omitempty"`
|
||||
Archived bool `json:"archived,omitempty"`
|
||||
InstalledDate int `json:"installed_date,omitempty"`
|
||||
ThumbDocumentID int64 `json:"thumb_document_id,omitempty"`
|
||||
Thumbs []PhotoSize `json:"thumbs,omitempty"`
|
||||
ThumbDCID int `json:"thumb_dc_id,omitempty"`
|
||||
ThumbVersion int `json:"thumb_version,omitempty"`
|
||||
DocumentIDs []int64 `json:"document_ids,omitempty"`
|
||||
Packs []StickerPack `json:"packs,omitempty"`
|
||||
SortOrder int `json:"sort_order,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
AccessHash int64 `json:"access_hash"`
|
||||
ShortName string `json:"short_name"`
|
||||
Title string `json:"title"`
|
||||
Count int `json:"count"`
|
||||
Hash int `json:"hash"`
|
||||
Kind StickerSetKind `json:"set_kind"`
|
||||
Official bool `json:"official,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
Videos bool `json:"videos,omitempty"`
|
||||
Emojis bool `json:"emojis,omitempty"`
|
||||
Masks bool `json:"masks,omitempty"`
|
||||
TextColor bool `json:"text_color,omitempty"`
|
||||
Creator bool `json:"creator,omitempty"`
|
||||
CreatorUserID int64 `json:"creator_user_id,omitempty"`
|
||||
Installed bool `json:"installed,omitempty"`
|
||||
Archived bool `json:"archived,omitempty"`
|
||||
Deleted bool `json:"deleted,omitempty"`
|
||||
InstalledDate int `json:"installed_date,omitempty"`
|
||||
ThumbDocumentID int64 `json:"thumb_document_id,omitempty"`
|
||||
Thumbs []PhotoSize `json:"thumbs,omitempty"`
|
||||
ThumbDCID int `json:"thumb_dc_id,omitempty"`
|
||||
ThumbVersion int `json:"thumb_version,omitempty"`
|
||||
DocumentIDs []int64 `json:"document_ids,omitempty"`
|
||||
Packs []StickerPack `json:"packs,omitempty"`
|
||||
Keywords []StickerKeyword `json:"keywords,omitempty"`
|
||||
SortOrder int `json:"sort_order,omitempty"`
|
||||
Software string `json:"software,omitempty"`
|
||||
// SystemKey 是 TDesktop 系统集的稳定标识(如 "animated_emoji"、"dice:🎲"),用于 InputStickerSet* 路由。
|
||||
SystemKey string `json:"system_key,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
MinStickerSetShortNameLen = 5
|
||||
MaxStickerSetShortNameLen = 32
|
||||
MaxStickerSetTitleLen = 64
|
||||
MaxStickerSetItems = 120
|
||||
MaxStickerSetKeywords = 20
|
||||
MaxStickerSetKeywordLen = 32
|
||||
)
|
||||
|
||||
type StickerSetItemInput struct {
|
||||
DocumentID int64
|
||||
DocumentAccessHash int64
|
||||
Emoji string
|
||||
Keywords string
|
||||
}
|
||||
|
||||
type CreateStickerSetRequest struct {
|
||||
CreatorUserID int64
|
||||
Title string
|
||||
ShortName string
|
||||
Kind StickerSetKind
|
||||
TextColor bool
|
||||
ThumbDocumentID int64
|
||||
ThumbAccessHash int64
|
||||
Items []StickerSetItemInput
|
||||
Software string
|
||||
Date int
|
||||
}
|
||||
|
||||
// ProfilePhotoKind distinguishes a user's real profile photo from the fallback
|
||||
// public photo shown when privacy hides the real one.
|
||||
type ProfilePhotoKind string
|
||||
|
|
|
|||
74
internal/domain/media_test.go
Normal file
74
internal/domain/media_test.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDocumentIsStickerSetMaterialAcceptsUploadedFormats(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
doc Document
|
||||
want bool
|
||||
wantMime string
|
||||
}{
|
||||
{
|
||||
name: "existing sticker",
|
||||
doc: Document{ID: 1, AccessHash: 11, Size: MaxStickerMaterialDocumentSize + 1, Attributes: []DocumentAttribute{{Kind: DocAttrSticker}}},
|
||||
want: true,
|
||||
wantMime: "",
|
||||
},
|
||||
{
|
||||
name: "tgs mime",
|
||||
doc: Document{ID: 2, AccessHash: 22, MimeType: "application/x-tgsticker", Size: 4096},
|
||||
want: true,
|
||||
wantMime: "application/x-tgsticker",
|
||||
},
|
||||
{
|
||||
name: "webp extension",
|
||||
doc: Document{ID: 3, AccessHash: 33, MimeType: "application/octet-stream", Size: 4096, Attributes: []DocumentAttribute{{Kind: DocAttrFilename, FileName: "sticker.WEBP"}}},
|
||||
want: true,
|
||||
wantMime: "image/webp",
|
||||
},
|
||||
{
|
||||
name: "lottie json mime",
|
||||
doc: Document{ID: 31, AccessHash: 331, MimeType: "application/lottie+json", Size: 4096},
|
||||
want: true,
|
||||
wantMime: "application/json",
|
||||
},
|
||||
{
|
||||
name: "lottie json extension",
|
||||
doc: Document{ID: 32, AccessHash: 332, MimeType: "application/octet-stream", Size: 4096, Attributes: []DocumentAttribute{{Kind: DocAttrFilename, FileName: "wave.JSON"}}},
|
||||
want: true,
|
||||
wantMime: "application/json",
|
||||
},
|
||||
{
|
||||
name: "mp4 mime",
|
||||
doc: Document{ID: 4, AccessHash: 44, MimeType: "video/mp4", Size: 4096, Attributes: []DocumentAttribute{{Kind: DocAttrVideo, W: 512, H: 512, Duration: 1}}},
|
||||
want: true,
|
||||
wantMime: "video/mp4",
|
||||
},
|
||||
{
|
||||
name: "mp4 without video attribute",
|
||||
doc: Document{ID: 40, AccessHash: 440, MimeType: "video/mp4", Size: 4096},
|
||||
wantMime: "video/mp4",
|
||||
},
|
||||
{
|
||||
name: "arbitrary file",
|
||||
doc: Document{ID: 5, AccessHash: 55, MimeType: "application/pdf", Size: 4096},
|
||||
wantMime: "application/pdf",
|
||||
},
|
||||
{
|
||||
name: "oversized upload",
|
||||
doc: Document{ID: 6, AccessHash: 66, MimeType: "image/webp", Size: MaxStickerMaterialDocumentSize + 1},
|
||||
wantMime: "image/webp",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.doc.IsStickerSetMaterial(); got != tt.want {
|
||||
t.Fatalf("IsStickerSetMaterial() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got := tt.doc.StickerSetMaterialMime(); got != tt.wantMime {
|
||||
t.Fatalf("StickerSetMaterialMime() = %q, want %q", got, tt.wantMime)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,21 @@ import "errors"
|
|||
// ErrStickerInvalid 表示输入文档不是合法的贴纸/GIF(faveSticker/saveGif 等)。
|
||||
var ErrStickerInvalid = errors.New("sticker document invalid")
|
||||
|
||||
var (
|
||||
ErrStickerSetTitleInvalid = errors.New("sticker set title invalid")
|
||||
ErrStickerSetShortNameInvalid = errors.New("sticker set short name invalid")
|
||||
ErrStickerSetShortNameOccupied = errors.New("sticker set short name occupied")
|
||||
ErrStickerSetTypeInvalid = errors.New("sticker set type invalid")
|
||||
ErrStickerSetEmpty = errors.New("sticker set empty")
|
||||
ErrStickerSetTooMuch = errors.New("sticker set too much")
|
||||
ErrStickerSetEmojiInvalid = errors.New("sticker set emoji invalid")
|
||||
ErrStickerSetFileInvalid = errors.New("sticker set file invalid")
|
||||
ErrStickerSetCreatorInvalid = errors.New("sticker set creator invalid")
|
||||
ErrStickerSetInvalid = errors.New("sticker set invalid")
|
||||
ErrStickerSetNotOwned = errors.New("sticker set not owned")
|
||||
ErrStickerSetPositionInvalid = errors.New("sticker set position invalid")
|
||||
)
|
||||
|
||||
// StickerCollectionKind 区分一个用户的几类个人贴纸集合。
|
||||
type StickerCollectionKind string
|
||||
|
||||
|
|
@ -22,9 +37,11 @@ const (
|
|||
// 各集合容量上界(最新在前,超出截断最旧)。仅为限制存储增长,非严格 Telegram 配额;
|
||||
// faved 不做 premium 分档(用户决策范围外),统一一个上界。
|
||||
const (
|
||||
MaxFavedStickers = 100
|
||||
MaxRecentStickers = 30
|
||||
MaxSavedGifs = 200
|
||||
MaxFavedStickers = 100
|
||||
MaxRecentStickers = 30
|
||||
MaxSavedGifs = 200
|
||||
MaxInstalledStickerSets = 200
|
||||
MaxCreatedStickerSets = 200
|
||||
)
|
||||
|
||||
// MaxStickerCollectionItems 返回某类集合的容量上界。
|
||||
|
|
@ -44,3 +61,14 @@ type StickerCollectionItem struct {
|
|||
DocumentID int64
|
||||
Date int
|
||||
}
|
||||
|
||||
// UserStickerSet 是某个账号安装/归档的一条 sticker set 状态。
|
||||
// set 元数据、文档和 packs 仍由 media/files 模块维护;这里仅保存用户视角。
|
||||
type UserStickerSet struct {
|
||||
OwnerUserID int64
|
||||
StickerSetID int64
|
||||
Kind StickerSetKind
|
||||
Archived bool
|
||||
InstalledDate int
|
||||
OrderValue int64
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ const (
|
|||
BotFatherUserID int64 = 93372553
|
||||
// BotFatherAccessHash 固定不变;与迁移 0090 的种子行双写,必须保持一致。
|
||||
BotFatherAccessHash int64 = 7421896403922962293
|
||||
|
||||
// StickersBotUserID 是内置 @Stickers 账号。它是 server 内置 service bot,
|
||||
// 不走外部 Bot API 进程。
|
||||
StickersBotUserID int64 = 1063110917
|
||||
// StickersBotAccessHash 固定不变;与 postgres 种子行双写,必须保持一致。
|
||||
StickersBotAccessHash int64 = 5213187021149032991
|
||||
)
|
||||
|
||||
// OfficialSystemUser 返回第一阶段内置的官方系统账号。
|
||||
|
|
@ -36,6 +42,19 @@ func BotFatherUser() User {
|
|||
}
|
||||
}
|
||||
|
||||
// StickersBotUser 返回内置 @Stickers 账号。username 不以 bot 结尾属种子例外(与官方一致)。
|
||||
func StickersBotUser() User {
|
||||
return User{
|
||||
ID: StickersBotUserID,
|
||||
AccessHash: StickersBotAccessHash,
|
||||
FirstName: "Stickers",
|
||||
Username: "Stickers",
|
||||
Verified: true,
|
||||
Bot: true,
|
||||
BotInfoVersion: 2,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemUserByID 返回内置系统账号;非系统账号返回 ok=false。
|
||||
// 所有对 777000 的硬编码注入点统一经此函数,新增内置账号只改这里。
|
||||
func SystemUserByID(id int64) (User, bool) {
|
||||
|
|
@ -44,6 +63,8 @@ func SystemUserByID(id int64) (User, bool) {
|
|||
return OfficialSystemUser(), true
|
||||
case BotFatherUserID:
|
||||
return BotFatherUser(), true
|
||||
case StickersBotUserID:
|
||||
return StickersBotUser(), true
|
||||
}
|
||||
return User{}, false
|
||||
}
|
||||
|
|
@ -55,7 +76,7 @@ func IsSystemUserID(id int64) bool {
|
|||
|
||||
func SystemUserByPhone(phone string) (User, bool) {
|
||||
phone = NormalizePhone(phone)
|
||||
for _, id := range []int64{OfficialSystemUserID, BotFatherUserID} {
|
||||
for _, id := range []int64{OfficialSystemUserID, BotFatherUserID, StickersBotUserID} {
|
||||
u, ok := SystemUserByID(id)
|
||||
if !ok || u.Phone == "" {
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue