feat: sync built-in sticker bot

This commit is contained in:
A 2026-07-01 21:55:55 +08:00
parent 7096625e13
commit 6867d201ed
60 changed files with 7063 additions and 144 deletions

View file

@ -49,6 +49,17 @@ type StickerCollectionStore interface {
ClearStickerCollection(ctx context.Context, userID int64, kind domain.StickerCollectionKind) error
}
// UserStickerSetStore persists per-user installed sticker set state.
// Sticker set metadata remains in MediaStore; this store only owns account-local
// installation/archive/order facts.
type UserStickerSetStore interface {
InstallUserStickerSet(ctx context.Context, userID int64, setID int64, kind domain.StickerSetKind, archived bool, installedDate int) error
UninstallUserStickerSet(ctx context.Context, userID int64, setID int64) error
SetUserStickerSetArchived(ctx context.Context, userID int64, setID int64, archived bool, now int) error
ReorderUserStickerSets(ctx context.Context, userID int64, kind domain.StickerSetKind, order []int64, now int) error
ListUserStickerSets(ctx context.Context, userID int64, kind domain.StickerSetKind, archived *bool, offsetID int64, limit int) ([]domain.UserStickerSet, int, error)
}
// SavedMusicStore persists one account's ordered profile music list.
type SavedMusicStore interface {
SaveMusic(ctx context.Context, req domain.SaveMusicRequest) error

View file

@ -44,10 +44,15 @@ type MediaStore interface {
// 贴纸集 / 可用 reaction。
PutStickerSet(ctx context.Context, set domain.StickerSet) error
CreateStickerSet(ctx context.Context, set domain.StickerSet, docs []domain.Document) error
UpdateStickerSet(ctx context.Context, set domain.StickerSet, docs []domain.Document) error
DeleteStickerSet(ctx context.Context, setID int64, creatorUserID int64) error
GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error)
GetStickerSetByShortName(ctx context.Context, shortName string) (domain.StickerSet, bool, error)
GetStickerSetBySystemKey(ctx context.Context, systemKey string) (domain.StickerSet, bool, error)
ListStickerSets(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error)
ListStickerSetsByCreator(ctx context.Context, creatorUserID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error)
StickerSetShortNameAvailable(ctx context.Context, shortName string) (bool, error)
CountStickerSets(ctx context.Context) (int, error)
PutAvailableReaction(ctx context.Context, r domain.AvailableReaction) error
ListAvailableReactions(ctx context.Context) ([]domain.AvailableReaction, error)

View file

@ -12,7 +12,7 @@ import (
// BotStore 是 store.BotStore 的内存实现。bot 的 users 行经注入的 UserStore 创建,
// 与 postgres 实现(单事务建 users+bots 两行)保持可见性一致。
// 内置 BotFather 的 bots 行预置(token 为空 = 不可登录),对齐迁移 0090 种子。
// 内置 service bots 的 bots 行预置(token 为空 = 不可登录),对齐迁移种子。
type BotStore struct {
mu sync.RWMutex
users *UserStore
@ -59,7 +59,13 @@ func NewBotStore(users *UserStore) *BotStore {
emojiPerms: make(map[[2]int64]bool),
customMethod: make(map[string]domain.BotWebViewCustomMethodQuery),
}
s.byID[domain.BotFatherUserID] = domain.BotProfile{
s.byID[domain.BotFatherUserID] = botFatherSeedProfile()
s.byID[domain.StickersBotUserID] = stickersSeedProfile()
return s
}
func botFatherSeedProfile() domain.BotProfile {
return domain.BotProfile{
BotUserID: domain.BotFatherUserID,
OwnerUserID: domain.BotFatherUserID,
Description: "BotFather is the one bot to rule them all. Use it to create new bot accounts and manage your existing bots.",
@ -72,7 +78,25 @@ func NewBotStore(users *UserStore) *BotStore {
{Command: "help", Description: "show help"},
},
}
return s
}
func stickersSeedProfile() domain.BotProfile {
return domain.BotProfile{
BotUserID: domain.StickersBotUserID,
OwnerUserID: domain.StickersBotUserID,
Description: "Create custom sticker and emoji packs for telesrv.",
Commands: []domain.BotCommand{
{Command: "start", Description: "start the sticker pack assistant"},
{Command: "help", Description: "show help"},
{Command: "newpack", Description: "create a sticker pack"},
{Command: "newemoji", Description: "create a custom emoji pack"},
{Command: "addsticker", Description: "add to one of your packs"},
{Command: "delsticker", Description: "remove one item from a pack"},
{Command: "publish", Description: "publish the current pack"},
{Command: "cancel", Description: "cancel the current operation"},
{Command: "packs", Description: "list your created packs"},
},
}
}
func (s *BotStore) CreateBotAccount(ctx context.Context, user domain.User, profile domain.BotProfile) (domain.User, domain.BotProfile, error) {

View file

@ -2,6 +2,7 @@ package memory
import (
"context"
"sort"
"sync"
"telesrv/internal/domain"
)
@ -14,6 +15,7 @@ type PasswordStore struct {
accountSettings map[int64]domain.AccountSettings
notifySettings map[notifySettingsKey]domain.PeerNotifySettings
stickerCollections map[stickerCollectionKey][]domain.StickerCollectionItem
userStickerSets map[int64]map[int64]domain.UserStickerSet
savedMusic map[int64][]domain.Document
businessProfiles map[int64]domain.BusinessProfile
businessChatLinks map[string]domain.BusinessChatLink
@ -48,6 +50,7 @@ func NewPasswordStore() *PasswordStore {
accountSettings: make(map[int64]domain.AccountSettings),
notifySettings: make(map[notifySettingsKey]domain.PeerNotifySettings),
stickerCollections: make(map[stickerCollectionKey][]domain.StickerCollectionItem),
userStickerSets: make(map[int64]map[int64]domain.UserStickerSet),
savedMusic: make(map[int64][]domain.Document),
businessProfiles: make(map[int64]domain.BusinessProfile),
businessChatLinks: make(map[string]domain.BusinessChatLink),
@ -234,6 +237,113 @@ func (s *PasswordStore) ClearStickerCollection(_ context.Context, userID int64,
return nil
}
func (s *PasswordStore) InstallUserStickerSet(_ context.Context, userID int64, setID int64, kind domain.StickerSetKind, archived bool, installedDate int) error {
if userID == 0 || setID == 0 {
return domain.ErrStickerInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
sets := s.userStickerSets[userID]
if sets == nil {
sets = make(map[int64]domain.UserStickerSet)
s.userStickerSets[userID] = sets
}
order := int64(installedDate) << 32
sets[setID] = domain.UserStickerSet{
OwnerUserID: userID,
StickerSetID: setID,
Kind: kind,
Archived: archived,
InstalledDate: installedDate,
OrderValue: order,
}
return nil
}
func (s *PasswordStore) UninstallUserStickerSet(_ context.Context, userID int64, setID int64) error {
s.mu.Lock()
if sets := s.userStickerSets[userID]; sets != nil {
delete(sets, setID)
}
s.mu.Unlock()
return nil
}
func (s *PasswordStore) SetUserStickerSetArchived(_ context.Context, userID int64, setID int64, archived bool, now int) error {
s.mu.Lock()
if sets := s.userStickerSets[userID]; sets != nil {
if item, ok := sets[setID]; ok {
item.Archived = archived
if !archived && now > 0 {
item.OrderValue = int64(now) << 32
}
sets[setID] = item
}
}
s.mu.Unlock()
return nil
}
func (s *PasswordStore) ReorderUserStickerSets(_ context.Context, userID int64, kind domain.StickerSetKind, order []int64, now int) error {
s.mu.Lock()
defer s.mu.Unlock()
sets := s.userStickerSets[userID]
if sets == nil {
return nil
}
orderValue := int64(now) << 32
for _, id := range order {
item, ok := sets[id]
if !ok || item.Kind != kind {
continue
}
item.OrderValue = orderValue
sets[id] = item
orderValue--
}
return nil
}
func (s *PasswordStore) ListUserStickerSets(_ context.Context, userID int64, kind domain.StickerSetKind, archived *bool, offsetID int64, limit int) ([]domain.UserStickerSet, int, error) {
if limit <= 0 || limit > domain.MaxInstalledStickerSets {
limit = domain.MaxInstalledStickerSets
}
s.mu.RLock()
raw := s.userStickerSets[userID]
items := make([]domain.UserStickerSet, 0, len(raw))
for _, item := range raw {
if item.Kind != kind {
continue
}
if archived != nil && item.Archived != *archived {
continue
}
items = append(items, item)
}
s.mu.RUnlock()
sort.SliceStable(items, func(i, j int) bool {
if items[i].OrderValue == items[j].OrderValue {
return items[i].StickerSetID > items[j].StickerSetID
}
return items[i].OrderValue > items[j].OrderValue
})
total := len(items)
if offsetID != 0 {
start := 0
for i, item := range items {
if item.StickerSetID == offsetID {
start = i + 1
break
}
}
items = items[start:]
}
if len(items) > limit {
items = items[:limit]
}
return append([]domain.UserStickerSet(nil), items...), total, nil
}
func (s *PasswordStore) ListNotifyExceptions(_ context.Context, ownerUserID int64) ([]domain.NotifyException, error) {
s.mu.RLock()
defer s.mu.RUnlock()

View file

@ -15,11 +15,11 @@ type UserStore struct {
nextID int64
}
// NewUserStore 创建内存 UserStore。内置系统账号(777000 / BotFather)预置进表,
// 与 postgres 的迁移种子(0005 / 0090)保持双 store 行为一致。
// NewUserStore 创建内存 UserStore。内置系统账号(777000 / BotFather / Stickers)
// 预置进表,与 postgres 的迁移种子保持双 store 行为一致。
func NewUserStore() *UserStore {
s := &UserStore{byID: make(map[int64]domain.User), nextID: domain.UserIDSequenceBase}
for _, id := range []int64{domain.OfficialSystemUserID, domain.BotFatherUserID} {
for _, id := range []int64{domain.OfficialSystemUserID, domain.BotFatherUserID, domain.StickersBotUserID} {
if u, ok := domain.SystemUserByID(id); ok {
s.byID[u.ID] = u
}

View file

@ -559,6 +559,125 @@ func (s *PasswordStore) ClearStickerCollection(ctx context.Context, userID int64
return nil
}
func (s *PasswordStore) InstallUserStickerSet(ctx context.Context, userID int64, setID int64, kind domain.StickerSetKind, archived bool, installedDate int) error {
if userID == 0 || setID == 0 {
return domain.ErrStickerInvalid
}
orderValue := int64(installedDate) << 32
_, err := s.db.Exec(ctx, `
INSERT INTO user_sticker_sets (owner_user_id, sticker_set_id, set_kind, archived, installed_date, order_value, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, now())
ON CONFLICT (owner_user_id, sticker_set_id) DO UPDATE SET
set_kind = EXCLUDED.set_kind,
archived = EXCLUDED.archived,
installed_date = CASE
WHEN user_sticker_sets.installed_date = 0 THEN EXCLUDED.installed_date
ELSE user_sticker_sets.installed_date
END,
order_value = EXCLUDED.order_value,
updated_at = now()`,
userID, setID, string(kind), archived, installedDate, orderValue)
if err != nil {
return fmt.Errorf("install user sticker set: %w", err)
}
return nil
}
func (s *PasswordStore) UninstallUserStickerSet(ctx context.Context, userID int64, setID int64) error {
if _, err := s.db.Exec(ctx, `
DELETE FROM user_sticker_sets
WHERE owner_user_id = $1 AND sticker_set_id = $2`, userID, setID); err != nil {
return fmt.Errorf("uninstall user sticker set: %w", err)
}
return nil
}
func (s *PasswordStore) SetUserStickerSetArchived(ctx context.Context, userID int64, setID int64, archived bool, now int) error {
orderValue := int64(now) << 32
_, err := s.db.Exec(ctx, `
UPDATE user_sticker_sets
SET archived = $3,
order_value = CASE WHEN $3::boolean = false AND $4::bigint > 0 THEN $4::bigint ELSE order_value END,
updated_at = now()
WHERE owner_user_id = $1 AND sticker_set_id = $2`, userID, setID, archived, orderValue)
if err != nil {
return fmt.Errorf("set user sticker set archived: %w", err)
}
return nil
}
func (s *PasswordStore) ReorderUserStickerSets(ctx context.Context, userID int64, kind domain.StickerSetKind, order []int64, now int) error {
if len(order) == 0 {
return nil
}
return withTx(ctx, s.db, "reorder user sticker sets", func(tx pgx.Tx) error {
orderValue := int64(now) << 32
for _, id := range order {
if _, err := tx.Exec(ctx, `
UPDATE user_sticker_sets
SET order_value = $4, updated_at = now()
WHERE owner_user_id = $1 AND set_kind = $2 AND sticker_set_id = $3`,
userID, string(kind), id, orderValue); err != nil {
return fmt.Errorf("update user sticker set order: %w", err)
}
orderValue--
}
return nil
})
}
func (s *PasswordStore) ListUserStickerSets(ctx context.Context, userID int64, kind domain.StickerSetKind, archived *bool, offsetID int64, limit int) ([]domain.UserStickerSet, int, error) {
if userID == 0 {
return nil, 0, nil
}
if limit <= 0 || limit > domain.MaxInstalledStickerSets {
limit = domain.MaxInstalledStickerSets
}
var archivedFilter any
if archived != nil {
archivedFilter = *archived
}
rows, err := s.db.Query(ctx, `
WITH ordered AS (
SELECT owner_user_id, sticker_set_id, set_kind, archived, installed_date, order_value,
ROW_NUMBER() OVER (ORDER BY order_value DESC, sticker_set_id DESC) AS rn,
COUNT(*) OVER () AS total
FROM user_sticker_sets
WHERE owner_user_id = $1
AND set_kind = $2
AND ($3::boolean IS NULL OR archived = $3::boolean)
),
page AS (
SELECT COALESCE((SELECT rn FROM ordered WHERE sticker_set_id = $4), 0) AS offset_rn
)
SELECT owner_user_id, sticker_set_id, set_kind, archived, installed_date, order_value, total
FROM ordered, page
WHERE ordered.rn > page.offset_rn
ORDER BY ordered.rn ASC
LIMIT $5`, userID, string(kind), archivedFilter, offsetID, limit)
if err != nil {
return nil, 0, fmt.Errorf("list user sticker sets: %w", err)
}
defer rows.Close()
out := make([]domain.UserStickerSet, 0, limit)
total := 0
for rows.Next() {
var (
item domain.UserStickerSet
kindText string
)
if err := rows.Scan(&item.OwnerUserID, &item.StickerSetID, &kindText, &item.Archived, &item.InstalledDate, &item.OrderValue, &total); err != nil {
return nil, 0, fmt.Errorf("scan user sticker set: %w", err)
}
item.Kind = domain.StickerSetKind(kindText)
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, 0, fmt.Errorf("iterate user sticker sets: %w", err)
}
return out, total, nil
}
func nullableBool(v *bool) any {
if v == nil {
return nil

View file

@ -8,7 +8,9 @@ import (
"sync"
"time"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"telesrv/internal/domain"
@ -237,15 +239,27 @@ ON CONFLICT (key) DO UPDATE SET
// ---- 文档 ----
func (s *MediaStore) PutDocument(ctx context.Context, doc domain.Document) error {
attrs, err := jsonArrayOrEmpty(doc.Attributes)
params, err := putDocumentParams(doc)
if err != nil {
return err
}
if err := s.q.PutDocument(ctx, params); err != nil {
return err
}
s.documents.put(doc.ID, doc)
return nil
}
func putDocumentParams(doc domain.Document) (sqlcgen.PutDocumentParams, error) {
attrs, err := jsonArrayOrEmpty(doc.Attributes)
if err != nil {
return sqlcgen.PutDocumentParams{}, err
}
thumbs, err := jsonArrayOrEmpty(doc.Thumbs)
if err != nil {
return err
return sqlcgen.PutDocumentParams{}, err
}
if err := s.q.PutDocument(ctx, sqlcgen.PutDocumentParams{
return sqlcgen.PutDocumentParams{
ID: doc.ID,
AccessHash: doc.AccessHash,
FileReference: bytesOrEmpty(doc.FileReference),
@ -255,11 +269,7 @@ func (s *MediaStore) PutDocument(ctx context.Context, doc domain.Document) error
DcID: int32(doc.DCID),
AttributesJson: attrs,
ThumbsJson: thumbs,
}); err != nil {
return err
}
s.documents.put(doc.ID, doc)
return nil
}, nil
}
func (s *MediaStore) GetDocument(ctx context.Context, id int64) (domain.Document, bool, error) {
@ -559,53 +569,399 @@ func (s *MediaStore) PutStickerSet(ctx context.Context, set domain.StickerSet) e
})
}
func (s *MediaStore) GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
func (s *MediaStore) CreateStickerSet(ctx context.Context, set domain.StickerSet, docs []domain.Document) error {
err := withTx(ctx, s.db, "create sticker set", func(tx pgx.Tx) error {
if err := insertStickerSet(ctx, tx, set); err != nil {
return err
}
return domain.StickerSet{}, false, err
qtx := s.q.WithTx(tx)
for _, doc := range docs {
params, err := putDocumentParams(doc)
if err != nil {
return err
}
if err := qtx.PutDocument(ctx, params); err != nil {
return err
}
}
return nil
})
if err != nil {
if stickerSetShortNameConflict(err) {
return domain.ErrStickerSetShortNameOccupied
}
return err
}
return stickerSetFromRow(row)
for _, doc := range docs {
s.documents.put(doc.ID, doc)
}
return nil
}
func (s *MediaStore) UpdateStickerSet(ctx context.Context, set domain.StickerSet, docs []domain.Document) error {
err := withTx(ctx, s.db, "update sticker set", func(tx pgx.Tx) error {
if err := updateStickerSet(ctx, tx, set); err != nil {
return err
}
qtx := s.q.WithTx(tx)
for _, doc := range docs {
params, err := putDocumentParams(doc)
if err != nil {
return err
}
if err := qtx.PutDocument(ctx, params); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
for _, doc := range docs {
s.documents.put(doc.ID, doc)
}
return nil
}
func (s *MediaStore) DeleteStickerSet(ctx context.Context, setID int64, creatorUserID int64) error {
return withTx(ctx, s.db, "delete sticker set", func(tx pgx.Tx) error {
tag, err := tx.Exec(ctx, `
UPDATE sticker_sets
SET deleted = true, updated_at = now()
WHERE id = $1
AND creator_user_id = $2
AND deleted = false`, setID, creatorUserID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return domain.ErrStickerSetInvalid
}
_, err = tx.Exec(ctx, `DELETE FROM user_sticker_sets WHERE sticker_set_id = $1`, setID)
return err
})
}
func insertStickerSet(ctx context.Context, db sqlcgen.DBTX, set domain.StickerSet) error {
thumbs, err := jsonArrayOrEmpty(set.Thumbs)
if err != nil {
return err
}
docIDs, err := jsonArrayOrEmpty(set.DocumentIDs)
if err != nil {
return err
}
packs, err := jsonArrayOrEmpty(set.Packs)
if err != nil {
return err
}
keywords, err := jsonArrayOrEmpty(set.Keywords)
if err != nil {
return err
}
kind := string(set.Kind)
if kind == "" {
kind = string(domain.StickerSetKindStickers)
}
_, err = db.Exec(ctx, `
INSERT INTO sticker_sets (
id, access_hash, short_name, title, count, hash, set_kind,
official, animated, videos, emojis, masks, text_color, creator_user_id,
installed, archived, deleted, installed_date,
thumb_document_id, thumbs, thumb_dc_id, thumb_version,
document_ids, packs, keywords, sort_order, system_key, software, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18,
$19, $20::jsonb, $21, $22,
$23::jsonb, $24::jsonb, $25::jsonb, $26, $27, $28, now()
)`,
set.ID, set.AccessHash, set.ShortName, set.Title, set.Count, set.Hash, kind,
set.Official, set.Animated, set.Videos, set.Emojis, set.Masks, set.TextColor, set.CreatorUserID,
set.Installed, set.Archived, set.Deleted, set.InstalledDate,
set.ThumbDocumentID, thumbs, set.ThumbDCID, set.ThumbVersion,
docIDs, packs, keywords, set.SortOrder, set.SystemKey, set.Software,
)
return err
}
func updateStickerSet(ctx context.Context, db sqlcgen.DBTX, set domain.StickerSet) error {
thumbs, err := jsonArrayOrEmpty(set.Thumbs)
if err != nil {
return err
}
docIDs, err := jsonArrayOrEmpty(set.DocumentIDs)
if err != nil {
return err
}
packs, err := jsonArrayOrEmpty(set.Packs)
if err != nil {
return err
}
keywords, err := jsonArrayOrEmpty(set.Keywords)
if err != nil {
return err
}
kind := string(set.Kind)
if kind == "" {
kind = string(domain.StickerSetKindStickers)
}
tag, err := db.Exec(ctx, `
UPDATE sticker_sets
SET title = $3,
count = $4,
hash = $5,
set_kind = $6,
official = $7,
animated = $8,
videos = $9,
emojis = $10,
masks = $11,
text_color = $12,
installed = $13,
archived = $14,
installed_date = $15,
thumb_document_id = $16,
thumbs = $17::jsonb,
thumb_dc_id = $18,
thumb_version = $19,
document_ids = $20::jsonb,
packs = $21::jsonb,
keywords = $22::jsonb,
sort_order = $23,
software = $24,
updated_at = now()
WHERE id = $1
AND access_hash = $2
AND deleted = false`,
set.ID, set.AccessHash,
set.Title, set.Count, set.Hash, kind,
set.Official, set.Animated, set.Videos, set.Emojis, set.Masks, set.TextColor,
set.Installed, set.Archived, set.InstalledDate,
set.ThumbDocumentID, thumbs, set.ThumbDCID, set.ThumbVersion,
docIDs, packs, keywords, set.SortOrder, set.Software,
)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return domain.ErrStickerSetInvalid
}
return nil
}
func stickerSetShortNameConflict(err error) bool {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) || pgErr.Code != pgerrcode.UniqueViolation {
return false
}
switch pgErr.ConstraintName {
case "sticker_sets_short_name_lower_idx", "sticker_sets_short_name_idx":
return true
default:
return false
}
}
func (s *MediaStore) GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error) {
return queryStickerSet(ctx, s.db, "id = $1", id)
}
func (s *MediaStore) GetStickerSetByShortName(ctx context.Context, shortName string) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetByShortName(ctx, shortName)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
return queryStickerSet(ctx, s.db, "lower(short_name) = lower($1)", shortName)
}
func (s *MediaStore) GetStickerSetBySystemKey(ctx context.Context, systemKey string) (domain.StickerSet, bool, error) {
row, err := s.q.GetStickerSetBySystemKey(ctx, systemKey)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
return queryStickerSet(ctx, s.db, "system_key = $1", systemKey)
}
func (s *MediaStore) ListStickerSets(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error) {
rows, err := s.q.ListStickerSetsByKind(ctx, string(kind))
rows, err := s.db.Query(ctx, stickerSetSelectSQL+`
WHERE set_kind = $1 AND deleted = false
ORDER BY sort_order ASC, id ASC`, string(kind))
if err != nil {
return nil, err
}
out := make([]domain.StickerSet, 0, len(rows))
for _, r := range rows {
set, _, err := stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(r))
defer rows.Close()
out := []domain.StickerSet{}
for rows.Next() {
set, err := scanStickerSet(rows)
if err != nil {
return nil, err
}
out = append(out, set)
}
return out, nil
return out, rows.Err()
}
func (s *MediaStore) ListStickerSetsByCreator(ctx context.Context, creatorUserID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error) {
if limit <= 0 {
limit = domain.MaxCreatedStickerSets
}
if limit > domain.MaxCreatedStickerSets {
limit = domain.MaxCreatedStickerSets
}
var total int
if err := s.db.QueryRow(ctx, `
SELECT count(*)::int
FROM sticker_sets
WHERE creator_user_id = $1 AND deleted = false`, creatorUserID).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := s.db.Query(ctx, stickerSetSelectSQL+`
WHERE creator_user_id = $1
AND deleted = false
AND ($2::bigint = 0 OR id < $2::bigint)
ORDER BY id DESC
LIMIT $3`, creatorUserID, offsetID, limit)
if err != nil {
return nil, 0, err
}
defer rows.Close()
out := []domain.StickerSet{}
for rows.Next() {
set, err := scanStickerSet(rows)
if err != nil {
return nil, 0, err
}
set.Creator = true
out = append(out, set)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return out, total, nil
}
func (s *MediaStore) StickerSetShortNameAvailable(ctx context.Context, shortName string) (bool, error) {
var exists bool
if err := s.db.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM sticker_sets
WHERE lower(short_name) = lower($1)
AND short_name <> ''
AND deleted = false
)`, shortName).Scan(&exists); err != nil {
return false, err
}
return !exists, nil
}
const stickerSetSelectSQL = `
SELECT
id, access_hash, short_name, title, count, hash, set_kind,
official, animated, videos, emojis, masks, installed, archived, installed_date,
thumb_document_id, thumbs::text AS thumbs_json, thumb_dc_id, thumb_version,
document_ids::text AS document_ids_json, packs::text AS packs_json, sort_order, system_key,
creator_user_id, text_color, deleted, software, keywords::text AS keywords_json
FROM sticker_sets
`
type stickerSetScanner interface {
Scan(dest ...any) error
}
func queryStickerSet(ctx context.Context, db sqlcgen.DBTX, predicate string, args ...any) (domain.StickerSet, bool, error) {
row := db.QueryRow(ctx, stickerSetSelectSQL+"WHERE "+predicate+" AND deleted = false LIMIT 1", args...)
set, err := scanStickerSet(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.StickerSet{}, false, nil
}
return domain.StickerSet{}, false, err
}
return set, true, nil
}
func scanStickerSet(row stickerSetScanner) (domain.StickerSet, error) {
var (
id int64
accessHash int64
shortName string
title string
count int32
hash int32
kind string
official bool
animated bool
videos bool
emojis bool
masks bool
installed bool
archived bool
installedDate int32
thumbDocumentID int64
thumbsJSON string
thumbDCID int32
thumbVersion int32
docIDsJSON string
packsJSON string
sortOrder int32
systemKey string
creatorUserID int64
textColor bool
deleted bool
software string
keywordsJSON string
)
if err := row.Scan(
&id, &accessHash, &shortName, &title, &count, &hash, &kind,
&official, &animated, &videos, &emojis, &masks, &installed, &archived, &installedDate,
&thumbDocumentID, &thumbsJSON, &thumbDCID, &thumbVersion,
&docIDsJSON, &packsJSON, &sortOrder, &systemKey,
&creatorUserID, &textColor, &deleted, &software, &keywordsJSON,
); err != nil {
return domain.StickerSet{}, err
}
thumbs, err := decodePhotoSizes(thumbsJSON)
if err != nil {
return domain.StickerSet{}, err
}
docIDs, err := decodeInt64Slice(docIDsJSON)
if err != nil {
return domain.StickerSet{}, err
}
packs, err := decodeStickerPacks(packsJSON)
if err != nil {
return domain.StickerSet{}, err
}
keywords, err := decodeStickerKeywords(keywordsJSON)
if err != nil {
return domain.StickerSet{}, err
}
return domain.StickerSet{
ID: id,
AccessHash: accessHash,
ShortName: shortName,
Title: title,
Count: int(count),
Hash: int(hash),
Kind: domain.StickerSetKind(kind),
Official: official,
Animated: animated,
Videos: videos,
Emojis: emojis,
Masks: masks,
TextColor: textColor,
CreatorUserID: creatorUserID,
Installed: installed,
Archived: archived,
Deleted: deleted,
InstalledDate: int(installedDate),
ThumbDocumentID: thumbDocumentID,
Thumbs: thumbs,
ThumbDCID: int(thumbDCID),
ThumbVersion: int(thumbVersion),
DocumentIDs: docIDs,
Packs: packs,
Keywords: keywords,
SortOrder: int(sortOrder),
SystemKey: systemKey,
Software: software,
}, nil
}
func (s *MediaStore) CountStickerSets(ctx context.Context) (int, error) {

View file

@ -136,3 +136,14 @@ func decodeStickerPacks(s string) ([]domain.StickerPack, error) {
}
return out, nil
}
func decodeStickerKeywords(s string) ([]domain.StickerKeyword, error) {
if s == "" || s == "[]" || s == "null" {
return nil, nil
}
var out []domain.StickerKeyword
if err := json.Unmarshal([]byte(s), &out); err != nil {
return nil, err
}
return out, nil
}