From 94f3843d2353d65052167b4ec24b7c82dad779de Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:22:31 +0800 Subject: [PATCH] fix(appearance): sync close Android theme wallpaper fallback Public projection of telesrv 2c8b7cb4c20692d00017bd84f68dd11d7915ec33 with neutral appearance seed naming. Covers gramsrv#29. --- internal/compat/tdesktop/appearance_seed.go | 47 ++++++++++++++ internal/compat/tdesktop/startup_stubs.go | 17 ++++-- .../compat/tdesktop/startup_stubs_test.go | 61 +++++++++++++++++++ internal/rpc/account.go | 16 +++++ internal/rpc/passive_compat_test.go | 42 ++++++++++--- 5 files changed, 170 insertions(+), 13 deletions(-) diff --git a/internal/compat/tdesktop/appearance_seed.go b/internal/compat/tdesktop/appearance_seed.go index 34f8eabf..dd119857 100644 --- a/internal/compat/tdesktop/appearance_seed.go +++ b/internal/compat/tdesktop/appearance_seed.go @@ -50,9 +50,56 @@ func LookupWallPaper(input tg.InputWallPaperClass) (tg.WallPaperClass, bool) { } } } + // DrKLO normally installs a default theme with the nested wallpaper slug + // stored on ThemeAccent. During accent restoration it can instead fall back + // to ThemeInfo.slug, which is the slug of the exact Theme advertised by + // account.getThemes. Accept that server-issued alias only when every setting + // of the matched theme points at one unambiguous file wallpaper. + if in, ok := input.(*tg.InputWallPaperSlug); ok { + if wallpaper, ok := lookupChatThemeWallpaperAlias(catalog.ChatThemes, in.Slug); ok { + return DefaultWallPaper(wallpaper), true + } + } return nil, false } +func lookupChatThemeWallpaperAlias(themes []appearance.ChatTheme, slug string) (appearance.Wallpaper, bool) { + if slug == "" { + return appearance.Wallpaper{}, false + } + var resolved appearance.Wallpaper + found := false + for _, theme := range themes { + if theme.Slug != slug || len(theme.Settings) == 0 { + continue + } + var themeWallpaper appearance.Wallpaper + for i, settings := range theme.Settings { + wallpaper := settings.Wallpaper + if wallpaper.Slug == "" || wallpaper.ID == 0 { + return appearance.Wallpaper{}, false + } + if i == 0 { + themeWallpaper = wallpaper + continue + } + if !sameWallpaperIdentity(themeWallpaper, wallpaper) { + return appearance.Wallpaper{}, false + } + } + if found && !sameWallpaperIdentity(resolved, themeWallpaper) { + return appearance.Wallpaper{}, false + } + resolved = themeWallpaper + found = true + } + return resolved, found +} + +func sameWallpaperIdentity(a, b appearance.Wallpaper) bool { + return a.ID == b.ID && a.AccessHash == b.AccessHash && a.Slug == b.Slug +} + // LookupWallPapers resolves multiple wallpapers from the Default seed catalog. func LookupWallPapers(inputs []tg.InputWallPaperClass) ([]tg.WallPaperClass, bool) { out := make([]tg.WallPaperClass, 0, len(inputs)) diff --git a/internal/compat/tdesktop/startup_stubs.go b/internal/compat/tdesktop/startup_stubs.go index 12c0a873..3cf8d147 100644 --- a/internal/compat/tdesktop/startup_stubs.go +++ b/internal/compat/tdesktop/startup_stubs.go @@ -57,7 +57,7 @@ func GlobalPrivacySettings() *tg.GlobalPrivacySettings { return &tg.GlobalPrivacySettings{} } -const chatThemesHash int64 = 2026062501 +const chatThemesHash int64 = 2026080101 const uniqueGiftChatThemesHash int64 = 2026061201 const wallPapersHash int64 = 2026062502 const peerColorsHash = 2026061202 @@ -233,9 +233,14 @@ func WallPapers(hash int64) tg.AccountWallPapersClass { } // chatThemeBaseThemes enumerates the base themes every emoji chat theme ships -// settings for. The client matches a chat theme's settings to whichever base -// theme the user currently runs, and DrKLO's theme picker only surfaces a chat -// theme whose settings cover at least four base themes +// settings for. The order is part of the DrKLO client contract: the basic Chat +// Settings picker selects index 0 for day and index 2 for night, matching its +// built-in Blue, Day, Night, Dark Blue ordering. Arctic is an additional base +// and therefore follows those four stable slots. +// +// The client matches a chat theme's settings to whichever base theme the user +// currently runs, and DrKLO's theme picker only surfaces a chat theme whose +// settings cover at least four base themes // (MediaDataController.generateEmojiPreviewThemes drops anything whose // items.size() < 4, one item per ThemeSettings). dark selects the bubble/accent // palette used for that base theme. @@ -245,9 +250,9 @@ var chatThemeBaseThemes = []struct { }{ {func() tg.BaseThemeClass { return &tg.BaseThemeClassic{} }, false}, {func() tg.BaseThemeClass { return &tg.BaseThemeDay{} }, false}, - {func() tg.BaseThemeClass { return &tg.BaseThemeArctic{} }, false}, - {func() tg.BaseThemeClass { return &tg.BaseThemeTinted{} }, true}, {func() tg.BaseThemeClass { return &tg.BaseThemeNight{} }, true}, + {func() tg.BaseThemeClass { return &tg.BaseThemeTinted{} }, true}, + {func() tg.BaseThemeClass { return &tg.BaseThemeArctic{} }, false}, } func AutoDownloadSettings() *tg.AccountAutoDownloadSettings { diff --git a/internal/compat/tdesktop/startup_stubs_test.go b/internal/compat/tdesktop/startup_stubs_test.go index d0330632..d72534d7 100644 --- a/internal/compat/tdesktop/startup_stubs_test.go +++ b/internal/compat/tdesktop/startup_stubs_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/iamxvbaba/td/tg" + + "telesrv/internal/seed/appearance" ) func TestNotifySettingsDefaultIsAudible(t *testing.T) { @@ -216,6 +218,39 @@ func TestDefaultThemesAreDefaultFlaggedForPicker(t *testing.T) { } } +func TestDefaultThemeSettingsFollowDrKLOPickerIndices(t *testing.T) { + for _, theme := range DefaultThemeList() { + settings, ok := theme.GetSettings() + if !ok || len(settings) < 5 { + t.Fatalf("theme %d settings len=%d ok=%v, want five stable base slots", theme.ID, len(settings), ok) + } + if _, ok := settings[0].BaseTheme.(*tg.BaseThemeClassic); !ok { + t.Fatalf("theme %d settings[0] base = %T, want classic", theme.ID, settings[0].BaseTheme) + } + if _, ok := settings[1].BaseTheme.(*tg.BaseThemeDay); !ok { + t.Fatalf("theme %d settings[1] base = %T, want day", theme.ID, settings[1].BaseTheme) + } + if _, ok := settings[2].BaseTheme.(*tg.BaseThemeNight); !ok { + t.Fatalf("theme %d settings[2] base = %T, want night", theme.ID, settings[2].BaseTheme) + } + if _, ok := settings[3].BaseTheme.(*tg.BaseThemeTinted); !ok { + t.Fatalf("theme %d settings[3] base = %T, want tinted", theme.ID, settings[3].BaseTheme) + } + if _, ok := settings[4].BaseTheme.(*tg.BaseThemeArctic); !ok { + t.Fatalf("theme %d settings[4] base = %T, want arctic", theme.ID, settings[4].BaseTheme) + } + wallpaperClass, ok := settings[2].GetWallpaper() + wallpaper, fileWallpaper := wallpaperClass.(*tg.WallPaper) + if !ok || !fileWallpaper || !wallpaper.GetDark() { + t.Fatalf("theme %d night slot wallpaper = %#v ok=%v, want dark file wallpaper", theme.ID, wallpaperClass, ok) + } + } + + if _, ok := ChatThemes(2026062501).(*tg.AccountThemesNotModified); ok { + t.Fatal("pre-fix chat theme hash was not invalidated after settings reorder") + } +} + func TestLookupDefaultThemeValidatesIdentity(t *testing.T) { themes := DefaultThemeList() if len(themes) == 0 { @@ -366,12 +401,38 @@ func TestDefaultThemeWallpaperReferencesResolve(t *testing.T) { t.Fatalf("theme %d settings[%d] wallpaper slug lookup = %#v", theme.ID, i, bySlug) } } + byThemeSlug, ok := LookupWallPaper(&tg.InputWallPaperSlug{Slug: theme.Slug}) + if !ok { + t.Fatalf("theme %d fallback wallpaper alias %q was not resolvable", theme.ID, theme.Slug) + } + aliasWallpaper, ok := byThemeSlug.(*tg.WallPaper) + firstWallpaperClass, firstOK := settings[0].GetWallpaper() + firstWallpaper, firstFile := firstWallpaperClass.(*tg.WallPaper) + if !ok || !firstOK || !firstFile || aliasWallpaper.ID != firstWallpaper.ID || aliasWallpaper.Slug != firstWallpaper.Slug { + t.Fatalf("theme %d fallback alias = %#v, want settings[0] wallpaper %#v", theme.ID, byThemeSlug, firstWallpaperClass) + } } if checked == 0 { t.Fatal("default themes exposed no file wallpapers") } } +func TestChatThemeWallpaperAliasRejectsAmbiguousSettings(t *testing.T) { + themes := []appearance.ChatTheme{{ + Slug: "theme-alias", + Settings: []appearance.ThemeSettings{ + {Wallpaper: appearance.Wallpaper{ID: 1, AccessHash: 11, Slug: "wallpaper-one"}}, + {Wallpaper: appearance.Wallpaper{ID: 2, AccessHash: 22, Slug: "wallpaper-two"}}, + }, + }} + if _, ok := lookupChatThemeWallpaperAlias(themes, "theme-alias"); ok { + t.Fatal("ambiguous theme wallpaper alias was accepted") + } + if _, ok := lookupChatThemeWallpaperAlias(themes, "unknown"); ok { + t.Fatal("unknown theme wallpaper alias was accepted") + } +} + func TestStarsRevenueStatsIsZeroBalanceCompatStub(t *testing.T) { got := StarsRevenueStats(false) if got == nil { diff --git a/internal/rpc/account.go b/internal/rpc/account.go index 27c8d9c8..8150bed9 100644 --- a/internal/rpc/account.go +++ b/internal/rpc/account.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/iamxvbaba/td/tg" + "go.uber.org/zap" "github.com/iamxvbaba/td/tlprofile" "telesrv/internal/branding" @@ -289,6 +290,7 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) { return false, tgerr400("WALLPAPER_INVALID") } if _, ok := tdesktop.LookupWallPaper(req.Wallpaper); !ok { + r.log.Info("wallpaper reference rejected", wallpaperReferenceLogFields("account.installWallPaper", req.Wallpaper)...) return false, tgerr400("WALLPAPER_INVALID") } return true, nil @@ -463,6 +465,20 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) { } +func wallpaperReferenceLogFields(method string, input tg.InputWallPaperClass) []zap.Field { + fields := []zap.Field{zap.String("method", method)} + switch wallpaper := input.(type) { + case *tg.InputWallPaperSlug: + return append(fields, zap.String("input_type", "inputWallPaperSlug"), zap.String("slug", wallpaper.Slug)) + case *tg.InputWallPaper: + return append(fields, zap.String("input_type", "inputWallPaper"), zap.Int64("wallpaper_id", wallpaper.ID)) + case *tg.InputWallPaperNoFile: + return append(fields, zap.String("input_type", "inputWallPaperNoFile"), zap.Int64("wallpaper_id", wallpaper.ID)) + default: + return append(fields, zap.String("input_type", "unknown")) + } +} + func (r *Router) onAccountGetPassword(ctx context.Context) (*tg.AccountPassword, error) { if r.deps.Account == nil { return tgPassword(domain.PasswordSettings{SecureRandom: []byte("telesrv-tdesktop-dev-secure-rand")}), nil diff --git a/internal/rpc/passive_compat_test.go b/internal/rpc/passive_compat_test.go index 92879f2d..0d4bd616 100644 --- a/internal/rpc/passive_compat_test.go +++ b/internal/rpc/passive_compat_test.go @@ -9,6 +9,8 @@ import ( "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" "go.uber.org/zap/zaptest" + + "telesrv/internal/compat/tdesktop" ) func TestAccountGetChatThemesReturnsStaticThemes(t *testing.T) { @@ -106,6 +108,21 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) { t.Fatalf("getWallPaper id = %d, want %d", oneWallpaper.ID, first.ID) } + defaultTheme := tdesktop.DefaultThemeList()[0] + themeAliasInput := &tg.InputWallPaperSlug{Slug: defaultTheme.Slug} + var aliasReq bin.Buffer + if err := (&tg.AccountGetWallPaperRequest{Wallpaper: themeAliasInput}).Encode(&aliasReq); err != nil { + t.Fatalf("encode getWallPaper theme alias request: %v", err) + } + aliasGot, err := r.Dispatch(ctx, [8]byte{}, 0, &aliasReq) + if err != nil { + t.Fatalf("dispatch getWallPaper theme alias: %v", err) + } + aliasWallpaper, ok := aliasGot.(*tg.WallPaper) + if !ok || aliasWallpaper.Slug == defaultTheme.Slug { + t.Fatalf("getWallPaper theme alias = %#v, want resolved wallpaper identity", aliasGot) + } + nofileInput := &tg.InputWallPaperNoFile{ID: 930000000000000001} var nofileReq bin.Buffer if err := (&tg.AccountGetWallPaperRequest{Wallpaper: nofileInput}).Encode(&nofileReq); err != nil { @@ -127,6 +144,7 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) { if err := (&tg.AccountGetMultiWallPapersRequest{Wallpapers: []tg.InputWallPaperClass{ input, &tg.InputWallPaperSlug{Slug: first.Slug}, + themeAliasInput, nofileInput, }}).Encode(&multiReq); err != nil { t.Fatalf("encode getMultiWallPapers request: %v", err) @@ -135,16 +153,18 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) { if err != nil { t.Fatalf("dispatch getMultiWallPapers: %v", err) } - if vector, ok := dispatchCanonicalValue(multiGot).([]tg.WallPaperClass); !ok || len(vector) != 3 { - t.Fatalf("getMultiWallPapers = %T %#v, want 3 wallpapers", multiGot, multiGot) + if vector, ok := dispatchCanonicalValue(multiGot).([]tg.WallPaperClass); !ok || len(vector) != 4 { + t.Fatalf("getMultiWallPapers = %T %#v, want 4 wallpapers", multiGot, multiGot) } for name, request := range map[string]bin.Encoder{ - "save": &tg.AccountSaveWallPaperRequest{Wallpaper: input}, - "install": &tg.AccountInstallWallPaperRequest{Wallpaper: input}, - "save_nofile": &tg.AccountSaveWallPaperRequest{Wallpaper: nofileInput}, - "install_nofile": &tg.AccountInstallWallPaperRequest{Wallpaper: nofileInput}, - "reset": &tg.AccountResetWallPapersRequest{}, + "save": &tg.AccountSaveWallPaperRequest{Wallpaper: input}, + "install": &tg.AccountInstallWallPaperRequest{Wallpaper: input}, + "save_theme_alias": &tg.AccountSaveWallPaperRequest{Wallpaper: themeAliasInput}, + "install_theme_alias": &tg.AccountInstallWallPaperRequest{Wallpaper: themeAliasInput}, + "save_nofile": &tg.AccountSaveWallPaperRequest{Wallpaper: nofileInput}, + "install_nofile": &tg.AccountInstallWallPaperRequest{Wallpaper: nofileInput}, + "reset": &tg.AccountResetWallPapersRequest{}, } { var encoded bin.Buffer if err := request.Encode(&encoded); err != nil { @@ -166,6 +186,14 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) { if _, err := r.Dispatch(ctx, [8]byte{}, 0, &badReq); err == nil || !strings.Contains(err.Error(), "WALLPAPER_INVALID") { t.Fatalf("bad getWallPaper err = %v, want WALLPAPER_INVALID", err) } + + var unknownAliasReq bin.Buffer + if err := (&tg.AccountInstallWallPaperRequest{Wallpaper: &tg.InputWallPaperSlug{Slug: "unknown-theme-alias"}}).Encode(&unknownAliasReq); err != nil { + t.Fatalf("encode unknown theme alias install request: %v", err) + } + if _, err := r.Dispatch(ctx, [8]byte{}, 0, &unknownAliasReq); err == nil || !strings.Contains(err.Error(), "WALLPAPER_INVALID") { + t.Fatalf("unknown theme alias install err = %v, want WALLPAPER_INVALID", err) + } } func TestPaymentsGetStarGiftCollectionsNoServiceFallbackAndValidatesPeer(t *testing.T) {