fix: sync birthday updates and system sticker sets
This commit is contained in:
parent
c38dd73fdd
commit
ddcf2d31ca
10 changed files with 431 additions and 35 deletions
|
|
@ -295,7 +295,8 @@ func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey str
|
|||
return nil // 该目录无 set_info.json → 跳过
|
||||
}
|
||||
var info struct {
|
||||
Result seedStickerSetResultJSON `json:"result"`
|
||||
InputSetType string `json:"input_set_type"`
|
||||
Result seedStickerSetResultJSON `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &info); err != nil {
|
||||
return fmt.Errorf("parse %s: %w", infoPath, err)
|
||||
|
|
@ -304,13 +305,18 @@ func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey str
|
|||
if sj.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
if systemKey == "" {
|
||||
systemKey = systemKeyForInputSetType(info.InputSetType)
|
||||
}
|
||||
kind := stickerSetKind(sj, systemKey)
|
||||
// 增量 seed:集已存在且内容 hash 未变则跳过(不重读文档/重传 blob)。force 时强制重导
|
||||
// (缩略图内联缓存修复路径)。这让 seedStickerSets 可在非空 store 上每次启动安全重扫,
|
||||
// 仅导入新增/变更集。
|
||||
// 仅导入新增/变更集;但系统集分类由导出元数据决定,旧库中 hash 相同但 system_key/kind
|
||||
// 错误的记录必须重写,避免 constructor 方式取不到同一个资源集。
|
||||
if !force {
|
||||
if existing, found, err := s.media.GetStickerSetByID(ctx, sj.ID); err != nil {
|
||||
return err
|
||||
} else if found && existing.Hash == sj.Hash {
|
||||
} else if found && existing.Hash == sj.Hash && existing.SystemKey == systemKey && existing.Kind == kind {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -333,7 +339,6 @@ func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey str
|
|||
}
|
||||
}
|
||||
|
||||
kind := stickerSetKind(sj, systemKey)
|
||||
set := domain.StickerSet{
|
||||
ID: sj.ID,
|
||||
AccessHash: sj.AccessHash,
|
||||
|
|
@ -558,6 +563,8 @@ func systemKeyForDefaultSet(dirName string) string {
|
|||
return domain.StickerSetSystemKeyEmojiDefaultTopicIcons
|
||||
case "DefaultSet_PremiumGifts":
|
||||
return domain.StickerSetSystemKeyPremiumGifts
|
||||
case "DefaultSet_TonGifts":
|
||||
return domain.StickerSetSystemKeyTonGifts
|
||||
case "DefaultSet_Dice_Normal":
|
||||
return "dice:\U0001f3b2"
|
||||
case "DefaultSet_Dice_Dart":
|
||||
|
|
@ -575,6 +582,30 @@ func systemKeyForDefaultSet(dirName string) string {
|
|||
}
|
||||
}
|
||||
|
||||
func systemKeyForInputSetType(inputSetType string) string {
|
||||
normalized := strings.TrimSpace(inputSetType)
|
||||
normalized = strings.TrimPrefix(normalized, "*")
|
||||
normalized = strings.TrimPrefix(normalized, "tg.")
|
||||
switch normalized {
|
||||
case "InputStickerSetAnimatedEmoji":
|
||||
return "animated_emoji"
|
||||
case "InputStickerSetAnimatedEmojiAnimations":
|
||||
return "animated_emoji_animations"
|
||||
case "InputStickerSetEmojiGenericAnimations":
|
||||
return "emoji_generic_animations"
|
||||
case "InputStickerSetEmojiDefaultStatuses", "InputStickerSetEmojiChannelDefaultStatuses":
|
||||
return domain.StickerSetSystemKeyEmojiDefaultStatuses
|
||||
case "InputStickerSetEmojiDefaultTopicIcons":
|
||||
return domain.StickerSetSystemKeyEmojiDefaultTopicIcons
|
||||
case "InputStickerSetPremiumGifts":
|
||||
return domain.StickerSetSystemKeyPremiumGifts
|
||||
case "InputStickerSetTonGifts":
|
||||
return domain.StickerSetSystemKeyTonGifts
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func stickerSetKind(sj seedStickerSetJSON, systemKey string) domain.StickerSetKind {
|
||||
switch {
|
||||
case systemKey != "":
|
||||
|
|
|
|||
|
|
@ -757,6 +757,155 @@ func TestSeedDocumentIDsAreImportedVerbatim(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSeedMediaUsesInputSetTypeForSystemSets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
seedDir := t.TempDir()
|
||||
cases := []struct {
|
||||
name string
|
||||
category string
|
||||
dirName string
|
||||
inputSetType string
|
||||
shortName string
|
||||
id int64
|
||||
docID int64
|
||||
emojis bool
|
||||
wantSystem string
|
||||
}{
|
||||
{
|
||||
name: "default topic icons from emoji export",
|
||||
category: "telegram_emoji_export",
|
||||
dirName: "Topics_7173162320003082",
|
||||
inputSetType: "*tg.InputStickerSetEmojiDefaultTopicIcons",
|
||||
shortName: "Topics",
|
||||
id: 7173162320003082,
|
||||
docID: 5312536423851630001,
|
||||
emojis: true,
|
||||
wantSystem: domain.StickerSetSystemKeyEmojiDefaultTopicIcons,
|
||||
},
|
||||
{
|
||||
name: "premium gifts from fetched sticker export",
|
||||
category: "telegram_stickers_export",
|
||||
dirName: "PremiumGifts_328917524764688479",
|
||||
inputSetType: "InputStickerSetPremiumGifts",
|
||||
shortName: "PremiumGifts",
|
||||
id: 328917524764688479,
|
||||
docID: 5710643921839718407,
|
||||
wantSystem: domain.StickerSetSystemKeyPremiumGifts,
|
||||
},
|
||||
{
|
||||
name: "ton gifts from fetched sticker export",
|
||||
category: "telegram_stickers_export",
|
||||
dirName: "TonGifts_9000000000000001",
|
||||
inputSetType: "*tg.InputStickerSetTonGifts",
|
||||
shortName: "TonGifts",
|
||||
id: 9000000000000001,
|
||||
docID: 9000000000000101,
|
||||
wantSystem: domain.StickerSetSystemKeyTonGifts,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := filepath.Join(seedDir, tc.category, tc.dirName)
|
||||
writeInputSetTypeSeed(t, dir, tc.inputSetType, tc.shortName, tc.id, tc.docID, tc.emojis)
|
||||
|
||||
media := newFakeMediaStore()
|
||||
blobs, err := NewLocalFS(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("local fs: %v", err)
|
||||
}
|
||||
svc := NewService(media, blobs, 2)
|
||||
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
|
||||
t.Fatalf("seed media: %v", err)
|
||||
}
|
||||
set, found, err := media.GetStickerSetBySystemKey(ctx, tc.wantSystem)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("GetStickerSetBySystemKey(%q) found=%v err=%v", tc.wantSystem, found, err)
|
||||
}
|
||||
if set.Kind != domain.StickerSetKindSystem || set.SystemKey != tc.wantSystem {
|
||||
t.Fatalf("set meta = %+v, want system key %q", set, tc.wantSystem)
|
||||
}
|
||||
if set.ID != tc.id || set.ShortName != tc.shortName || len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != tc.docID {
|
||||
t.Fatalf("set = %+v, want id=%d short=%s doc=%d", set, tc.id, tc.shortName, tc.docID)
|
||||
}
|
||||
resolved, docs, found, err := svc.ResolveStickerSet(ctx, domain.StickerSetRef{
|
||||
Kind: domain.StickerSetRefBySystem,
|
||||
SystemKey: tc.wantSystem,
|
||||
})
|
||||
if err != nil || !found {
|
||||
t.Fatalf("ResolveStickerSet(%q) found=%v err=%v", tc.wantSystem, found, err)
|
||||
}
|
||||
if resolved.ID != tc.id || len(docs) != 1 || docs[0].ID != tc.docID {
|
||||
t.Fatalf("resolved = set %d docs %+v, want set %d doc %d", resolved.ID, docs, tc.id, tc.docID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedMediaReclassifiesExistingSystemSetWhenHashUnchanged(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
seedDir := t.TempDir()
|
||||
const (
|
||||
setID int64 = 7173162320003082
|
||||
docID int64 = 5312536423851630001
|
||||
shortName = "Topics"
|
||||
)
|
||||
writeInputSetTypeSeed(t,
|
||||
filepath.Join(seedDir, "telegram_emoji_export", "Topics_7173162320003082"),
|
||||
"*tg.InputStickerSetEmojiDefaultTopicIcons",
|
||||
shortName,
|
||||
setID,
|
||||
docID,
|
||||
true,
|
||||
)
|
||||
|
||||
media := newFakeMediaStore()
|
||||
if err := media.PutStickerSet(ctx, domain.StickerSet{
|
||||
ID: setID,
|
||||
ShortName: shortName,
|
||||
Hash: 17,
|
||||
Kind: domain.StickerSetKindEmoji,
|
||||
}); err != nil {
|
||||
t.Fatalf("preload existing set: %v", err)
|
||||
}
|
||||
blobs, err := NewLocalFS(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("local fs: %v", err)
|
||||
}
|
||||
svc := NewService(media, blobs, 2)
|
||||
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
|
||||
t.Fatalf("seed media: %v", err)
|
||||
}
|
||||
set, found, err := media.GetStickerSetBySystemKey(ctx, domain.StickerSetSystemKeyEmojiDefaultTopicIcons)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("GetStickerSetBySystemKey(topic_icons) found=%v err=%v", found, err)
|
||||
}
|
||||
if set.Kind != domain.StickerSetKindSystem || set.SystemKey != domain.StickerSetSystemKeyEmojiDefaultTopicIcons {
|
||||
t.Fatalf("set meta = %+v, want reclassified system set", set)
|
||||
}
|
||||
if len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != docID {
|
||||
t.Fatalf("set document ids = %+v, want [%d]", set.DocumentIDs, docID)
|
||||
}
|
||||
}
|
||||
|
||||
func writeInputSetTypeSeed(t *testing.T, setDir, inputSetType, shortName string, setID, docID int64, emojis bool) {
|
||||
t.Helper()
|
||||
stickersDir := filepath.Join(setDir, "stickers")
|
||||
if err := os.MkdirAll(stickersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
attrType := "DocumentAttributeSticker"
|
||||
if emojis {
|
||||
attrType = "DocumentAttributeCustomEmoji"
|
||||
}
|
||||
raw := fmt.Sprintf(`{"api_call":"messages.getStickerSet","input_set_type":%q,"result":{"_":"StickerSet","set":{"id":%d,"access_hash":2,"title":%q,"short_name":%q,"count":1,"hash":17,"emojis":%v},"packs":[{"emoticon":"🎁","documents":[%d]}],"documents":[{"id":%d,"access_hash":3,"file_reference":"","date":"2026-07-05T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"dc_id":4,"attributes":[{"_":"DocumentAttributeImageSize","w":512,"h":512},{"_":%q,"alt":"🎁","stickerset":{"id":%d,"access_hash":2}},{"_":"DocumentAttributeFilename","file_name":"AnimatedSticker.tgs"}],"thumbs":[]}]}}`, inputSetType, setID, shortName, shortName, emojis, docID, docID, attrType, setID)
|
||||
if err := os.WriteFile(filepath.Join(setDir, "set_info.json"), []byte(raw), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(stickersDir, fmt.Sprintf("system_%d.tgs", docID)), []byte("tgs!"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedStickerSetInstalledFlagNeverMarksViewerState(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -680,6 +680,8 @@ const (
|
|||
StickerSetSystemKeyEmojiDefaultTopicIcons = "emoji_default_topic_icons"
|
||||
// StickerSetSystemKeyPremiumGifts 对应 premium gifts 系统集。
|
||||
StickerSetSystemKeyPremiumGifts = "premium_gifts"
|
||||
// StickerSetSystemKeyTonGifts 对应 TON gifts 系统集。
|
||||
StickerSetSystemKeyTonGifts = "ton_gifts"
|
||||
)
|
||||
|
||||
// StickerSetKind 区分贴纸集用途(影响 getAllStickers / getEmojiStickers 归类)。
|
||||
|
|
|
|||
|
|
@ -1292,8 +1292,8 @@ func (r *Router) onAccountUpdateUsername(ctx context.Context, username string) (
|
|||
}
|
||||
|
||||
// onAccountUpdateBirthday 持久化资料页生日(account.updateBirthday)。birthday 缺省即清除;
|
||||
// 月/日/年非法返回 BIRTHDAY_INVALID。生日落在 userFull(按隐私 PrivacyKeyBirthday 对外裁剪),
|
||||
// 故只需失效本人 userFull 投影缓存,客户端重拉 getFullUser 即见最新值。
|
||||
// 月/日/年非法返回 BIRTHDAY_INVALID。生日落在 userFull(按隐私 PrivacyKeyBirthday 对外裁剪)。
|
||||
// 写入后推 updateUser 信号给本人其它在线 session,促使已加载 full profile 的客户端重拉。
|
||||
func (r *Router) onAccountUpdateBirthday(ctx context.Context, req *tg.AccountUpdateBirthdayRequest) (bool, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -1318,6 +1318,7 @@ func (r *Router) onAccountUpdateBirthday(ctx context.Context, req *tg.AccountUpd
|
|||
return false, internalErr()
|
||||
}
|
||||
r.invalidateRPCProjectionForUser(u.ID)
|
||||
r.pushSelfUserChangedUpdate(ctx, u)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -1509,6 +1510,17 @@ func (r *Router) pushUsernameUpdate(ctx context.Context, u domain.User) {
|
|||
})
|
||||
}
|
||||
|
||||
func (r *Router) pushSelfUserChangedUpdate(ctx context.Context, u domain.User) {
|
||||
if u.ID == 0 {
|
||||
return
|
||||
}
|
||||
r.pushUserUpdates(ctx, u.ID, &tg.Updates{
|
||||
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
|
||||
Users: []tg.UserClass{r.tgSelfUser(u)},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
}
|
||||
|
||||
func tgAuthorization(a domain.Authorization, currentAuthKeyID [8]byte, now int) tg.Authorization {
|
||||
created := int(a.CreatedAt.Unix())
|
||||
if created == 0 {
|
||||
|
|
|
|||
84
internal/rpc/account_birthday_rpc_test.go
Normal file
84
internal/rpc/account_birthday_rpc_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func TestAccountUpdateBirthdayPersistsFullUserAndPushesRefresh(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550003301", FirstName: "Owner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
sessions := &captureSessions{}
|
||||
router := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
birthday := tg.Birthday{Day: 14, Month: 2}
|
||||
birthday.SetYear(1990)
|
||||
req := &tg.AccountUpdateBirthdayRequest{}
|
||||
req.SetBirthday(birthday)
|
||||
callCtx := WithSessionID(WithUserID(ctx, owner.ID), 4242)
|
||||
ok, err := router.onAccountUpdateBirthday(callCtx, req)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("update birthday = ok %v err %v, want true/nil", ok, err)
|
||||
}
|
||||
|
||||
saved, found, err := userStore.ByID(ctx, owner.ID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load saved user found=%v err=%v", found, err)
|
||||
}
|
||||
if saved.Birthday != (domain.Birthday{Day: 14, Month: 2, Year: 1990}) {
|
||||
t.Fatalf("saved birthday = %+v, want 14/2/1990", saved.Birthday)
|
||||
}
|
||||
|
||||
full, err := router.onUsersGetFullUser(WithUserID(ctx, owner.ID), &tg.InputUserSelf{})
|
||||
if err != nil {
|
||||
t.Fatalf("get full user: %v", err)
|
||||
}
|
||||
gotBirthday, ok := full.FullUser.GetBirthday()
|
||||
if !ok {
|
||||
t.Fatal("full user missing birthday")
|
||||
}
|
||||
gotYear, gotYearOK := gotBirthday.GetYear()
|
||||
if gotBirthday.Day != 14 || gotBirthday.Month != 2 || !gotYearOK || gotYear != 1990 {
|
||||
t.Fatalf("full user birthday = %+v yearOK=%v, want 14/2/1990", gotBirthday, gotYearOK)
|
||||
}
|
||||
|
||||
snap := sessions.snapshot()
|
||||
if snap.userID != owner.ID || snap.sessionID != 4242 {
|
||||
t.Fatalf("push target user/session = %d/%d, want %d/4242", snap.userID, snap.sessionID, owner.ID)
|
||||
}
|
||||
updates, ok := snap.message.(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("pushed message = %T, want *tg.Updates", snap.message)
|
||||
}
|
||||
hasUserUpdate := false
|
||||
for _, update := range updates.Updates {
|
||||
if u, ok := update.(*tg.UpdateUser); ok && u.UserID == owner.ID {
|
||||
hasUserUpdate = true
|
||||
}
|
||||
}
|
||||
if !hasUserUpdate {
|
||||
t.Fatalf("updates = %+v, want UpdateUser for self", updates.Updates)
|
||||
}
|
||||
if len(updates.Users) != 1 {
|
||||
t.Fatalf("pushed users = %d, want 1 self user", len(updates.Users))
|
||||
}
|
||||
pushedUser, ok := updates.Users[0].(*tg.User)
|
||||
if !ok || pushedUser.ID != owner.ID {
|
||||
t.Fatalf("pushed user = %T %+v, want self user", updates.Users[0], updates.Users[0])
|
||||
}
|
||||
}
|
||||
|
|
@ -411,6 +411,14 @@ func tgInputStickerSetFromSystemKey(systemKey string) (tg.InputStickerSetClass,
|
|||
return &tg.InputStickerSetAnimatedEmojiAnimations{}, true
|
||||
case "emoji_generic_animations":
|
||||
return &tg.InputStickerSetEmojiGenericAnimations{}, true
|
||||
case domain.StickerSetSystemKeyEmojiDefaultStatuses:
|
||||
return &tg.InputStickerSetEmojiDefaultStatuses{}, true
|
||||
case domain.StickerSetSystemKeyEmojiDefaultTopicIcons:
|
||||
return &tg.InputStickerSetEmojiDefaultTopicIcons{}, true
|
||||
case domain.StickerSetSystemKeyPremiumGifts:
|
||||
return &tg.InputStickerSetPremiumGifts{}, true
|
||||
case domain.StickerSetSystemKeyTonGifts:
|
||||
return &tg.InputStickerSetTonGifts{}, true
|
||||
default:
|
||||
if strings.HasPrefix(systemKey, "dice:") {
|
||||
return &tg.InputStickerSetDice{Emoticon: strings.TrimPrefix(systemKey, "dice:")}, true
|
||||
|
|
@ -705,6 +713,8 @@ func stickerSetRefFromInput(input tg.InputStickerSetClass) (domain.StickerSetRef
|
|||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyEmojiDefaultTopicIcons}, true
|
||||
case *tg.InputStickerSetPremiumGifts:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyPremiumGifts}, true
|
||||
case *tg.InputStickerSetTonGifts:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyTonGifts}, true
|
||||
case *tg.InputStickerSetDice:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "dice:" + in.Emoticon}, true
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ func TestStickerSetRefFromSystemInputs(t *testing.T) {
|
|||
{"emoji channel default statuses", &tg.InputStickerSetEmojiChannelDefaultStatuses{}, domain.StickerSetSystemKeyEmojiDefaultStatuses},
|
||||
{"emoji default topic icons", &tg.InputStickerSetEmojiDefaultTopicIcons{}, domain.StickerSetSystemKeyEmojiDefaultTopicIcons},
|
||||
{"premium gifts", &tg.InputStickerSetPremiumGifts{}, domain.StickerSetSystemKeyPremiumGifts},
|
||||
{"ton gifts", &tg.InputStickerSetTonGifts{}, domain.StickerSetSystemKeyTonGifts},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue