feat: sync built-in sticker bot
This commit is contained in:
parent
7096625e13
commit
6867d201ed
60 changed files with 7063 additions and 144 deletions
|
|
@ -11,7 +11,8 @@ import (
|
|||
)
|
||||
|
||||
// 本文件实现 app/bots 的 RouterHooks 回调:token revoke 后的 session 失效闭环,
|
||||
// 以及命令变更后的 updateBotCommands 在线推送。Router 创建后经
|
||||
// 命令变更后的 updateBotCommands 在线推送,以及 @Stickers 发布后的
|
||||
// updateStickerSets 在线提示。Router 创建后经
|
||||
// botsService.SetRouterHooks(router) 装配(见 cmd/telesrv/main.go)。
|
||||
|
||||
// maxBotCommandsPushPeers 限制单次命令变更的推送扇出(bot 的最近 dialog peer 数)。
|
||||
|
|
@ -60,6 +61,26 @@ func (r *Router) PushBotCommandsChanged(ctx context.Context, botUserID int64, co
|
|||
go r.pushBotCommandsChanged(context.WithoutCancel(ctx), botUserID, cmds)
|
||||
}
|
||||
|
||||
// PushStickerSetsChanged 给单个用户在线 session 推 updateStickerSets。该 update 无
|
||||
// pts,不进 getDifference;权威安装态已写 user_sticker_sets,离线端下次
|
||||
// messages.getAllStickers/messages.getEmojiStickers 会重建。
|
||||
func (r *Router) PushStickerSetsChanged(ctx context.Context, userID int64, kind domain.StickerSetKind) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
r.invalidateStickerCatalog(kind)
|
||||
go func() {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
r.log.Error("push sticker sets panicked", zap.Int64("user_id", userID), zap.Any("panic", rec))
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer cancel()
|
||||
r.pushStickerSetsUpdate(ctx, userID, kind)
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *Router) pushBotCommandsChanged(ctx context.Context, botUserID int64, commands []domain.BotCommand) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
|
|
|
|||
|
|
@ -578,6 +578,8 @@ func tgStickerSet(set domain.StickerSet) tg.StickerSet {
|
|||
Official: set.Official,
|
||||
Masks: set.Masks,
|
||||
Emojis: set.Emojis,
|
||||
TextColor: set.TextColor,
|
||||
Creator: set.Creator,
|
||||
ID: set.ID,
|
||||
AccessHash: set.AccessHash,
|
||||
Title: set.Title,
|
||||
|
|
@ -633,12 +635,23 @@ func tgStickerPacks(packs []domain.StickerPack) []tg.StickerPack {
|
|||
return out
|
||||
}
|
||||
|
||||
func tgStickerKeywords(keywords []domain.StickerKeyword) []tg.StickerKeyword {
|
||||
out := make([]tg.StickerKeyword, 0, len(keywords))
|
||||
for _, kw := range keywords {
|
||||
if kw.DocumentID == 0 || len(kw.Keywords) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, tg.StickerKeyword{DocumentID: kw.DocumentID, Keyword: append([]string(nil), kw.Keywords...)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// tgMessagesStickerSet 构造完整 messages.stickerSet(set + packs + documents)。
|
||||
func tgMessagesStickerSet(set domain.StickerSet, docs []domain.Document) *tg.MessagesStickerSet {
|
||||
return &tg.MessagesStickerSet{
|
||||
Set: tgStickerSet(set),
|
||||
Packs: tgStickerPacks(set.Packs),
|
||||
Keywords: []tg.StickerKeyword{},
|
||||
Keywords: tgStickerKeywords(set.Keywords),
|
||||
Documents: tgDocuments(docs),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ func applyTgUserBotFields(out *tg.User, u domain.User) {
|
|||
version = 1
|
||||
}
|
||||
out.SetBotInfoVersion(version)
|
||||
if u.ID != domain.BotFatherUserID {
|
||||
if !domain.IsSystemUserID(u.ID) {
|
||||
out.SetBotBusiness(true)
|
||||
}
|
||||
out.Phone = ""
|
||||
|
|
|
|||
|
|
@ -607,6 +607,15 @@ type FilesService interface {
|
|||
GetDocuments(ctx context.Context, ids []int64) ([]domain.Document, error)
|
||||
ResolveStickerSet(ctx context.Context, ref domain.StickerSetRef) (set domain.StickerSet, documents []domain.Document, found bool, err error)
|
||||
ListStickerSets(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error)
|
||||
CheckStickerSetShortName(ctx context.Context, shortName string) (bool, error)
|
||||
SuggestStickerSetShortName(ctx context.Context, title string, userID int64) (string, error)
|
||||
CreateStickerSet(ctx context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error)
|
||||
ListCreatedStickerSets(ctx context.Context, userID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error)
|
||||
AddStickerToSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error)
|
||||
RemoveStickerFromSet(ctx context.Context, actorUserID int64, documentID int64, accessHash int64) (domain.StickerSet, []domain.Document, error)
|
||||
ChangeStickerPosition(ctx context.Context, actorUserID int64, documentID int64, accessHash int64, position int) (domain.StickerSet, []domain.Document, error)
|
||||
RenameStickerSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef, title string) (domain.StickerSet, []domain.Document, error)
|
||||
DeleteStickerSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef) (domain.StickerSetKind, error)
|
||||
// 头像(profile photo)与消息媒体组装。
|
||||
CreatePhotoFromUpload(ctx context.Context, file domain.UploadedFileRef) (domain.Photo, error)
|
||||
CreatePhotoFromBytes(ctx context.Context, data []byte) (domain.Photo, error)
|
||||
|
|
|
|||
|
|
@ -517,17 +517,12 @@ func (r *Router) validateRequiredSavedHistoryParentPeer(ctx context.Context, use
|
|||
}
|
||||
|
||||
func messagesAllStickersEmpty(hash int64) tg.MessagesAllStickersClass {
|
||||
if hash != 0 {
|
||||
return &tg.MessagesAllStickersNotModified{}
|
||||
}
|
||||
return &tg.MessagesAllStickers{Sets: []tg.StickerSet{}}
|
||||
return &tg.MessagesAllStickers{Hash: 0, Sets: []tg.StickerSet{}}
|
||||
}
|
||||
|
||||
func messagesFeaturedStickersEmpty(hash int64) tg.MessagesFeaturedStickersClass {
|
||||
if hash != 0 {
|
||||
return &tg.MessagesFeaturedStickersNotModified{Count: 0}
|
||||
}
|
||||
return &tg.MessagesFeaturedStickers{
|
||||
Hash: 0,
|
||||
Count: 0,
|
||||
Sets: []tg.StickerSetCoveredClass{},
|
||||
Unread: []int64{},
|
||||
|
|
|
|||
|
|
@ -91,6 +91,11 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
d.OnMessagesGetAvailableReactions(r.onMessagesGetAvailableReactions)
|
||||
d.OnMessagesGetAvailableEffects(r.onMessagesGetAvailableEffects)
|
||||
d.OnMessagesGetStickers(r.onMessagesGetStickers)
|
||||
d.OnMessagesInstallStickerSet(r.onMessagesInstallStickerSet)
|
||||
d.OnMessagesUninstallStickerSet(r.onMessagesUninstallStickerSet)
|
||||
d.OnMessagesReorderStickerSets(r.onMessagesReorderStickerSets)
|
||||
d.OnMessagesToggleStickerSets(r.onMessagesToggleStickerSets)
|
||||
d.OnMessagesGetMyStickers(r.onMessagesGetMyStickers)
|
||||
d.OnMessagesGetArchivedStickers(func(ctx context.Context, req *tg.MessagesGetArchivedStickersRequest) (*tg.MessagesArchivedStickers, error) {
|
||||
return &tg.MessagesArchivedStickers{
|
||||
Count: 0,
|
||||
|
|
|
|||
263
internal/rpc/messages_sticker_sets.go
Normal file
263
internal/rpc/messages_sticker_sets.go
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type userStickerSetService 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)
|
||||
}
|
||||
|
||||
func (r *Router) userStickerSetSvc() (userStickerSetService, bool) {
|
||||
svc, ok := r.deps.Account.(userStickerSetService)
|
||||
return svc, ok
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesInstallStickerSet(ctx context.Context, req *tg.MessagesInstallStickerSetRequest) (tg.MessagesStickerSetInstallResultClass, error) {
|
||||
if req == nil {
|
||||
return nil, stickersetInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
set, _, err := r.resolveInstallableStickerSet(ctx, req.Stickerset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if svc, ok := r.userStickerSetSvc(); ok {
|
||||
if err := svc.InstallUserStickerSet(ctx, userID, set.ID, userStickerSetKind(set), req.Archived, int(r.clock.Now().Unix())); err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
}
|
||||
r.pushStickerSetsUpdate(ctx, userID, userStickerSetKind(set))
|
||||
return &tg.MessagesStickerSetInstallResultSuccess{}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesUninstallStickerSet(ctx context.Context, input tg.InputStickerSetClass) (bool, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
set, _, err := r.resolveInstallableStickerSet(ctx, input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if svc, ok := r.userStickerSetSvc(); ok {
|
||||
if err := svc.UninstallUserStickerSet(ctx, userID, set.ID); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
r.pushStickerSetsUpdate(ctx, userID, userStickerSetKind(set))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesReorderStickerSets(ctx context.Context, req *tg.MessagesReorderStickerSetsRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
kind := stickerSetKindFromFlags(req.Masks, req.Emojis)
|
||||
order := uniqueNonZeroInt64s(req.Order, domain.MaxInstalledStickerSets)
|
||||
if len(order) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if svc, ok := r.userStickerSetSvc(); ok {
|
||||
if err := svc.ReorderUserStickerSets(ctx, userID, kind, order, int(r.clock.Now().Unix())); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
r.pushStickerSetsOrderUpdate(ctx, userID, kind, order)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesToggleStickerSets(ctx context.Context, req *tg.MessagesToggleStickerSetsRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if len(req.Stickersets) > domain.MaxInstalledStickerSets {
|
||||
return false, limitInvalidErr()
|
||||
}
|
||||
svc, hasSvc := r.userStickerSetSvc()
|
||||
kinds := make(map[domain.StickerSetKind]bool)
|
||||
now := int(r.clock.Now().Unix())
|
||||
for _, input := range req.Stickersets {
|
||||
set, _, err := r.resolveInstallableStickerSet(ctx, input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
kind := userStickerSetKind(set)
|
||||
kinds[kind] = true
|
||||
if !hasSvc {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case req.Uninstall:
|
||||
if err := svc.UninstallUserStickerSet(ctx, userID, set.ID); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
case req.Archive:
|
||||
if err := svc.SetUserStickerSetArchived(ctx, userID, set.ID, true, now); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
case req.Unarchive:
|
||||
if err := svc.SetUserStickerSetArchived(ctx, userID, set.ID, false, now); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
default:
|
||||
if err := svc.InstallUserStickerSet(ctx, userID, set.ID, kind, false, now); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
for kind := range kinds {
|
||||
r.pushStickerSetsUpdate(ctx, userID, kind)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesGetMyStickers(ctx context.Context, req *tg.MessagesGetMyStickersRequest) (*tg.MessagesMyStickers, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if r.deps.Files == nil {
|
||||
return &tg.MessagesMyStickers{Count: 0, Sets: []tg.StickerSetCoveredClass{}}, nil
|
||||
}
|
||||
limit := 50
|
||||
var offsetID int64
|
||||
if req != nil {
|
||||
if req.OffsetID < 0 {
|
||||
return nil, offsetInvalidErr()
|
||||
}
|
||||
if req.Limit < 0 || req.Limit > domain.MaxCreatedStickerSets {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
limit = req.Limit
|
||||
}
|
||||
offsetID = req.OffsetID
|
||||
}
|
||||
sets, total, err := r.deps.Files.ListCreatedStickerSets(ctx, userID, offsetID, limit)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
covered := make([]tg.StickerSetCoveredClass, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
set.Creator = true
|
||||
covered = append(covered, &tg.StickerSetNoCovered{Set: tgStickerSet(set)})
|
||||
}
|
||||
return &tg.MessagesMyStickers{Count: total, Sets: covered}, nil
|
||||
}
|
||||
|
||||
func (r *Router) resolveInstallableStickerSet(ctx context.Context, input tg.InputStickerSetClass) (domain.StickerSet, []domain.Document, error) {
|
||||
ref, ok := stickerSetRefFromInput(input)
|
||||
if !ok || (ref.Kind != domain.StickerSetRefByID && ref.Kind != domain.StickerSetRefByShortName) {
|
||||
return domain.StickerSet{}, nil, stickersetInvalidErr()
|
||||
}
|
||||
if r.deps.Files == nil {
|
||||
return domain.StickerSet{}, nil, stickersetInvalidErr()
|
||||
}
|
||||
set, docs, found, err := r.deps.Files.ResolveStickerSet(ctx, ref)
|
||||
if err != nil {
|
||||
return domain.StickerSet{}, nil, internalErr()
|
||||
}
|
||||
if !found || set.ID == 0 {
|
||||
return domain.StickerSet{}, nil, stickersetInvalidErr()
|
||||
}
|
||||
if ref.Kind == domain.StickerSetRefByID && set.AccessHash != ref.AccessHash {
|
||||
return domain.StickerSet{}, nil, stickersetInvalidErr()
|
||||
}
|
||||
return set, docs, nil
|
||||
}
|
||||
|
||||
func userStickerSetKind(set domain.StickerSet) domain.StickerSetKind {
|
||||
switch {
|
||||
case set.Kind == domain.StickerSetKindMasks || set.Masks:
|
||||
return domain.StickerSetKindMasks
|
||||
case set.Kind == domain.StickerSetKindEmoji || set.Emojis:
|
||||
return domain.StickerSetKindEmoji
|
||||
default:
|
||||
return domain.StickerSetKindStickers
|
||||
}
|
||||
}
|
||||
|
||||
func stickerSetKindFromFlags(masks, emojis bool) domain.StickerSetKind {
|
||||
switch {
|
||||
case masks:
|
||||
return domain.StickerSetKindMasks
|
||||
case emojis:
|
||||
return domain.StickerSetKindEmoji
|
||||
default:
|
||||
return domain.StickerSetKindStickers
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueNonZeroInt64s(in []int64, max int) []int64 {
|
||||
if max <= 0 {
|
||||
max = len(in)
|
||||
}
|
||||
out := make([]int64, 0, len(in))
|
||||
seen := make(map[int64]struct{}, len(in))
|
||||
for _, v := range in {
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
if len(out) >= max {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) pushStickerSetsUpdate(ctx context.Context, userID int64, kind domain.StickerSetKind) {
|
||||
update := &tg.UpdateStickerSets{}
|
||||
switch kind {
|
||||
case domain.StickerSetKindMasks:
|
||||
update.SetMasks(true)
|
||||
case domain.StickerSetKindEmoji:
|
||||
update.SetEmojis(true)
|
||||
}
|
||||
r.pushUserUpdates(ctx, userID, &tg.Updates{
|
||||
Updates: []tg.UpdateClass{update},
|
||||
Users: []tg.UserClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) pushStickerSetsOrderUpdate(ctx context.Context, userID int64, kind domain.StickerSetKind, order []int64) {
|
||||
update := &tg.UpdateStickerSetsOrder{Order: append([]int64(nil), order...)}
|
||||
switch kind {
|
||||
case domain.StickerSetKindMasks:
|
||||
update.SetMasks(true)
|
||||
case domain.StickerSetKindEmoji:
|
||||
update.SetEmojis(true)
|
||||
}
|
||||
r.pushUserUpdates(ctx, userID, &tg.Updates{
|
||||
Updates: []tg.UpdateClass{update},
|
||||
Users: []tg.UserClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
}
|
||||
306
internal/rpc/messages_sticker_sets_rpc_test.go
Normal file
306
internal/rpc/messages_sticker_sets_rpc_test.go
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appaccount "telesrv/internal/app/account"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func userStickerSetRouter(t *testing.T) (*Router, *memory.PasswordStore, *captureSessions) {
|
||||
t.Helper()
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
101: {ID: 101, AccessHash: 11, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
102: {ID: 102, AccessHash: 12, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindStickers: {
|
||||
{ID: 10, AccessHash: 100, ShortName: "funny", Title: "Funny", Kind: domain.StickerSetKindStickers, Count: 1, Hash: 7, Installed: true, InstalledDate: 1, DocumentIDs: []int64{101}},
|
||||
{ID: 20, AccessHash: 200, ShortName: "work", Title: "Work", Kind: domain.StickerSetKindStickers, Count: 1, Hash: 8, Installed: true, InstalledDate: 1, DocumentIDs: []int64{102}},
|
||||
},
|
||||
domain.StickerSetKindEmoji: {
|
||||
{ID: 30, AccessHash: 300, ShortName: "emoji_fun", Title: "Emoji Fun", Kind: domain.StickerSetKindEmoji, Emojis: true, Count: 1, Hash: 9, Installed: true, InstalledDate: 1, DocumentIDs: []int64{101}},
|
||||
},
|
||||
},
|
||||
}
|
||||
passwordStore := memory.NewPasswordStore()
|
||||
sessions := &captureSessions{}
|
||||
router := New(Config{}, Deps{
|
||||
Account: appaccount.NewService(passwordStore, appaccount.WithUserStickerSets(passwordStore)),
|
||||
Files: files,
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
return router, passwordStore, sessions
|
||||
}
|
||||
|
||||
func TestMessagesInstallStickerSetPersistsAndPushesUpdate(t *testing.T) {
|
||||
r, store, sessions := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
out, err := r.onMessagesInstallStickerSet(ctx, &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "funny"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("install sticker set: %v", err)
|
||||
}
|
||||
if _, ok := out.(*tg.MessagesStickerSetInstallResultSuccess); !ok {
|
||||
t.Fatalf("install result = %T, want *tg.MessagesStickerSetInstallResultSuccess", out)
|
||||
}
|
||||
|
||||
items, total, err := store.ListUserStickerSets(ctx, 1000000001, domain.StickerSetKindStickers, nil, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list installed sets: %v", err)
|
||||
}
|
||||
if total != 1 || len(items) != 1 || items[0].StickerSetID != 10 || items[0].Archived {
|
||||
t.Fatalf("installed sets = total %d items %+v, want one active set 10", total, items)
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindStickers, nil)
|
||||
}
|
||||
|
||||
func TestMessagesInstallStickerSetUpdatesAllStickersProjection(t *testing.T) {
|
||||
r, _, _ := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
before, err := r.onMessagesGetAllStickers(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get all stickers before install: %v", err)
|
||||
}
|
||||
if full, ok := before.(*tg.MessagesAllStickers); ok && len(full.Sets) != 0 {
|
||||
t.Fatalf("all stickers before install = %+v, want empty non-default catalog", full.Sets)
|
||||
}
|
||||
if _, err := r.onMessagesInstallStickerSet(ctx, &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "funny"},
|
||||
}); err != nil {
|
||||
t.Fatalf("install sticker set: %v", err)
|
||||
}
|
||||
|
||||
after, err := r.onMessagesGetAllStickers(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get all stickers after install: %v", err)
|
||||
}
|
||||
full, ok := after.(*tg.MessagesAllStickers)
|
||||
if !ok {
|
||||
t.Fatalf("get all stickers after install = %T, want *tg.MessagesAllStickers", after)
|
||||
}
|
||||
if len(full.Sets) != 1 || full.Sets[0].ID != 10 || full.Sets[0].InstalledDate == 0 {
|
||||
t.Fatalf("all stickers after install = %+v, want installed set 10", full.Sets)
|
||||
}
|
||||
|
||||
if ok, err := r.onMessagesUninstallStickerSet(ctx, &tg.InputStickerSetID{ID: 10, AccessHash: 100}); err != nil || !ok {
|
||||
t.Fatalf("uninstall sticker set = %v %v", ok, err)
|
||||
}
|
||||
afterUninstall, err := r.onMessagesGetAllStickers(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get all stickers after uninstall: %v", err)
|
||||
}
|
||||
if full, ok := afterUninstall.(*tg.MessagesAllStickers); ok && len(full.Sets) != 0 {
|
||||
t.Fatalf("all stickers after uninstall = %+v, want empty", full.Sets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesEmptyViewerStickerSetsInvalidateOldClientHash(t *testing.T) {
|
||||
r, _, _ := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
stickers, err := r.onMessagesGetAllStickers(ctx, 3041827464193675523)
|
||||
if err != nil {
|
||||
t.Fatalf("get all stickers with old hash: %v", err)
|
||||
}
|
||||
full, ok := stickers.(*tg.MessagesAllStickers)
|
||||
if !ok {
|
||||
t.Fatalf("get all stickers with old hash = %T, want full empty list", stickers)
|
||||
}
|
||||
if full.Hash != 0 || len(full.Sets) != 0 {
|
||||
t.Fatalf("empty sticker list = hash %d sets %+v, want hash 0 with no sets", full.Hash, full.Sets)
|
||||
}
|
||||
|
||||
emoji, err := r.onMessagesGetEmojiStickers(ctx, 7254637046733671932)
|
||||
if err != nil {
|
||||
t.Fatalf("get emoji stickers with old hash: %v", err)
|
||||
}
|
||||
emojiFull, ok := emoji.(*tg.MessagesAllStickers)
|
||||
if !ok {
|
||||
t.Fatalf("get emoji stickers with old hash = %T, want full empty list", emoji)
|
||||
}
|
||||
if emojiFull.Hash != 0 || len(emojiFull.Sets) != 0 {
|
||||
t.Fatalf("empty emoji list = hash %d sets %+v, want hash 0 with no sets", emojiFull.Hash, emojiFull.Sets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetStickerSetUsesViewerInstallState(t *testing.T) {
|
||||
r, _, _ := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
before, err := r.onMessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "funny"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get sticker set before install: %v", err)
|
||||
}
|
||||
full, ok := before.(*tg.MessagesStickerSet)
|
||||
if !ok {
|
||||
t.Fatalf("get sticker set before install = %T, want *tg.MessagesStickerSet", before)
|
||||
}
|
||||
if full.Set.InstalledDate != 0 {
|
||||
t.Fatalf("preview installed_date before install = %d, want 0", full.Set.InstalledDate)
|
||||
}
|
||||
|
||||
if _, err := r.onMessagesInstallStickerSet(ctx, &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "funny"},
|
||||
}); err != nil {
|
||||
t.Fatalf("install sticker set: %v", err)
|
||||
}
|
||||
after, err := r.onMessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "funny"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get sticker set after install: %v", err)
|
||||
}
|
||||
full, ok = after.(*tg.MessagesStickerSet)
|
||||
if !ok {
|
||||
t.Fatalf("get sticker set after install = %T, want *tg.MessagesStickerSet", after)
|
||||
}
|
||||
if full.Set.InstalledDate == 0 {
|
||||
t.Fatalf("preview installed_date after install = 0, want viewer install date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesInstallStickerSetRejectsBadAccessHash(t *testing.T) {
|
||||
r, store, sessions := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
out, err := r.onMessagesInstallStickerSet(ctx, &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetID{ID: 10, AccessHash: 999},
|
||||
})
|
||||
if out != nil || !tgerr.Is(err, "STICKERSET_INVALID") {
|
||||
t.Fatalf("install with bad access hash = %T %v, want STICKERSET_INVALID", out, err)
|
||||
}
|
||||
items, total, err := store.ListUserStickerSets(ctx, 1000000001, domain.StickerSetKindStickers, nil, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list installed sets: %v", err)
|
||||
}
|
||||
if total != 0 || len(items) != 0 {
|
||||
t.Fatalf("installed after rejected install = total %d items %+v, want empty", total, items)
|
||||
}
|
||||
if push := sessions.lastUserPush(); push != nil {
|
||||
t.Fatalf("push after rejected install = %T, want nil", push)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesReorderAndToggleStickerSets(t *testing.T) {
|
||||
r, store, sessions := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
for _, shortName := range []string{"funny", "work"} {
|
||||
if _, err := r.onMessagesInstallStickerSet(ctx, &tg.MessagesInstallStickerSetRequest{Stickerset: &tg.InputStickerSetShortName{ShortName: shortName}}); err != nil {
|
||||
t.Fatalf("install %s: %v", shortName, err)
|
||||
}
|
||||
}
|
||||
if ok, err := r.onMessagesReorderStickerSets(ctx, &tg.MessagesReorderStickerSetsRequest{Order: []int64{20, 10, 20, 0}}); err != nil || !ok {
|
||||
t.Fatalf("reorder = %v %v", ok, err)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, store, ctx, 1000000001, domain.StickerSetKindStickers, nil); len(got) != 2 || got[0] != 20 || got[1] != 10 {
|
||||
t.Fatalf("installed order = %v, want [20 10]", got)
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindStickers, []int64{20, 10})
|
||||
|
||||
if ok, err := r.onMessagesToggleStickerSets(ctx, &tg.MessagesToggleStickerSetsRequest{
|
||||
Stickersets: []tg.InputStickerSetClass{&tg.InputStickerSetID{ID: 20, AccessHash: 200}},
|
||||
Uninstall: true,
|
||||
}); err != nil || !ok {
|
||||
t.Fatalf("toggle uninstall = %v %v", ok, err)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, store, ctx, 1000000001, domain.StickerSetKindStickers, nil); len(got) != 1 || got[0] != 10 {
|
||||
t.Fatalf("installed after toggle uninstall = %v, want [10]", got)
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindStickers, nil)
|
||||
}
|
||||
|
||||
func TestMessagesToggleEmojiStickerSetsUsesEmojiUpdateFlag(t *testing.T) {
|
||||
r, store, sessions := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
if ok, err := r.onMessagesToggleStickerSets(ctx, &tg.MessagesToggleStickerSetsRequest{
|
||||
Stickersets: []tg.InputStickerSetClass{&tg.InputStickerSetShortName{ShortName: "emoji_fun"}},
|
||||
}); err != nil || !ok {
|
||||
t.Fatalf("toggle emoji install = %v %v", ok, err)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, store, ctx, 1000000001, domain.StickerSetKindEmoji, nil); len(got) != 1 || got[0] != 30 {
|
||||
t.Fatalf("emoji installed sets = %v, want [30]", got)
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindEmoji, nil)
|
||||
}
|
||||
|
||||
func TestMessagesGetMyStickersEmptyUntilCreatorStore(t *testing.T) {
|
||||
r, _, _ := userStickerSetRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
out, err := r.onMessagesGetMyStickers(ctx, &tg.MessagesGetMyStickersRequest{Limit: 50})
|
||||
if err != nil {
|
||||
t.Fatalf("get my stickers: %v", err)
|
||||
}
|
||||
if out.Count != 0 || len(out.Sets) != 0 {
|
||||
t.Fatalf("get my stickers = count %d sets %d, want empty creator-owned page", out.Count, len(out.Sets))
|
||||
}
|
||||
out, err = r.onMessagesGetMyStickers(ctx, &tg.MessagesGetMyStickersRequest{Limit: domain.MaxInstalledStickerSets + 1})
|
||||
if out != nil || !tgerr.Is(err, "LIMIT_INVALID") {
|
||||
t.Fatalf("get my stickers over limit = %T %v, want LIMIT_INVALID", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func installedStickerSetIDs(t *testing.T, store *memory.PasswordStore, ctx context.Context, userID int64, kind domain.StickerSetKind, archived *bool) []int64 {
|
||||
t.Helper()
|
||||
items, _, err := store.ListUserStickerSets(ctx, userID, kind, archived, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list installed sticker sets: %v", err)
|
||||
}
|
||||
out := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, item.StickerSetID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertStickerSetsUpdate(t *testing.T, push any, kind domain.StickerSetKind, order []int64) {
|
||||
t.Helper()
|
||||
updates, ok := push.(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("push = %T, want *tg.Updates", push)
|
||||
}
|
||||
if len(updates.Updates) != 1 {
|
||||
t.Fatalf("push updates = %d, want 1", len(updates.Updates))
|
||||
}
|
||||
if order != nil {
|
||||
update, ok := updates.Updates[0].(*tg.UpdateStickerSetsOrder)
|
||||
if !ok {
|
||||
t.Fatalf("push update = %T, want *tg.UpdateStickerSetsOrder", updates.Updates[0])
|
||||
}
|
||||
if len(update.Order) != len(order) {
|
||||
t.Fatalf("order update = %v, want %v", update.Order, order)
|
||||
}
|
||||
for i := range order {
|
||||
if update.Order[i] != order[i] {
|
||||
t.Fatalf("order update = %v, want %v", update.Order, order)
|
||||
}
|
||||
}
|
||||
if update.Masks != (kind == domain.StickerSetKindMasks) || update.Emojis != (kind == domain.StickerSetKindEmoji) {
|
||||
t.Fatalf("order flags masks=%v emojis=%v for kind %s", update.Masks, update.Emojis, kind)
|
||||
}
|
||||
return
|
||||
}
|
||||
update, ok := updates.Updates[0].(*tg.UpdateStickerSets)
|
||||
if !ok {
|
||||
t.Fatalf("push update = %T, want *tg.UpdateStickerSets", updates.Updates[0])
|
||||
}
|
||||
if update.Masks != (kind == domain.StickerSetKindMasks) || update.Emojis != (kind == domain.StickerSetKindEmoji) {
|
||||
t.Fatalf("update flags masks=%v emojis=%v for kind %s", update.Masks, update.Emojis, kind)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +169,7 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
|||
r.registerUpdates(d)
|
||||
r.registerAccount(d)
|
||||
r.registerMessages(d)
|
||||
r.registerStickers(d)
|
||||
r.registerChannels(d)
|
||||
r.registerUpload(d)
|
||||
r.registerPhotos(d)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
|
|
@ -87,6 +89,9 @@ func (f *fakeFiles) GetDocuments(_ context.Context, ids []int64) ([]domain.Docum
|
|||
func (f *fakeFiles) ResolveStickerSet(_ context.Context, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error) {
|
||||
for _, sets := range f.sets {
|
||||
for _, set := range sets {
|
||||
if set.Deleted {
|
||||
continue
|
||||
}
|
||||
match := false
|
||||
switch ref.Kind {
|
||||
case domain.StickerSetRefByID:
|
||||
|
|
@ -114,6 +119,411 @@ func (f *fakeFiles) ListStickerSets(_ context.Context, kind domain.StickerSetKin
|
|||
sets := f.sets[kind]
|
||||
return append([]domain.StickerSet(nil), sets...), nil
|
||||
}
|
||||
func (f *fakeFiles) CheckStickerSetShortName(_ context.Context, shortName string) (bool, error) {
|
||||
if !validTestStickerShortName(shortName) {
|
||||
return false, domain.ErrStickerSetShortNameInvalid
|
||||
}
|
||||
for _, sets := range f.sets {
|
||||
for _, set := range sets {
|
||||
if set.ShortName != "" && strings.EqualFold(set.ShortName, shortName) && !set.Deleted {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
func validTestStickerShortName(shortName string) bool {
|
||||
shortName = strings.ToLower(strings.TrimSpace(shortName))
|
||||
if len(shortName) < domain.MinStickerSetShortNameLen || len(shortName) > domain.MaxStickerSetShortNameLen {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(shortName); i++ {
|
||||
ch := shortName[i]
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9' && i > 0) || (ch == '_' && i > 0 && i < len(shortName)-1) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (f *fakeFiles) SuggestStickerSetShortName(ctx context.Context, title string, userID int64) (string, error) {
|
||||
base := strings.ToLower(strings.TrimSpace(title))
|
||||
base = strings.ReplaceAll(base, " ", "_")
|
||||
if base == "" {
|
||||
base = "stickers"
|
||||
}
|
||||
if len(base) < domain.MinStickerSetShortNameLen {
|
||||
base += "_pack"
|
||||
}
|
||||
if len(base) > domain.MaxStickerSetShortNameLen {
|
||||
base = strings.Trim(base[:domain.MaxStickerSetShortNameLen], "_")
|
||||
}
|
||||
candidates := []string{base, base + "_pack", base + "_2"}
|
||||
for _, c := range candidates {
|
||||
if ok, err := f.CheckStickerSetShortName(ctx, c); err != nil {
|
||||
continue
|
||||
} else if ok {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return "", domain.ErrStickerSetShortNameOccupied
|
||||
}
|
||||
func (f *fakeFiles) CreateStickerSet(_ context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error) {
|
||||
if f.sets == nil {
|
||||
f.sets = map[domain.StickerSetKind][]domain.StickerSet{}
|
||||
}
|
||||
if f.docs == nil {
|
||||
f.docs = map[int64]domain.Document{}
|
||||
}
|
||||
if strings.TrimSpace(req.Title) == "" {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetTitleInvalid
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetEmpty
|
||||
}
|
||||
shortName := strings.ToLower(strings.TrimSpace(req.ShortName))
|
||||
if shortName == "" {
|
||||
shortName = "created_pack"
|
||||
}
|
||||
if ok, err := f.CheckStickerSetShortName(context.Background(), shortName); err != nil {
|
||||
return domain.StickerSet{}, nil, err
|
||||
} else if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetShortNameOccupied
|
||||
}
|
||||
kind := req.Kind
|
||||
if kind == "" {
|
||||
kind = domain.StickerSetKindStickers
|
||||
}
|
||||
docIDs := make([]int64, 0, len(req.Items))
|
||||
packs := []domain.StickerPack{}
|
||||
keywords := []domain.StickerKeyword{}
|
||||
docs := make([]domain.Document, 0, len(req.Items))
|
||||
for _, item := range req.Items {
|
||||
doc, ok := f.docs[item.DocumentID]
|
||||
if !ok || doc.AccessHash != item.DocumentAccessHash || !doc.IsStickerSetMaterial() {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
if strings.TrimSpace(item.Emoji) == "" {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetEmojiInvalid
|
||||
}
|
||||
docIDs = append(docIDs, item.DocumentID)
|
||||
packs = append(packs, domain.StickerPack{Emoticon: item.Emoji, DocumentIDs: []int64{item.DocumentID}})
|
||||
if item.Keywords != "" {
|
||||
keywords = append(keywords, domain.StickerKeyword{DocumentID: item.DocumentID, Keywords: []string{strings.TrimSpace(item.Keywords)}})
|
||||
}
|
||||
doc.Attributes = []domain.DocumentAttribute{{Kind: domain.DocAttrSticker, Alt: item.Emoji, StickerSetID: 9000, StickerSetAccessHash: 9001}}
|
||||
if kind == domain.StickerSetKindEmoji {
|
||||
doc.Attributes[0].Kind = domain.DocAttrCustomEmoji
|
||||
doc.Attributes[0].TextColor = req.TextColor
|
||||
}
|
||||
f.docs[item.DocumentID] = doc
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
set := domain.StickerSet{
|
||||
ID: 9000 + int64(len(f.sets[kind])),
|
||||
AccessHash: 9001 + int64(len(f.sets[kind])),
|
||||
ShortName: shortName,
|
||||
Title: req.Title,
|
||||
Kind: kind,
|
||||
Emojis: kind == domain.StickerSetKindEmoji,
|
||||
Masks: kind == domain.StickerSetKindMasks,
|
||||
TextColor: kind == domain.StickerSetKindEmoji && req.TextColor,
|
||||
Creator: true,
|
||||
CreatorUserID: req.CreatorUserID,
|
||||
Count: len(docIDs),
|
||||
Hash: 77 + len(f.sets[kind]),
|
||||
DocumentIDs: docIDs,
|
||||
Packs: packs,
|
||||
Keywords: keywords,
|
||||
}
|
||||
f.sets[kind] = append(f.sets[kind], set)
|
||||
return set, docs, nil
|
||||
}
|
||||
func (f *fakeFiles) ListCreatedStickerSets(_ context.Context, userID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error) {
|
||||
var all []domain.StickerSet
|
||||
for _, sets := range f.sets {
|
||||
for _, set := range sets {
|
||||
if set.CreatorUserID == userID && !set.Deleted {
|
||||
set.Creator = true
|
||||
all = append(all, set)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].ID > all[j].ID })
|
||||
total := len(all)
|
||||
if offsetID != 0 {
|
||||
filtered := all[:0]
|
||||
for _, set := range all {
|
||||
if set.ID < offsetID {
|
||||
filtered = append(filtered, set)
|
||||
}
|
||||
}
|
||||
all = filtered
|
||||
}
|
||||
if limit > 0 && len(all) > limit {
|
||||
all = all[:limit]
|
||||
}
|
||||
return all, total, nil
|
||||
}
|
||||
func (f *fakeFiles) AddStickerToSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error) {
|
||||
kind, idx, ok := f.fakeStickerSetIndex(ref)
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
||||
}
|
||||
set := f.sets[kind][idx]
|
||||
if set.CreatorUserID != actorUserID {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned
|
||||
}
|
||||
doc, ok := f.docs[item.DocumentID]
|
||||
if !ok || doc.AccessHash != item.DocumentAccessHash || !doc.IsStickerSetMaterial() {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
if setID, _, ok := doc.StickerSetRef(); ok && setID != 0 && setID != set.ID {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
if fakeContainsInt64(set.DocumentIDs, doc.ID) {
|
||||
return set, f.fakeStickerSetDocs(set), nil
|
||||
}
|
||||
emoji := strings.TrimSpace(item.Emoji)
|
||||
if emoji == "" {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetEmojiInvalid
|
||||
}
|
||||
doc = fakeAttachStickerSet(doc, set, emoji)
|
||||
f.docs[doc.ID] = doc
|
||||
set.DocumentIDs = append(set.DocumentIDs, doc.ID)
|
||||
set.Count = len(set.DocumentIDs)
|
||||
set.Packs = fakeAddStickerPackDoc(set.Packs, emoji, doc.ID)
|
||||
if kw := strings.TrimSpace(item.Keywords); kw != "" {
|
||||
set.Keywords = fakeUpsertStickerKeyword(set.Keywords, domain.StickerKeyword{DocumentID: doc.ID, Keywords: []string{kw}})
|
||||
}
|
||||
set.Hash++
|
||||
f.sets[kind][idx] = set
|
||||
return set, f.fakeStickerSetDocs(set), nil
|
||||
}
|
||||
func (f *fakeFiles) RemoveStickerFromSet(_ context.Context, actorUserID int64, documentID int64, accessHash int64) (domain.StickerSet, []domain.Document, error) {
|
||||
doc, ok := f.docs[documentID]
|
||||
if !ok || doc.AccessHash != accessHash || !doc.IsStickerLike() {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
setID, setAccessHash, ok := doc.StickerSetRef()
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
kind, idx, ok := f.fakeStickerSetIndex(domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash})
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
||||
}
|
||||
set := f.sets[kind][idx]
|
||||
if set.CreatorUserID != actorUserID {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned
|
||||
}
|
||||
pos := fakeIndexInt64(set.DocumentIDs, documentID)
|
||||
if pos < 0 {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
set.DocumentIDs = append(append([]int64(nil), set.DocumentIDs[:pos]...), set.DocumentIDs[pos+1:]...)
|
||||
set.Count = len(set.DocumentIDs)
|
||||
set.Packs = fakeRemoveStickerPackDoc(set.Packs, documentID)
|
||||
set.Keywords = fakeRemoveStickerKeyword(set.Keywords, documentID)
|
||||
set.Hash++
|
||||
doc = fakeDetachStickerSet(doc)
|
||||
f.docs[doc.ID] = doc
|
||||
f.sets[kind][idx] = set
|
||||
return set, f.fakeStickerSetDocs(set), nil
|
||||
}
|
||||
func (f *fakeFiles) ChangeStickerPosition(_ context.Context, actorUserID int64, documentID int64, accessHash int64, position int) (domain.StickerSet, []domain.Document, error) {
|
||||
doc, ok := f.docs[documentID]
|
||||
if !ok || doc.AccessHash != accessHash || !doc.IsStickerLike() {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
setID, setAccessHash, ok := doc.StickerSetRef()
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
kind, idx, ok := f.fakeStickerSetIndex(domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash})
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
||||
}
|
||||
set := f.sets[kind][idx]
|
||||
if set.CreatorUserID != actorUserID {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned
|
||||
}
|
||||
from := fakeIndexInt64(set.DocumentIDs, documentID)
|
||||
if from < 0 {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
if position < 0 || position >= len(set.DocumentIDs) {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetPositionInvalid
|
||||
}
|
||||
set.DocumentIDs = fakeMoveInt64(set.DocumentIDs, from, position)
|
||||
set.Hash++
|
||||
f.sets[kind][idx] = set
|
||||
return set, f.fakeStickerSetDocs(set), nil
|
||||
}
|
||||
func (f *fakeFiles) RenameStickerSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef, title string) (domain.StickerSet, []domain.Document, error) {
|
||||
kind, idx, ok := f.fakeStickerSetIndex(ref)
|
||||
if !ok {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
||||
}
|
||||
set := f.sets[kind][idx]
|
||||
if set.CreatorUserID != actorUserID {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned
|
||||
}
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetTitleInvalid
|
||||
}
|
||||
set.Title = title
|
||||
set.Hash++
|
||||
f.sets[kind][idx] = set
|
||||
return set, f.fakeStickerSetDocs(set), nil
|
||||
}
|
||||
func (f *fakeFiles) DeleteStickerSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef) (domain.StickerSetKind, error) {
|
||||
kind, idx, ok := f.fakeStickerSetIndex(ref)
|
||||
if !ok {
|
||||
return "", domain.ErrStickerSetInvalid
|
||||
}
|
||||
set := f.sets[kind][idx]
|
||||
if set.CreatorUserID != actorUserID {
|
||||
return "", domain.ErrStickerSetNotOwned
|
||||
}
|
||||
set.Deleted = true
|
||||
f.sets[kind][idx] = set
|
||||
return kind, nil
|
||||
}
|
||||
func (f *fakeFiles) fakeStickerSetIndex(ref domain.StickerSetRef) (domain.StickerSetKind, int, bool) {
|
||||
for kind, sets := range f.sets {
|
||||
for idx, set := range sets {
|
||||
if set.Deleted {
|
||||
continue
|
||||
}
|
||||
switch ref.Kind {
|
||||
case domain.StickerSetRefByID:
|
||||
if set.ID == ref.ID && (ref.AccessHash == 0 || set.AccessHash == ref.AccessHash) {
|
||||
return kind, idx, true
|
||||
}
|
||||
case domain.StickerSetRefByShortName:
|
||||
if strings.EqualFold(set.ShortName, ref.ShortName) {
|
||||
return kind, idx, true
|
||||
}
|
||||
case domain.StickerSetRefBySystem:
|
||||
if set.SystemKey == ref.SystemKey {
|
||||
return kind, idx, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", 0, false
|
||||
}
|
||||
func (f *fakeFiles) fakeStickerSetDocs(set domain.StickerSet) []domain.Document {
|
||||
out := make([]domain.Document, 0, len(set.DocumentIDs))
|
||||
for _, id := range set.DocumentIDs {
|
||||
if doc, ok := f.docs[id]; ok {
|
||||
out = append(out, doc)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
func fakeAttachStickerSet(doc domain.Document, set domain.StickerSet, emoji string) domain.Document {
|
||||
want := domain.DocAttrSticker
|
||||
if set.Kind == domain.StickerSetKindEmoji || set.Emojis {
|
||||
want = domain.DocAttrCustomEmoji
|
||||
}
|
||||
attrs := append([]domain.DocumentAttribute(nil), doc.Attributes...)
|
||||
replaced := false
|
||||
for i := range attrs {
|
||||
if attrs[i].Kind != domain.DocAttrSticker && attrs[i].Kind != domain.DocAttrCustomEmoji {
|
||||
continue
|
||||
}
|
||||
attrs[i].Kind = want
|
||||
attrs[i].Alt = emoji
|
||||
attrs[i].StickerSetID = set.ID
|
||||
attrs[i].StickerSetAccessHash = set.AccessHash
|
||||
attrs[i].TextColor = set.TextColor
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
if !replaced {
|
||||
attrs = append(attrs, domain.DocumentAttribute{Kind: want, Alt: emoji, StickerSetID: set.ID, StickerSetAccessHash: set.AccessHash, TextColor: set.TextColor})
|
||||
}
|
||||
doc.Attributes = attrs
|
||||
return doc
|
||||
}
|
||||
func fakeDetachStickerSet(doc domain.Document) domain.Document {
|
||||
attrs := append([]domain.DocumentAttribute(nil), doc.Attributes...)
|
||||
for i := range attrs {
|
||||
if attrs[i].Kind == domain.DocAttrSticker || attrs[i].Kind == domain.DocAttrCustomEmoji {
|
||||
attrs[i].StickerSetID = 0
|
||||
attrs[i].StickerSetAccessHash = 0
|
||||
attrs[i].TextColor = false
|
||||
break
|
||||
}
|
||||
}
|
||||
doc.Attributes = attrs
|
||||
return doc
|
||||
}
|
||||
func fakeAddStickerPackDoc(packs []domain.StickerPack, emoji string, documentID int64) []domain.StickerPack {
|
||||
out := append([]domain.StickerPack(nil), packs...)
|
||||
for i := range out {
|
||||
out[i].DocumentIDs = append([]int64(nil), out[i].DocumentIDs...)
|
||||
if out[i].Emoticon == emoji {
|
||||
if !fakeContainsInt64(out[i].DocumentIDs, documentID) {
|
||||
out[i].DocumentIDs = append(out[i].DocumentIDs, documentID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
return append(out, domain.StickerPack{Emoticon: emoji, DocumentIDs: []int64{documentID}})
|
||||
}
|
||||
func fakeRemoveStickerPackDoc(packs []domain.StickerPack, documentID int64) []domain.StickerPack {
|
||||
out := make([]domain.StickerPack, 0, len(packs))
|
||||
for _, pack := range packs {
|
||||
ids := make([]int64, 0, len(pack.DocumentIDs))
|
||||
for _, id := range pack.DocumentIDs {
|
||||
if id != documentID {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) != 0 {
|
||||
out = append(out, domain.StickerPack{Emoticon: pack.Emoticon, DocumentIDs: ids})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
func fakeUpsertStickerKeyword(in []domain.StickerKeyword, keyword domain.StickerKeyword) []domain.StickerKeyword {
|
||||
out := fakeRemoveStickerKeyword(in, keyword.DocumentID)
|
||||
return append(out, keyword)
|
||||
}
|
||||
func fakeRemoveStickerKeyword(in []domain.StickerKeyword, documentID int64) []domain.StickerKeyword {
|
||||
out := make([]domain.StickerKeyword, 0, len(in))
|
||||
for _, kw := range in {
|
||||
if kw.DocumentID != documentID {
|
||||
out = append(out, kw)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
func fakeContainsInt64(in []int64, value int64) bool {
|
||||
return fakeIndexInt64(in, value) >= 0
|
||||
}
|
||||
func fakeIndexInt64(in []int64, value int64) int {
|
||||
for i, v := range in {
|
||||
if v == value {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
func fakeMoveInt64(in []int64, from, to int) []int64 {
|
||||
out := append([]int64(nil), in...)
|
||||
value := out[from]
|
||||
out = append(out[:from], out[from+1:]...)
|
||||
if to >= len(out) {
|
||||
return append(out, value)
|
||||
}
|
||||
out = append(out[:to], append([]int64{value}, out[to:]...)...)
|
||||
return out
|
||||
}
|
||||
func (f *fakeFiles) CreatePhotoFromUpload(_ context.Context, _ domain.UploadedFileRef) (domain.Photo, error) {
|
||||
photo := domain.Photo{ID: 777, AccessHash: 7, DCID: 2, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 800, H: 600}}}
|
||||
return f.putPhoto(photo), nil
|
||||
|
|
|
|||
|
|
@ -52,3 +52,10 @@ func (r *Router) stickerCatalogSets(ctx context.Context, kind domain.StickerSetK
|
|||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
func (r *Router) invalidateStickerCatalog(kind domain.StickerSetKind) {
|
||||
if r.stickerCatalog == nil || r.stickerCatalog.cache == nil {
|
||||
return
|
||||
}
|
||||
r.stickerCatalog.cache.Invalidate(kind)
|
||||
}
|
||||
|
|
|
|||
330
internal/rpc/sticker_link_smoke_test.go
Normal file
330
internal/rpc/sticker_link_smoke_test.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appaccount "telesrv/internal/app/account"
|
||||
botsapp "telesrv/internal/app/bots"
|
||||
appmessages "telesrv/internal/app/messages"
|
||||
apppolls "telesrv/internal/app/polls"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
"telesrv/internal/web/stickerlinks"
|
||||
)
|
||||
|
||||
func TestCustomStickerPackLinkInstallAndSendSmoke(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009001", FirstName: "Alice"})
|
||||
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 12, Phone: "15550009002", FirstName: "Bob"})
|
||||
dialogStore := memory.NewDialogStore()
|
||||
messageStore := memory.NewMessageStore(dialogStore)
|
||||
pollStore := memory.NewPollStore()
|
||||
messageStore.AttachPollStore(pollStore)
|
||||
passwordStore := memory.NewPasswordStore()
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
101: {
|
||||
ID: 101,
|
||||
AccessHash: 1101,
|
||||
DCID: 2,
|
||||
MimeType: "image/webp",
|
||||
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}},
|
||||
},
|
||||
},
|
||||
photos: map[int64]domain.Photo{},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{},
|
||||
}
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Account: appaccount.NewService(passwordStore, appaccount.WithUserStickerSets(passwordStore)),
|
||||
Users: appusers.NewService(userStore),
|
||||
Messages: appmessages.NewService(messageStore, dialogStore),
|
||||
Files: files,
|
||||
Polls: apppolls.NewService(pollStore),
|
||||
Sessions: &captureSessions{},
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := r.onStickersCreateStickerSet(WithUserID(ctx, alice.ID), &tg.StickersCreateStickerSetRequest{
|
||||
UserID: &tg.InputUserSelf{},
|
||||
Title: "Alice Fresh Pack",
|
||||
ShortName: "alice_fresh_pack",
|
||||
Stickers: []tg.InputStickerSetItem{{
|
||||
Document: &tg.InputDocument{ID: 101, AccessHash: 1101},
|
||||
Emoji: "🙂",
|
||||
Keywords: "fresh",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sticker set: %v", err)
|
||||
}
|
||||
createdFull, ok := created.(*tg.MessagesStickerSet)
|
||||
if !ok {
|
||||
t.Fatalf("created = %T, want *tg.MessagesStickerSet", created)
|
||||
}
|
||||
|
||||
web := stickerlinks.NewHandler(files, "https://telesrv.net")
|
||||
rr := httptest.NewRecorder()
|
||||
web.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/addstickers/alice_fresh_pack", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("sticker link status = %d body=%q, want 200", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{"https://telesrv.net/addstickers/alice_fresh_pack", "telesrv://addstickers?set=alice_fresh_pack", "tg://addstickers?set=alice_fresh_pack"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("sticker link body missing %q:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, `window.location.href = "tg://`) {
|
||||
t.Fatalf("sticker link must auto-open telesrv://, not tg://:\n%s", body)
|
||||
}
|
||||
|
||||
preview, err := r.onMessagesGetStickerSet(WithUserID(ctx, bob.ID), &tg.MessagesGetStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "alice_fresh_pack"},
|
||||
Hash: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bob preview sticker set: %v", err)
|
||||
}
|
||||
previewFull, ok := preview.(*tg.MessagesStickerSet)
|
||||
if !ok || previewFull.Set.ID != createdFull.Set.ID || len(previewFull.Documents) != 1 {
|
||||
t.Fatalf("preview = %T %+v, want created set with one document", preview, preview)
|
||||
}
|
||||
|
||||
if _, err := r.onMessagesInstallStickerSet(WithUserID(ctx, bob.ID), &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "alice_fresh_pack"},
|
||||
}); err != nil {
|
||||
t.Fatalf("bob install sticker set: %v", err)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, passwordStore, ctx, bob.ID, domain.StickerSetKindStickers, nil); len(got) != 1 || got[0] != createdFull.Set.ID {
|
||||
t.Fatalf("bob installed sets = %v, want [%d]", got, createdFull.Set.ID)
|
||||
}
|
||||
|
||||
if _, err := r.onMessagesSendMedia(WithUserID(ctx, bob.ID), &tg.MessagesSendMediaRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
|
||||
Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 101, AccessHash: 1101}},
|
||||
RandomID: 7001,
|
||||
}); err != nil {
|
||||
t.Fatalf("bob send sticker: %v", err)
|
||||
}
|
||||
|
||||
historyReq := &tg.MessagesGetHistoryRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
|
||||
Limit: 10,
|
||||
}
|
||||
var raw bin.Buffer
|
||||
if err := historyReq.Encode(&raw); err != nil {
|
||||
t.Fatalf("encode history request: %v", err)
|
||||
}
|
||||
enc, err := r.Dispatch(WithUserID(ctx, alice.ID), [8]byte{}, 0, &raw)
|
||||
if err != nil {
|
||||
t.Fatalf("alice get history: %v", err)
|
||||
}
|
||||
box, ok := enc.(*tg.MessagesMessagesBox)
|
||||
if !ok {
|
||||
t.Fatalf("history response = %T, want *tg.MessagesMessagesBox", enc)
|
||||
}
|
||||
messages, ok := box.Messages.(*tg.MessagesMessages)
|
||||
if !ok {
|
||||
t.Fatalf("history payload = %T, want *tg.MessagesMessages", box.Messages)
|
||||
}
|
||||
if len(messages.Messages) != 1 {
|
||||
t.Fatalf("history messages = %d, want 1", len(messages.Messages))
|
||||
}
|
||||
msg, ok := messages.Messages[0].(*tg.Message)
|
||||
if !ok {
|
||||
t.Fatalf("history message = %T, want *tg.Message", messages.Messages[0])
|
||||
}
|
||||
media, ok := msg.Media.(*tg.MessageMediaDocument)
|
||||
if !ok {
|
||||
t.Fatalf("history media = %T, want *tg.MessageMediaDocument", msg.Media)
|
||||
}
|
||||
if got := tgDocumentID(media.Document); got != 101 {
|
||||
t.Fatalf("history document id = %d, want 101", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStickersBotCreatePackLinkInstallIsolationSmoke(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 21, Phone: "15550009101", FirstName: "Alice"})
|
||||
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009102", FirstName: "Bob"})
|
||||
dialogStore := memory.NewDialogStore()
|
||||
messageStore := memory.NewMessageStore(dialogStore)
|
||||
pollStore := memory.NewPollStore()
|
||||
messageStore.AttachPollStore(pollStore)
|
||||
passwordStore := memory.NewPasswordStore()
|
||||
accountService := appaccount.NewService(passwordStore, appaccount.WithUserStickerSets(passwordStore))
|
||||
botStore := memory.NewBotStore(userStore)
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
401: {
|
||||
ID: 401,
|
||||
AccessHash: 4401,
|
||||
DCID: 2,
|
||||
MimeType: "image/webp",
|
||||
Size: 4096,
|
||||
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: "alice.webp"}},
|
||||
},
|
||||
},
|
||||
photos: map[int64]domain.Photo{},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{},
|
||||
}
|
||||
botsService := botsapp.NewService(userStore, botStore, messageStore,
|
||||
botsapp.WithStickerSetCreator(files),
|
||||
botsapp.WithUserStickerSets(accountService))
|
||||
messagesService := appmessages.NewService(messageStore, dialogStore,
|
||||
appmessages.WithBotResponder(botsService))
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Account: accountService,
|
||||
Users: appusers.NewService(userStore),
|
||||
Messages: messagesService,
|
||||
Files: files,
|
||||
Polls: apppolls.NewService(pollStore),
|
||||
Sessions: &captureSessions{},
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
botsService.SetRouterHooks(r)
|
||||
|
||||
sendStickersBotText(t, r, alice, "/newpack", 9101)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "sticker pack")
|
||||
sendStickersBotText(t, r, alice, "Alice Bot Pack", 9102)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "Lottie JSON")
|
||||
sendStickersBotDocument(t, r, alice, 401, 4401, 9103)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "emoji")
|
||||
sendStickersBotText(t, r, alice, "🙂", 9104)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "Added")
|
||||
sendStickersBotText(t, r, alice, "/publish", 9105)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "short name")
|
||||
sendStickersBotText(t, r, alice, "alice_bot_pack", 9106)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "https://telesrv.net/addstickers/alice_bot_pack")
|
||||
|
||||
created := files.sets[domain.StickerSetKindStickers]
|
||||
if len(created) != 1 || created[0].ShortName != "alice_bot_pack" || created[0].CreatorUserID != alice.ID {
|
||||
t.Fatalf("created sets = %+v, want Alice alice_bot_pack", created)
|
||||
}
|
||||
setID := created[0].ID
|
||||
if got := installedStickerSetIDs(t, passwordStore, ctx, alice.ID, domain.StickerSetKindStickers, nil); len(got) != 1 || got[0] != setID {
|
||||
t.Fatalf("alice installed sets = %v, want [%d]", got, setID)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, passwordStore, ctx, bob.ID, domain.StickerSetKindStickers, nil); len(got) != 0 {
|
||||
t.Fatalf("bob installed sets before link = %v, want empty", got)
|
||||
}
|
||||
if got := allStickerSetIDs(t, r, WithUserID(ctx, bob.ID), domain.StickerSetKindStickers); len(got) != 0 {
|
||||
t.Fatalf("bob getAllStickers before install = %v, want empty", got)
|
||||
}
|
||||
|
||||
web := stickerlinks.NewHandler(files, "https://telesrv.net")
|
||||
rr := httptest.NewRecorder()
|
||||
web.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/addstickers/alice_bot_pack", nil))
|
||||
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "https://telesrv.net/addstickers/alice_bot_pack") {
|
||||
t.Fatalf("sticker bot link response = %d %q", rr.Code, rr.Body.String())
|
||||
}
|
||||
preview, err := r.onMessagesGetStickerSet(WithUserID(ctx, bob.ID), &tg.MessagesGetStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "alice_bot_pack"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bob preview bot-created sticker set: %v", err)
|
||||
}
|
||||
previewFull, ok := preview.(*tg.MessagesStickerSet)
|
||||
if !ok || previewFull.Set.ID != setID || previewFull.Set.InstalledDate != 0 {
|
||||
t.Fatalf("bob preview = %T %+v, want uninstalled created set", preview, preview)
|
||||
}
|
||||
if _, err := r.onMessagesInstallStickerSet(WithUserID(ctx, bob.ID), &tg.MessagesInstallStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "alice_bot_pack"},
|
||||
}); err != nil {
|
||||
t.Fatalf("bob install bot-created sticker set: %v", err)
|
||||
}
|
||||
if got := installedStickerSetIDs(t, passwordStore, ctx, bob.ID, domain.StickerSetKindStickers, nil); len(got) != 1 || got[0] != setID {
|
||||
t.Fatalf("bob installed sets after link = %v, want [%d]", got, setID)
|
||||
}
|
||||
if got := allStickerSetIDs(t, r, WithUserID(ctx, alice.ID), domain.StickerSetKindStickers); len(got) != 1 || got[0] != setID {
|
||||
t.Fatalf("alice getAllStickers = %v, want [%d]", got, setID)
|
||||
}
|
||||
if got := allStickerSetIDs(t, r, WithUserID(ctx, bob.ID), domain.StickerSetKindStickers); len(got) != 1 || got[0] != setID {
|
||||
t.Fatalf("bob getAllStickers after install = %v, want [%d]", got, setID)
|
||||
}
|
||||
if got := allStickerSetIDs(t, r, WithUserID(ctx, bob.ID), domain.StickerSetKindEmoji); len(got) != 0 {
|
||||
t.Fatalf("bob getEmojiStickers = %v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func sendStickersBotText(t *testing.T, r *Router, user domain.User, text string, randomID int64) {
|
||||
t.Helper()
|
||||
if _, err := r.onMessagesSendMessage(WithUserID(context.Background(), user.ID), &tg.MessagesSendMessageRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: domain.StickersBotUserID, AccessHash: domain.StickersBotAccessHash},
|
||||
Message: text,
|
||||
RandomID: randomID,
|
||||
}); err != nil {
|
||||
t.Fatalf("send @Stickers text %q: %v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendStickersBotDocument(t *testing.T, r *Router, user domain.User, docID, accessHash, randomID int64) {
|
||||
t.Helper()
|
||||
if _, err := r.onMessagesSendMedia(WithUserID(context.Background(), user.ID), &tg.MessagesSendMediaRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: domain.StickersBotUserID, AccessHash: domain.StickersBotAccessHash},
|
||||
Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: docID, AccessHash: accessHash}},
|
||||
RandomID: randomID,
|
||||
}); err != nil {
|
||||
t.Fatalf("send @Stickers document %d: %v", docID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForStickersReply(t *testing.T, messages *memory.MessageStore, userID int64, want string) string {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for {
|
||||
list, err := messages.ListByUser(context.Background(), userID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.StickersBotUserID},
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list @Stickers history: %v", err)
|
||||
}
|
||||
for _, msg := range list.Messages {
|
||||
if msg.From.ID == domain.StickersBotUserID && strings.Contains(msg.Body, want) {
|
||||
return msg.Body
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("no @Stickers reply containing %q; history=%+v", want, list.Messages)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func allStickerSetIDs(t *testing.T, r *Router, ctx context.Context, kind domain.StickerSetKind) []int64 {
|
||||
t.Helper()
|
||||
var (
|
||||
out tg.MessagesAllStickersClass
|
||||
err error
|
||||
)
|
||||
if kind == domain.StickerSetKindEmoji {
|
||||
out, err = r.onMessagesGetEmojiStickers(ctx, 0)
|
||||
} else {
|
||||
out, err = r.onMessagesGetAllStickers(ctx, 0)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("get sticker sets for kind %s: %v", kind, err)
|
||||
}
|
||||
full, ok := out.(*tg.MessagesAllStickers)
|
||||
if !ok {
|
||||
t.Fatalf("get sticker sets for kind %s = %T, want *tg.MessagesAllStickers", kind, out)
|
||||
}
|
||||
ids := make([]int64, 0, len(full.Sets))
|
||||
for _, set := range full.Sets {
|
||||
ids = append(ids, set.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
|
@ -90,6 +90,10 @@ func (r *Router) onMessagesGetStickerSet(ctx context.Context, req *tg.MessagesGe
|
|||
if fallbackSet, fallbackDocs, fallbackFound, fallbackErr := r.resolvePlaceholderStickerSet(ctx, ref); fallbackErr != nil {
|
||||
return nil, internalErr()
|
||||
} else if fallbackFound {
|
||||
fallbackSet, fallbackErr = r.stickerSetWithViewerInstallState(ctx, fallbackSet)
|
||||
if fallbackErr != nil {
|
||||
return nil, fallbackErr
|
||||
}
|
||||
if r.log != nil {
|
||||
r.log.Debug("getStickerSet placeholder fallback",
|
||||
zap.String("short_name", ref.ShortName),
|
||||
|
|
@ -109,6 +113,10 @@ func (r *Router) onMessagesGetStickerSet(ctx context.Context, req *tg.MessagesGe
|
|||
if req.Hash != 0 && req.Hash == set.Hash {
|
||||
return &tg.MessagesStickerSetNotModified{}, nil
|
||||
}
|
||||
set, err = r.stickerSetWithViewerInstallState(ctx, set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
|
|
@ -191,8 +199,14 @@ func (r *Router) allStickersForKind(ctx context.Context, hash int64, kind domain
|
|||
if r.deps.Files == nil {
|
||||
return messagesAllStickersEmpty(hash), nil
|
||||
}
|
||||
// perf:从目录缓存读集(TTL 内 hash 命中不打 PG)。
|
||||
sets := r.stickerCatalogSets(ctx, kind)
|
||||
sets, handled, err := r.installedStickerSetsForViewer(ctx, kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !handled {
|
||||
// 兼容无 per-user 安装态的测试/旧内存路径:从目录缓存读全局 installed 标志。
|
||||
sets = installedGlobalStickerSets(r.stickerCatalogSets(ctx, kind))
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return messagesAllStickersEmpty(hash), nil
|
||||
}
|
||||
|
|
@ -203,6 +217,107 @@ func (r *Router) allStickersForKind(ctx context.Context, hash int64, kind domain
|
|||
return &tg.MessagesAllStickers{Hash: catalogHash, Sets: tgStickerSets(sets)}, nil
|
||||
}
|
||||
|
||||
func (r *Router) installedStickerSetsForViewer(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, bool, error) {
|
||||
svc, ok := r.userStickerSetSvc()
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil || userID == 0 {
|
||||
if err != nil {
|
||||
return nil, true, internalErr()
|
||||
}
|
||||
return nil, true, nil
|
||||
}
|
||||
userSets, _, err := svc.ListUserStickerSets(ctx, userID, kind, nil, 0, domain.MaxInstalledStickerSets)
|
||||
if err != nil {
|
||||
return nil, true, internalErr()
|
||||
}
|
||||
out := make([]domain.StickerSet, 0, len(userSets))
|
||||
for _, item := range userSets {
|
||||
if item.Archived || item.StickerSetID == 0 {
|
||||
continue
|
||||
}
|
||||
set, _, found, err := r.deps.Files.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: item.StickerSetID})
|
||||
if err != nil {
|
||||
return nil, true, internalErr()
|
||||
}
|
||||
if !found || set.ID == 0 || set.Deleted || userStickerSetKind(set) != kind {
|
||||
continue
|
||||
}
|
||||
set = stickerSetWithoutViewerInstallState(set)
|
||||
out = append(out, stickerSetWithViewerInstallItem(set, item))
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
|
||||
func (r *Router) stickerSetsWithViewerInstallState(ctx context.Context, kind domain.StickerSetKind, sets []domain.StickerSet) ([]domain.StickerSet, error) {
|
||||
out := make([]domain.StickerSet, 0, len(sets))
|
||||
byID := make(map[int64]int, len(sets))
|
||||
for _, set := range sets {
|
||||
set = stickerSetWithoutViewerInstallState(set)
|
||||
byID[set.ID] = len(out)
|
||||
out = append(out, set)
|
||||
}
|
||||
svc, ok := r.userStickerSetSvc()
|
||||
if !ok {
|
||||
return out, nil
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 {
|
||||
return out, nil
|
||||
}
|
||||
userSets, _, err := svc.ListUserStickerSets(ctx, userID, kind, nil, 0, domain.MaxInstalledStickerSets)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
for _, item := range userSets {
|
||||
i, ok := byID[item.StickerSetID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i] = stickerSetWithViewerInstallItem(out[i], item)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) stickerSetWithViewerInstallState(ctx context.Context, set domain.StickerSet) (domain.StickerSet, error) {
|
||||
sets, err := r.stickerSetsWithViewerInstallState(ctx, userStickerSetKind(set), []domain.StickerSet{set})
|
||||
if err != nil {
|
||||
return domain.StickerSet{}, err
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return stickerSetWithoutViewerInstallState(set), nil
|
||||
}
|
||||
return sets[0], nil
|
||||
}
|
||||
|
||||
func stickerSetWithoutViewerInstallState(set domain.StickerSet) domain.StickerSet {
|
||||
set.Installed = false
|
||||
set.InstalledDate = 0
|
||||
return set
|
||||
}
|
||||
|
||||
func stickerSetWithViewerInstallItem(set domain.StickerSet, item domain.UserStickerSet) domain.StickerSet {
|
||||
set.Installed = true
|
||||
set.Archived = item.Archived
|
||||
set.InstalledDate = item.InstalledDate
|
||||
return set
|
||||
}
|
||||
|
||||
func installedGlobalStickerSets(sets []domain.StickerSet) []domain.StickerSet {
|
||||
out := make([]domain.StickerSet, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
if set.Installed && !set.Archived {
|
||||
out = append(out, set)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// featuredCoversPerSet 限制每个 featured 集解析的封面贴纸数量(trending 预览用)。
|
||||
const featuredCoversPerSet = 5
|
||||
|
||||
|
|
@ -233,7 +348,12 @@ func (r *Router) featuredStickersForKind(ctx context.Context, hash int64, kind d
|
|||
if len(visible) == 0 {
|
||||
return messagesFeaturedStickersEmpty(hash), nil
|
||||
}
|
||||
catalogHash := stickerSetsCatalogHash(visible)
|
||||
var err error
|
||||
visible, err = r.stickerSetsWithViewerInstallState(ctx, kind, visible)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
catalogHash := featuredStickerSetsHash(visible)
|
||||
if hash != 0 && hash == catalogHash {
|
||||
// 关键 perf 短路:目录未变直接返回,不解析任何封面文档。
|
||||
return &tg.MessagesFeaturedStickersNotModified{Count: len(visible)}, nil
|
||||
|
|
@ -333,6 +453,20 @@ func stickerSetsCatalogHash(sets []domain.StickerSet) int64 {
|
|||
return int64(tdesktopCountHash(values))
|
||||
}
|
||||
|
||||
func featuredStickerSetsHash(sets []domain.StickerSet) int64 {
|
||||
values := make([]int64, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
if set.ID == 0 {
|
||||
return 0
|
||||
}
|
||||
if set.Archived {
|
||||
continue
|
||||
}
|
||||
values = append(values, set.ID)
|
||||
}
|
||||
return int64(tdesktopCountHash(values))
|
||||
}
|
||||
|
||||
func boolHashValue(v bool) int64 {
|
||||
if v {
|
||||
return 1
|
||||
|
|
|
|||
351
internal/rpc/stickers_creator.go
Normal file
351
internal/rpc/stickers_creator.go
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) registerStickers(d *tg.ServerDispatcher) {
|
||||
d.OnStickersCreateStickerSet(r.onStickersCreateStickerSet)
|
||||
d.OnStickersCheckShortName(r.onStickersCheckShortName)
|
||||
d.OnStickersSuggestShortName(r.onStickersSuggestShortName)
|
||||
d.OnStickersAddStickerToSet(r.onStickersAddStickerToSet)
|
||||
d.OnStickersRemoveStickerFromSet(r.onStickersRemoveStickerFromSet)
|
||||
d.OnStickersChangeStickerPosition(r.onStickersChangeStickerPosition)
|
||||
d.OnStickersRenameStickerSet(r.onStickersRenameStickerSet)
|
||||
d.OnStickersDeleteStickerSet(r.onStickersDeleteStickerSet)
|
||||
}
|
||||
|
||||
func (r *Router) onStickersCreateStickerSet(ctx context.Context, req *tg.StickersCreateStickerSetRequest) (tg.MessagesStickerSetClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
if r.deps.Files == nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if req.Masks && req.Emojis {
|
||||
return nil, packTypeInvalidErr()
|
||||
}
|
||||
userID, err := r.stickerSetCreatorUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]domain.StickerSetItemInput, 0, len(req.Stickers))
|
||||
for _, item := range req.Stickers {
|
||||
id, accessHash, ok := inputDocumentRef(item.Document)
|
||||
if !ok {
|
||||
return nil, stickerFileInvalidErr()
|
||||
}
|
||||
items = append(items, domain.StickerSetItemInput{
|
||||
DocumentID: id,
|
||||
DocumentAccessHash: accessHash,
|
||||
Emoji: item.Emoji,
|
||||
Keywords: item.Keywords,
|
||||
})
|
||||
}
|
||||
thumbID, thumbAccessHash, ok := inputDocumentRef(req.Thumb)
|
||||
if req.Thumb != nil && !ok {
|
||||
return nil, stickerFileInvalidErr()
|
||||
}
|
||||
kind := domain.StickerSetKindStickers
|
||||
if req.Emojis {
|
||||
kind = domain.StickerSetKindEmoji
|
||||
} else if req.Masks {
|
||||
kind = domain.StickerSetKindMasks
|
||||
}
|
||||
set, docs, err := r.deps.Files.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||
CreatorUserID: userID,
|
||||
Title: req.Title,
|
||||
ShortName: req.ShortName,
|
||||
Kind: kind,
|
||||
TextColor: req.TextColor,
|
||||
ThumbDocumentID: thumbID,
|
||||
ThumbAccessHash: thumbAccessHash,
|
||||
Items: items,
|
||||
Software: req.Software,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, stickerSetCreateErr(err)
|
||||
}
|
||||
if svc, ok := r.userStickerSetSvc(); ok {
|
||||
if err := svc.InstallUserStickerSet(ctx, userID, set.ID, userStickerSetKind(set), false, int(r.clock.Now().Unix())); err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
set.Installed = true
|
||||
set.InstalledDate = int(r.clock.Now().Unix())
|
||||
}
|
||||
r.invalidateStickerCatalog(userStickerSetKind(set))
|
||||
r.pushStickerSetsUpdate(ctx, userID, userStickerSetKind(set))
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersCheckShortName(ctx context.Context, shortName string) (bool, error) {
|
||||
if r.deps.Files == nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
available, err := r.deps.Files.CheckStickerSetShortName(ctx, shortName)
|
||||
if err != nil {
|
||||
return false, stickerSetShortNameCheckErr(err)
|
||||
}
|
||||
return available, nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersSuggestShortName(ctx context.Context, title string) (*tg.StickersSuggestedShortName, error) {
|
||||
if r.deps.Files == nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
shortName, err := r.deps.Files.SuggestStickerSetShortName(ctx, title, userID)
|
||||
if err != nil {
|
||||
return nil, stickerSetSuggestShortNameErr(err)
|
||||
}
|
||||
return &tg.StickersSuggestedShortName{ShortName: shortName}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersAddStickerToSet(ctx context.Context, req *tg.StickersAddStickerToSetRequest) (tg.MessagesStickerSetClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, err := r.stickerSetActorUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref, ok := stickerSetRefFromInput(req.Stickerset)
|
||||
if !ok {
|
||||
return nil, stickersetInvalidErr()
|
||||
}
|
||||
documentID, accessHash, ok := inputDocumentRef(req.Sticker.Document)
|
||||
if !ok {
|
||||
return nil, stickerFileInvalidErr()
|
||||
}
|
||||
set, docs, err := r.deps.Files.AddStickerToSet(ctx, userID, ref, domain.StickerSetItemInput{
|
||||
DocumentID: documentID,
|
||||
DocumentAccessHash: accessHash,
|
||||
Emoji: req.Sticker.Emoji,
|
||||
Keywords: req.Sticker.Keywords,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, stickerSetManagementErr(err)
|
||||
}
|
||||
r.notifyStickerSetMutated(ctx, userID, set)
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersRemoveStickerFromSet(ctx context.Context, input tg.InputDocumentClass) (tg.MessagesStickerSetClass, error) {
|
||||
userID, err := r.stickerSetActorUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
documentID, accessHash, ok := inputDocumentRef(input)
|
||||
if !ok {
|
||||
return nil, stickerFileInvalidErr()
|
||||
}
|
||||
set, docs, err := r.deps.Files.RemoveStickerFromSet(ctx, userID, documentID, accessHash)
|
||||
if err != nil {
|
||||
return nil, stickerSetManagementErr(err)
|
||||
}
|
||||
r.notifyStickerSetMutated(ctx, userID, set)
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersChangeStickerPosition(ctx context.Context, req *tg.StickersChangeStickerPositionRequest) (tg.MessagesStickerSetClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, err := r.stickerSetActorUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
documentID, accessHash, ok := inputDocumentRef(req.Sticker)
|
||||
if !ok {
|
||||
return nil, stickerFileInvalidErr()
|
||||
}
|
||||
set, docs, err := r.deps.Files.ChangeStickerPosition(ctx, userID, documentID, accessHash, req.Position)
|
||||
if err != nil {
|
||||
return nil, stickerSetManagementErr(err)
|
||||
}
|
||||
r.notifyStickerSetMutated(ctx, userID, set)
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersRenameStickerSet(ctx context.Context, req *tg.StickersRenameStickerSetRequest) (tg.MessagesStickerSetClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, err := r.stickerSetActorUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref, ok := stickerSetRefFromInput(req.Stickerset)
|
||||
if !ok {
|
||||
return nil, stickersetInvalidErr()
|
||||
}
|
||||
set, docs, err := r.deps.Files.RenameStickerSet(ctx, userID, ref, req.Title)
|
||||
if err != nil {
|
||||
return nil, stickerSetManagementErr(err)
|
||||
}
|
||||
r.notifyStickerSetMutated(ctx, userID, set)
|
||||
return tgMessagesStickerSet(set, docs), nil
|
||||
}
|
||||
|
||||
func (r *Router) onStickersDeleteStickerSet(ctx context.Context, input tg.InputStickerSetClass) (bool, error) {
|
||||
userID, err := r.stickerSetActorUserID(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ref, ok := stickerSetRefFromInput(input)
|
||||
if !ok {
|
||||
return false, stickersetInvalidErr()
|
||||
}
|
||||
kind, err := r.deps.Files.DeleteStickerSet(ctx, userID, ref)
|
||||
if err != nil {
|
||||
return false, stickerSetManagementErr(err)
|
||||
}
|
||||
r.invalidateStickerCatalog(kind)
|
||||
r.pushStickerSetsUpdate(ctx, userID, kind)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) stickerSetActorUserID(ctx context.Context) (int64, error) {
|
||||
if r.deps.Files == nil {
|
||||
return 0, internalErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return 0, internalErr()
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (r *Router) notifyStickerSetMutated(ctx context.Context, userID int64, set domain.StickerSet) {
|
||||
kind := userStickerSetKind(set)
|
||||
r.invalidateStickerCatalog(kind)
|
||||
r.pushStickerSetsUpdate(ctx, userID, kind)
|
||||
}
|
||||
|
||||
func (r *Router) stickerSetCreatorUserID(ctx context.Context, input tg.InputUserClass) (int64, error) {
|
||||
currentUserID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return 0, internalErr()
|
||||
}
|
||||
if r.deps.Users == nil {
|
||||
switch v := input.(type) {
|
||||
case *tg.InputUserSelf:
|
||||
return currentUserID, nil
|
||||
case *tg.InputUser:
|
||||
if v != nil && v.UserID == currentUserID {
|
||||
return currentUserID, nil
|
||||
}
|
||||
}
|
||||
return 0, userIDInvalidErr()
|
||||
}
|
||||
user, found, err := r.userFromInput(ctx, currentUserID, input)
|
||||
if err != nil {
|
||||
return 0, internalErr()
|
||||
}
|
||||
if !found || user.ID != currentUserID {
|
||||
return 0, userIDInvalidErr()
|
||||
}
|
||||
return currentUserID, nil
|
||||
}
|
||||
|
||||
func inputDocumentRef(input tg.InputDocumentClass) (int64, int64, bool) {
|
||||
doc, ok := input.(*tg.InputDocument)
|
||||
if !ok || doc == nil || doc.ID == 0 || doc.AccessHash == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return doc.ID, doc.AccessHash, true
|
||||
}
|
||||
|
||||
func packShortNameInvalidErr() error { return tgerr400("PACK_SHORT_NAME_INVALID") }
|
||||
func packShortNameOccupiedErr() error { return tgerr400("PACK_SHORT_NAME_OCCUPIED") }
|
||||
func packTitleInvalidErr() error { return tgerr400("PACK_TITLE_INVALID") }
|
||||
func packTypeInvalidErr() error { return tgerr400("PACK_TYPE_INVALID") }
|
||||
func stickersEmptyErr() error { return tgerr400("STICKERS_EMPTY") }
|
||||
func stickersTooMuchErr() error { return tgerr400("STICKERS_TOO_MUCH") }
|
||||
func stickerEmojiInvalidErr() error { return tgerr400("STICKER_EMOJI_INVALID") }
|
||||
func stickerFileInvalidErr() error { return tgerr400("STICKER_FILE_INVALID") }
|
||||
func shortNameInvalidErr() error { return tgerr400("SHORT_NAME_INVALID") }
|
||||
func titleInvalidErr() error { return tgerr400("TITLE_INVALID") }
|
||||
func positionInvalidErr() error { return tgerr400("POSITION_INVALID") }
|
||||
|
||||
func stickerSetCreateErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStickerSetTitleInvalid):
|
||||
return packTitleInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetShortNameInvalid):
|
||||
return packShortNameInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetShortNameOccupied):
|
||||
return packShortNameOccupiedErr()
|
||||
case errors.Is(err, domain.ErrStickerSetTypeInvalid):
|
||||
return packTypeInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetEmpty):
|
||||
return stickersEmptyErr()
|
||||
case errors.Is(err, domain.ErrStickerSetTooMuch):
|
||||
return stickersTooMuchErr()
|
||||
case errors.Is(err, domain.ErrStickerSetEmojiInvalid):
|
||||
return stickerEmojiInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetFileInvalid), errors.Is(err, domain.ErrDocumentInvalid):
|
||||
return stickerFileInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetCreatorInvalid):
|
||||
return userIDInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func stickerSetShortNameCheckErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStickerSetShortNameInvalid):
|
||||
return shortNameInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func stickerSetSuggestShortNameErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStickerSetTitleInvalid):
|
||||
return titleInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetShortNameOccupied):
|
||||
return packShortNameOccupiedErr()
|
||||
case errors.Is(err, domain.ErrStickerSetCreatorInvalid):
|
||||
return userIDInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func stickerSetManagementErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStickerSetTitleInvalid):
|
||||
return packTitleInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetEmpty):
|
||||
return stickersEmptyErr()
|
||||
case errors.Is(err, domain.ErrStickerSetTooMuch):
|
||||
return stickersTooMuchErr()
|
||||
case errors.Is(err, domain.ErrStickerSetEmojiInvalid):
|
||||
return stickerEmojiInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetFileInvalid), errors.Is(err, domain.ErrDocumentInvalid):
|
||||
return stickerFileInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetCreatorInvalid):
|
||||
return userIDInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetPositionInvalid):
|
||||
return positionInvalidErr()
|
||||
case errors.Is(err, domain.ErrStickerSetInvalid), errors.Is(err, domain.ErrStickerSetNotOwned):
|
||||
return stickersetInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
253
internal/rpc/stickers_creator_rpc_test.go
Normal file
253
internal/rpc/stickers_creator_rpc_test.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appaccount "telesrv/internal/app/account"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func stickerCreatorRouter(t *testing.T) (*Router, *fakeFiles, *memory.PasswordStore, *captureSessions) {
|
||||
t.Helper()
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
101: {ID: 101, AccessHash: 11, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
102: {ID: 102, AccessHash: 12, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
103: {ID: 103, AccessHash: 13, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{},
|
||||
}
|
||||
passwordStore := memory.NewPasswordStore()
|
||||
sessions := &captureSessions{}
|
||||
router := New(Config{}, Deps{
|
||||
Account: appaccount.NewService(passwordStore, appaccount.WithUserStickerSets(passwordStore)),
|
||||
Files: files,
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
return router, files, passwordStore, sessions
|
||||
}
|
||||
|
||||
func TestStickersCreateStickerSetInstallsAndInvalidatesCatalog(t *testing.T) {
|
||||
r, _, store, sessions := stickerCreatorRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
before, err := r.onMessagesGetAllStickers(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get all before create: %v", err)
|
||||
}
|
||||
if full, ok := before.(*tg.MessagesAllStickers); ok && len(full.Sets) != 0 {
|
||||
t.Fatalf("all stickers before create = %+v, want empty", full.Sets)
|
||||
}
|
||||
|
||||
out, err := r.onStickersCreateStickerSet(ctx, &tg.StickersCreateStickerSetRequest{
|
||||
UserID: &tg.InputUserSelf{},
|
||||
Title: "Fresh Pack",
|
||||
ShortName: "fresh_pack",
|
||||
Stickers: []tg.InputStickerSetItem{{
|
||||
Document: &tg.InputDocument{ID: 101, AccessHash: 11},
|
||||
Emoji: "🙂",
|
||||
Keywords: "fresh,happy",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sticker set: %v", err)
|
||||
}
|
||||
full, ok := out.(*tg.MessagesStickerSet)
|
||||
if !ok {
|
||||
t.Fatalf("create result = %T, want *tg.MessagesStickerSet", out)
|
||||
}
|
||||
if full.Set.ShortName != "fresh_pack" || !full.Set.Creator || full.Set.InstalledDate == 0 {
|
||||
t.Fatalf("created set = %+v, want creator installed fresh_pack", full.Set)
|
||||
}
|
||||
if len(full.Packs) != 1 || len(full.Keywords) != 1 || len(full.Documents) != 1 {
|
||||
t.Fatalf("created payload packs=%d keywords=%d docs=%d, want 1/1/1", len(full.Packs), len(full.Keywords), len(full.Documents))
|
||||
}
|
||||
if got := installedStickerSetIDs(t, store, ctx, 1000000001, domain.StickerSetKindStickers, nil); len(got) != 1 || got[0] != full.Set.ID {
|
||||
t.Fatalf("installed created set ids = %v, want [%d]", got, full.Set.ID)
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindStickers, nil)
|
||||
|
||||
owned, err := r.onMessagesGetMyStickers(ctx, &tg.MessagesGetMyStickersRequest{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("get my stickers: %v", err)
|
||||
}
|
||||
if owned.Count != 1 || len(owned.Sets) != 1 {
|
||||
t.Fatalf("my stickers = count %d sets %d, want one created set", owned.Count, len(owned.Sets))
|
||||
}
|
||||
|
||||
after, err := r.onMessagesGetAllStickers(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get all after create: %v", err)
|
||||
}
|
||||
all, ok := after.(*tg.MessagesAllStickers)
|
||||
if !ok {
|
||||
t.Fatalf("all after create = %T, want *tg.MessagesAllStickers", after)
|
||||
}
|
||||
if len(all.Sets) != 1 || all.Sets[0].ID != full.Set.ID {
|
||||
t.Fatalf("all after create = %+v, want created set", all.Sets)
|
||||
}
|
||||
|
||||
available, err := r.onStickersCheckShortName(ctx, "fresh_pack")
|
||||
if err != nil {
|
||||
t.Fatalf("check short name: %v", err)
|
||||
}
|
||||
if available {
|
||||
t.Fatalf("fresh_pack available = true, want false after create")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStickersCreateStickerSetRejectsBadDocumentAccessHash(t *testing.T) {
|
||||
r, _, _, _ := stickerCreatorRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
out, err := r.onStickersCreateStickerSet(ctx, &tg.StickersCreateStickerSetRequest{
|
||||
UserID: &tg.InputUserSelf{},
|
||||
Title: "Fresh Pack",
|
||||
ShortName: "fresh_pack",
|
||||
Stickers: []tg.InputStickerSetItem{{
|
||||
Document: &tg.InputDocument{ID: 101, AccessHash: 999},
|
||||
Emoji: "🙂",
|
||||
}},
|
||||
})
|
||||
if out != nil || !tgerr.Is(err, "STICKER_FILE_INVALID") {
|
||||
t.Fatalf("create with bad document hash = %T %v, want STICKER_FILE_INVALID", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStickersSuggestAndCheckShortNameValidation(t *testing.T) {
|
||||
r, _, _, _ := stickerCreatorRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
suggested, err := r.onStickersSuggestShortName(ctx, "Fresh Pack")
|
||||
if err != nil {
|
||||
t.Fatalf("suggest short name: %v", err)
|
||||
}
|
||||
if suggested.ShortName == "" {
|
||||
t.Fatalf("suggested short name empty")
|
||||
}
|
||||
if ok, err := r.onStickersCheckShortName(ctx, "bad!"); ok || !tgerr.Is(err, "SHORT_NAME_INVALID") {
|
||||
t.Fatalf("check invalid short name = %v %v, want SHORT_NAME_INVALID", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStickersManageCreatedStickerSetRPCs(t *testing.T) {
|
||||
r, files, _, sessions := stickerCreatorRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
created, err := r.onStickersCreateStickerSet(ctx, &tg.StickersCreateStickerSetRequest{
|
||||
UserID: &tg.InputUserSelf{},
|
||||
Title: "Fresh Pack",
|
||||
ShortName: "fresh_pack",
|
||||
Stickers: []tg.InputStickerSetItem{{
|
||||
Document: &tg.InputDocument{ID: 101, AccessHash: 11},
|
||||
Emoji: "🙂",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sticker set: %v", err)
|
||||
}
|
||||
full := created.(*tg.MessagesStickerSet)
|
||||
setInput := &tg.InputStickerSetID{ID: full.Set.ID, AccessHash: full.Set.AccessHash}
|
||||
|
||||
added, err := r.onStickersAddStickerToSet(ctx, &tg.StickersAddStickerToSetRequest{
|
||||
Stickerset: setInput,
|
||||
Sticker: tg.InputStickerSetItem{
|
||||
Document: &tg.InputDocument{ID: 102, AccessHash: 12},
|
||||
Emoji: "😄",
|
||||
Keywords: "smile",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("add sticker: %v", err)
|
||||
}
|
||||
addedFull := added.(*tg.MessagesStickerSet)
|
||||
if addedFull.Set.Count != 2 || len(addedFull.Documents) != 2 || len(addedFull.Keywords) != 1 {
|
||||
t.Fatalf("after add count=%d docs=%d keywords=%d, want 2/2/1", addedFull.Set.Count, len(addedFull.Documents), len(addedFull.Keywords))
|
||||
}
|
||||
assertStickerSetsUpdate(t, sessions.lastUserPush(), domain.StickerSetKindStickers, nil)
|
||||
|
||||
moved, err := r.onStickersChangeStickerPosition(ctx, &tg.StickersChangeStickerPositionRequest{
|
||||
Sticker: &tg.InputDocument{ID: 102, AccessHash: 12},
|
||||
Position: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("change sticker position: %v", err)
|
||||
}
|
||||
movedFull := moved.(*tg.MessagesStickerSet)
|
||||
if len(movedFull.Documents) != 2 || tgDocumentID(movedFull.Documents[0]) != 102 {
|
||||
t.Fatalf("documents after move = %+v, want doc 102 first", movedFull.Documents)
|
||||
}
|
||||
|
||||
renamed, err := r.onStickersRenameStickerSet(ctx, &tg.StickersRenameStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetShortName{ShortName: "fresh_pack"},
|
||||
Title: "Renamed Pack",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("rename sticker set: %v", err)
|
||||
}
|
||||
renamedFull := renamed.(*tg.MessagesStickerSet)
|
||||
if renamedFull.Set.Title != "Renamed Pack" {
|
||||
t.Fatalf("renamed title = %q, want Renamed Pack", renamedFull.Set.Title)
|
||||
}
|
||||
|
||||
removed, err := r.onStickersRemoveStickerFromSet(ctx, &tg.InputDocument{ID: 102, AccessHash: 12})
|
||||
if err != nil {
|
||||
t.Fatalf("remove sticker: %v", err)
|
||||
}
|
||||
removedFull := removed.(*tg.MessagesStickerSet)
|
||||
if removedFull.Set.Count != 1 || len(removedFull.Documents) != 1 || tgDocumentID(removedFull.Documents[0]) != 101 {
|
||||
t.Fatalf("after remove count=%d docs=%+v, want only doc 101", removedFull.Set.Count, removedFull.Documents)
|
||||
}
|
||||
|
||||
ok, err := r.onStickersDeleteStickerSet(ctx, setInput)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("delete sticker set = %v %v, want true nil", ok, err)
|
||||
}
|
||||
if _, _, found, err := files.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByShortName, ShortName: "fresh_pack"}); err != nil || found {
|
||||
t.Fatalf("resolve deleted set = found %v err %v, want miss", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStickersManageCreatedStickerSetRejectsNonCreator(t *testing.T) {
|
||||
r, _, _, _ := stickerCreatorRouter(t)
|
||||
ownerCtx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
created, err := r.onStickersCreateStickerSet(ownerCtx, &tg.StickersCreateStickerSetRequest{
|
||||
UserID: &tg.InputUserSelf{},
|
||||
Title: "Fresh Pack",
|
||||
ShortName: "fresh_pack",
|
||||
Stickers: []tg.InputStickerSetItem{{
|
||||
Document: &tg.InputDocument{ID: 101, AccessHash: 11},
|
||||
Emoji: "🙂",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sticker set: %v", err)
|
||||
}
|
||||
full := created.(*tg.MessagesStickerSet)
|
||||
otherCtx := WithUserID(context.Background(), 1000000002)
|
||||
out, err := r.onStickersAddStickerToSet(otherCtx, &tg.StickersAddStickerToSetRequest{
|
||||
Stickerset: &tg.InputStickerSetID{ID: full.Set.ID, AccessHash: full.Set.AccessHash},
|
||||
Sticker: tg.InputStickerSetItem{
|
||||
Document: &tg.InputDocument{ID: 102, AccessHash: 12},
|
||||
Emoji: "😄",
|
||||
},
|
||||
})
|
||||
if out != nil || !tgerr.Is(err, "STICKERSET_INVALID") {
|
||||
t.Fatalf("non-creator add = %T %v, want STICKERSET_INVALID", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func tgDocumentID(doc tg.DocumentClass) int64 {
|
||||
if d, ok := doc.(*tg.Document); ok && d != nil {
|
||||
return d.ID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
@ -26,6 +26,21 @@ func TestStickerSetsCatalogHashMatchesTDesktopFormula(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFeaturedStickerSetsHashMatchesTDesktopFormula(t *testing.T) {
|
||||
sets := []domain.StickerSet{
|
||||
{ID: 10, Hash: 123},
|
||||
{ID: 11, Hash: 456},
|
||||
}
|
||||
got := featuredStickerSetsHash(sets)
|
||||
const want int64 = 365072220181
|
||||
if got != want {
|
||||
t.Fatalf("featuredStickerSetsHash() = %d, want %d", got, want)
|
||||
}
|
||||
if catalog := stickerSetsCatalogHash(sets); catalog == got {
|
||||
t.Fatalf("test fixture no longer distinguishes featured hash from catalog hash: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetAllStickersUsesTDesktopHashForNotModified(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &fakeFiles{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue