fix: sync Android default theme installs

This commit is contained in:
iamxvbaba 2026-07-30 20:13:22 +08:00
parent 80c36a8ab4
commit 0d917ef87e
8 changed files with 263 additions and 7 deletions

View file

@ -220,6 +220,12 @@ func (r *Router) onAccountSaveTheme(ctx context.Context, req *tg.AccountSaveThem
if userID == 0 {
return false, authKeyUnregisteredErr()
}
if _, ok := tdesktop.LookupDefaultTheme(req.Theme); ok {
// Default catalog themes are always present in account.getThemes. Saving
// or unsaving one is therefore an idempotent signal and must not create a
// synthetic custom-theme row or user install.
return true, nil
}
if r.deps.Themes == nil {
return false, notImplementedErr()
}
@ -252,14 +258,20 @@ func (r *Router) onAccountInstallTheme(ctx context.Context, req *tg.AccountInsta
if userID == 0 {
return false, authKeyUnregisteredErr()
}
if r.deps.Themes == nil {
return false, notImplementedErr()
}
dark := req.GetDark()
theme, ok := req.GetTheme()
if !ok {
return true, nil // 无 theme 引用:基础主题 no-op 安装
}
if _, ok := tdesktop.LookupDefaultTheme(theme); ok {
// The immutable defaults were issued by account.getThemes but do not
// live in the custom theme store. Applying one is a successful signal;
// the Android client owns the active day/night choice locally.
return true, nil
}
if r.deps.Themes == nil {
return false, notImplementedErr()
}
ref, ok := themeRefFromInput(theme)
if !ok {
return false, themeInvalidErr()
@ -280,6 +292,9 @@ func (r *Router) onAccountGetTheme(ctx context.Context, req *tg.AccountGetThemeR
if err != nil {
return nil, internalErr()
}
if t, ok := tdesktop.LookupDefaultTheme(req.Theme); ok {
return projectThemeForClient(ctx, &t), nil
}
if r.deps.Themes == nil {
return nil, notImplementedErr()
}

View file

@ -10,6 +10,7 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
"telesrv/internal/compat/tdesktop"
"telesrv/internal/domain"
)
@ -119,6 +120,26 @@ func TestLegacyThemeWireDispatch(t *testing.T) {
t.Fatalf("installTheme legacy wire id = %#x err=%v, want boolTrue", id, err)
}
// 同一 legacy overlay 必须接受 account.getThemes 下发的静态默认主题,
// 且不能要求该引用存在于自定义主题 store。
defaultTheme := tdesktop.DefaultThemeList()[0]
var defaultIB bin.Buffer
defaultIB.PutID(legacyInstallThemeID)
defaultIB.PutInt32((1 << 0) | (1 << 1))
defaultIB.PutString("android")
(&tg.InputTheme{ID: defaultTheme.ID, AccessHash: defaultTheme.AccessHash}).Encode(&defaultIB)
enc, err = r.Dispatch(ctx, authKeyID, sessionID, &defaultIB)
if err != nil {
t.Fatalf("installTheme legacy default dispatch: %v", err)
}
boolWire.Reset()
if err := enc.Encode(&boolWire); err != nil {
t.Fatalf("encode installTheme legacy default result: %v", err)
}
if id, err := boolWire.ID(); err != nil || id != tg.BoolTrueTypeID {
t.Fatalf("installTheme legacy default wire id = %#x err=%v, want boolTrue", id, err)
}
// 已声明 legacy 方法仍必须由静态 decoder 精确消费完整结构。
var malformed bin.Buffer
malformed.PutID(legacyCreateThemeID)

View file

@ -126,6 +126,95 @@ func TestAccountCreateThemeFullFlow(t *testing.T) {
}
}
func TestAccountDefaultThemeReferencesResolveWithoutCustomPersistence(t *testing.T) {
const userID = 1000012
ctx := WithClientInfo(
WithUserID(context.Background(), userID),
ClientInfo{Type: ClientTypeAndroid, AppVersion: "12.9.0 (69669)"},
)
r := newThemeRouter(t, &fakeFiles{})
defaults := tdesktop.DefaultThemeList()
if len(defaults) == 0 {
t.Fatal("default theme catalog is empty")
}
for i, theme := range defaults {
t.Run(theme.Slug, func(t *testing.T) {
input := &tg.InputTheme{ID: theme.ID, AccessHash: theme.AccessHash}
install := &tg.AccountInstallThemeRequest{}
install.SetDark(i%2 == 0)
install.SetTheme(input)
install.SetFormat("android")
if ok, err := r.onAccountInstallTheme(ctx, install); err != nil || !ok {
t.Fatalf("install default theme %d = %v/%v, want true/nil", theme.ID, ok, err)
}
for _, unsave := range []bool{false, true} {
if ok, err := r.onAccountSaveTheme(ctx, &tg.AccountSaveThemeRequest{
Theme: input,
Unsave: unsave,
}); err != nil || !ok {
t.Fatalf("save default theme %d unsave=%v = %v/%v, want true/nil", theme.ID, unsave, ok, err)
}
}
got, err := r.onAccountGetTheme(ctx, &tg.AccountGetThemeRequest{
Format: "android",
Theme: &tg.InputThemeSlug{Slug: theme.Slug},
})
if err != nil || got.ID != theme.ID || got.AccessHash != theme.AccessHash {
t.Fatalf("get default theme %d = %#v/%v", theme.ID, got, err)
}
if ok, err := r.onAccountSaveTheme(ctx, &tg.AccountSaveThemeRequest{
Theme: &tg.InputThemeSlug{Slug: theme.Slug},
}); err != nil || !ok {
t.Fatalf("save default theme slug %q = %v/%v, want true/nil", theme.Slug, ok, err)
}
})
}
// Reproduce issue #29 through the canonical Layer 228 wire constructor,
// not only by invoking the typed handler directly.
canonical := &tg.AccountInstallThemeRequest{}
canonical.SetDark(true)
canonical.SetTheme(&tg.InputTheme{ID: defaults[0].ID, AccessHash: defaults[0].AccessHash})
canonical.SetFormat("android")
var body bin.Buffer
if err := canonical.Encode(&body); err != nil {
t.Fatalf("encode canonical installTheme: %v", err)
}
var authKeyID [8]byte
authKeyID[0] = 2
encoded, err := r.Dispatch(ctx, authKeyID, 1001, &body)
if err != nil {
t.Fatalf("canonical installTheme dispatch: %v", err)
}
var result bin.Buffer
if err := encoded.Encode(&result); err != nil {
t.Fatalf("encode canonical installTheme result: %v", err)
}
if id, err := result.ID(); err != nil || id != tg.BoolTrueTypeID {
t.Fatalf("canonical installTheme result id = %#x err=%v, want boolTrue", id, err)
}
installed, err := r.deps.Themes.ListInstalled(ctx, userID)
if err != nil {
t.Fatalf("list custom installs after default signals: %v", err)
}
if len(installed) != 0 {
t.Fatalf("default signals created %d custom installs, want 0", len(installed))
}
forged := defaults[0]
badInstall := &tg.AccountInstallThemeRequest{}
badInstall.SetTheme(&tg.InputTheme{ID: forged.ID, AccessHash: forged.AccessHash + 1})
badInstall.SetFormat("android")
if _, err := r.onAccountInstallTheme(ctx, badInstall); !tgerr.Is(err, "THEME_INVALID") {
t.Fatalf("install forged default reference err = %v, want THEME_INVALID", err)
}
}
// TestAccountGetThemesIncludesUserThemes 验证 getThemes 跨设备同步:返回内置默认主题
// (is_default=true,emoji 预览条用)+ 当前用户创建的自定义主题(is_default=false,creator=true);
// hash 稳定→NotModified,集合变化→重取。