feat: sync built-in sticker bot
This commit is contained in:
parent
7096625e13
commit
6867d201ed
60 changed files with 7063 additions and 144 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue