Merge remote-tracking branch 'upstream/main' into merge-gramsrv-9106877
This commit is contained in:
commit
ac6a50c5ff
697 changed files with 100880 additions and 8052 deletions
22
internal/compat/android/report.go
Normal file
22
internal/compat/android/report.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package android
|
||||
|
||||
const clientType = "android"
|
||||
|
||||
// OfferInitialMessageReportOptions preserves DrKLO's official channel-report
|
||||
// flow. DrKLO starts that flow with an empty message-id vector and only opens
|
||||
// its message selector after a chosen option receives MESSAGE_ID_REQUIRED.
|
||||
//
|
||||
// This exception is deliberately limited to the non-mutating first request:
|
||||
// a selected option, a comment, or any non-Android caller must still pass the
|
||||
// normal messages.report message-id validation.
|
||||
func OfferInitialMessageReportOptions(
|
||||
client string,
|
||||
messageIDCount int,
|
||||
option []byte,
|
||||
comment string,
|
||||
) bool {
|
||||
return client == clientType &&
|
||||
messageIDCount == 0 &&
|
||||
len(option) == 0 &&
|
||||
comment == ""
|
||||
}
|
||||
32
internal/compat/android/report_test.go
Normal file
32
internal/compat/android/report_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package android
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOfferInitialMessageReportOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
client string
|
||||
messageIDCount int
|
||||
option []byte
|
||||
comment string
|
||||
want bool
|
||||
}{
|
||||
{name: "android initial discovery", client: "android", want: true},
|
||||
{name: "desktop keeps protocol error", client: "tdesktop"},
|
||||
{name: "selected option requires messages", client: "android", option: []byte("spam")},
|
||||
{name: "comment requires messages", client: "android", comment: "details"},
|
||||
{name: "message ids use normal flow", client: "android", messageIDCount: 1},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := OfferInitialMessageReportOptions(
|
||||
test.client,
|
||||
test.messageIDCount,
|
||||
test.option,
|
||||
test.comment,
|
||||
); got != test.want {
|
||||
t.Fatalf("OfferInitialMessageReportOptions() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
internal/compat/android/stars.go
Normal file
24
internal/compat/android/stars.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package android
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// DirectInvoiceCurrencies are the Google Play billing currencies for which
|
||||
// DrKLO's official client must use Telegram's invoice flow. telesrv does not
|
||||
// publish or verify Google Play products, so this prevents Stars options with
|
||||
// no store_product from entering the Play Billing branch once it is ready.
|
||||
var directInvoiceCurrencies = []string{
|
||||
"AED", "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "COP", "CZK", "DKK",
|
||||
"EGP", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW",
|
||||
"KZT", "MXN", "MYR", "NGN", "NOK", "NZD", "PEN", "PHP", "PKR", "PLN",
|
||||
"QAR", "RON", "RUB", "SAR", "SEK", "SGD", "THB", "TRY", "TWD", "UAH",
|
||||
"USD", "VND", "ZAR",
|
||||
}
|
||||
|
||||
func DirectInvoiceCurrencies() []string {
|
||||
return append([]string(nil), directInvoiceCurrencies...)
|
||||
}
|
||||
|
||||
func DirectInvoiceCurrenciesJSON() string {
|
||||
body, _ := json.Marshal(directInvoiceCurrencies)
|
||||
return string(body)
|
||||
}
|
||||
30
internal/compat/android/stars_test.go
Normal file
30
internal/compat/android/stars_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package android
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDirectInvoiceCurrenciesAreStableAndContainUSD(t *testing.T) {
|
||||
values := DirectInvoiceCurrencies()
|
||||
if len(values) == 0 {
|
||||
t.Fatal("direct invoice currency list is empty")
|
||||
}
|
||||
foundUSD := false
|
||||
for _, value := range values {
|
||||
if value == "USD" {
|
||||
foundUSD = true
|
||||
}
|
||||
}
|
||||
if !foundUSD {
|
||||
t.Fatalf("direct invoice currencies = %v, want USD", values)
|
||||
}
|
||||
values[0] = "MUTATED"
|
||||
if DirectInvoiceCurrencies()[0] == "MUTATED" {
|
||||
t.Fatal("DirectInvoiceCurrencies returned mutable package storage")
|
||||
}
|
||||
var decoded []string
|
||||
if err := json.Unmarshal([]byte(DirectInvoiceCurrenciesJSON()), &decoded); err != nil || len(decoded) != len(values) {
|
||||
t.Fatalf("currency JSON = %q decoded=%v err=%v", DirectInvoiceCurrenciesJSON(), decoded, err)
|
||||
}
|
||||
}
|
||||
46
internal/compat/ios/theme_colors.go
Normal file
46
internal/compat/ios/theme_colors.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package ios
|
||||
|
||||
import "github.com/iamxvbaba/td/tg"
|
||||
|
||||
// ProjectThemes converts theme accent colors to the ARGB representation used by
|
||||
// Telegram-iOS. Official chat-theme seed colors are RGB24 values, while iOS
|
||||
// passes accent_color and outbox_accent_color to UIColor(argb:); a zero high
|
||||
// byte would therefore make every accent-tinted control fully transparent.
|
||||
//
|
||||
// The projection is copy-on-write so the Android/TDesktop catalog remains
|
||||
// byte-for-byte unchanged. message_colors and wallpaper colors intentionally
|
||||
// stay RGB24, as required by the TL schema and all audited clients.
|
||||
func ProjectThemes(themes []tg.Theme) []tg.Theme {
|
||||
if len(themes) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]tg.Theme, len(themes))
|
||||
for i := range themes {
|
||||
out[i] = themes[i]
|
||||
settings, ok := themes[i].GetSettings()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
projected := make([]tg.ThemeSettings, len(settings))
|
||||
for j := range settings {
|
||||
projected[j] = settings[j]
|
||||
projected[j].AccentColor = opaqueARGB(settings[j].AccentColor)
|
||||
if color, ok := settings[j].GetOutboxAccentColor(); ok {
|
||||
projected[j].SetOutboxAccentColor(opaqueARGB(color))
|
||||
}
|
||||
if colors, ok := settings[j].GetMessageColors(); ok {
|
||||
projected[j].SetMessageColors(append([]int(nil), colors...))
|
||||
}
|
||||
}
|
||||
out[i].SetSettings(projected)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func opaqueARGB(color int) int {
|
||||
value := uint32(int32(color))
|
||||
if value>>24 == 0 {
|
||||
value |= 0xff000000
|
||||
}
|
||||
return int(int32(value))
|
||||
}
|
||||
75
internal/compat/ios/theme_colors_test.go
Normal file
75
internal/compat/ios/theme_colors_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package ios
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
)
|
||||
|
||||
func TestProjectThemesMakesOnlyIOSAccentColorsOpaque(t *testing.T) {
|
||||
settings := tg.ThemeSettings{
|
||||
BaseTheme: &tg.BaseThemeClassic{},
|
||||
AccentColor: 0x29b071,
|
||||
}
|
||||
settings.SetOutboxAccentColor(0x4cb064)
|
||||
settings.SetMessageColors([]int{0xd4f1ff, 0xb9e4ff})
|
||||
wallpaper := &tg.WallPaperNoFile{}
|
||||
settings.SetWallpaper(wallpaper)
|
||||
theme := tg.Theme{ID: 1, Slug: "green", Title: "Green"}
|
||||
theme.SetSettings([]tg.ThemeSettings{settings})
|
||||
|
||||
projected := ProjectThemes([]tg.Theme{theme})
|
||||
if len(projected) != 1 {
|
||||
t.Fatalf("ProjectThemes length = %d, want 1", len(projected))
|
||||
}
|
||||
got, ok := projected[0].GetSettings()
|
||||
if !ok || len(got) != 1 {
|
||||
t.Fatalf("projected settings = %#v ok=%v, want one setting", got, ok)
|
||||
}
|
||||
if color := uint32(int32(got[0].AccentColor)); color != 0xff29b071 {
|
||||
t.Fatalf("accent color = %#08x, want opaque ARGB 0xff29b071", color)
|
||||
}
|
||||
if color, ok := got[0].GetOutboxAccentColor(); !ok || uint32(int32(color)) != 0xff4cb064 {
|
||||
t.Fatalf("outbox accent = %#08x ok=%v, want opaque ARGB 0xff4cb064", uint32(int32(color)), ok)
|
||||
}
|
||||
colors, ok := got[0].GetMessageColors()
|
||||
if !ok || len(colors) != 2 || colors[0] != 0xd4f1ff || colors[1] != 0xb9e4ff {
|
||||
t.Fatalf("message colors = %#v ok=%v, want unchanged RGB24 values", colors, ok)
|
||||
}
|
||||
if got[0].Wallpaper != wallpaper {
|
||||
t.Fatal("wallpaper changed during accent-only projection")
|
||||
}
|
||||
|
||||
sourceSettings, _ := theme.GetSettings()
|
||||
if sourceSettings[0].AccentColor != 0x29b071 {
|
||||
t.Fatalf("source accent mutated to %#x", sourceSettings[0].AccentColor)
|
||||
}
|
||||
sourceOutbox, _ := sourceSettings[0].GetOutboxAccentColor()
|
||||
if sourceOutbox != 0x4cb064 {
|
||||
t.Fatalf("source outbox accent mutated to %#x", sourceOutbox)
|
||||
}
|
||||
colors[0] = 1
|
||||
sourceColors, _ := sourceSettings[0].GetMessageColors()
|
||||
if sourceColors[0] != 0xd4f1ff {
|
||||
t.Fatalf("source message colors mutated through projection: %#v", sourceColors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectThemesPreservesExistingAlphaAndAbsentOutbox(t *testing.T) {
|
||||
argb := uint32(0x80445566)
|
||||
settings := tg.ThemeSettings{
|
||||
BaseTheme: &tg.BaseThemeTinted{},
|
||||
AccentColor: int(int32(argb)),
|
||||
}
|
||||
theme := tg.Theme{ID: 2}
|
||||
theme.SetSettings([]tg.ThemeSettings{settings})
|
||||
|
||||
projected := ProjectThemes([]tg.Theme{theme})
|
||||
got, _ := projected[0].GetSettings()
|
||||
if color := uint32(int32(got[0].AccentColor)); color != argb {
|
||||
t.Fatalf("existing ARGB color = %#08x, want %#08x", color, argb)
|
||||
}
|
||||
if _, ok := got[0].GetOutboxAccentColor(); ok {
|
||||
t.Fatal("absent outbox accent became present")
|
||||
}
|
||||
}
|
||||
|
|
@ -38,9 +38,68 @@ func LookupWallPaper(input tg.InputWallPaperClass) (tg.WallPaperClass, bool) {
|
|||
return DefaultWallPaper(wallpaper), true
|
||||
}
|
||||
}
|
||||
// account.getThemes/account.getChatThemes also advertise wallpapers nested
|
||||
// in ThemeSettings. The default getWallPapers export is a filtered list and
|
||||
// does not contain every nested entry, so these identities must remain part
|
||||
// of the same lookup boundary or Android can render a theme that it cannot
|
||||
// subsequently install.
|
||||
for _, theme := range catalog.ChatThemes {
|
||||
for _, settings := range theme.Settings {
|
||||
if inputWallPaperMatches(input, settings.Wallpaper) {
|
||||
return DefaultWallPaper(settings.Wallpaper), true
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package tdesktop
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
|
@ -13,18 +14,27 @@ import (
|
|||
// 字段值取 Telegram 常见默认;TDesktop 联调阶段按客户端实际需要微调
|
||||
// (记录于 docs/compatibility-matrix.md)。
|
||||
func BuildConfig(dc int, ip string, port int, now time.Time, publicBaseURL string) *tg.Config {
|
||||
// TELESRV_ADVERTISE_IP is validated during config loading. Parse again here
|
||||
// only to derive the wire ipv6 flag and to render IPv4-mapped addresses in
|
||||
// their canonical form. Keeping the advertised route in help.getConfig is a
|
||||
// protocol invariant: clients replace or persist this list for reconnects.
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err == nil {
|
||||
addr = addr.Unmap()
|
||||
ip = addr.String()
|
||||
}
|
||||
meURLPrefix := links.NormalizeBaseURL(publicBaseURL) + "/"
|
||||
config := &tg.Config{
|
||||
Date: int(now.Unix()),
|
||||
Expires: int(now.Add(time.Hour).Unix()),
|
||||
TestMode: false,
|
||||
ThisDC: dc,
|
||||
// 不下发 DCOptions:客户端(TDesktop patch / drklo fork)已写死 static DC
|
||||
// 地址,空列表会让客户端保留它——drklo ConnectionsManager.cpp 的 processConfig
|
||||
// 在 dc_options 为空时整段跳过 replaceAddresses/saveConfig,既不覆盖也不持久化。
|
||||
// 服务端因此无需配置对外可达 IP,换网络/部署只改客户端写死地址即可。ip/port
|
||||
// 参数暂留,供未来需要显式 advertise 时改回。
|
||||
DCOptions: nil,
|
||||
DCOptions: []tg.DCOption{{
|
||||
Ipv6: addr.Is6(),
|
||||
ID: dc,
|
||||
IPAddress: ip,
|
||||
Port: port,
|
||||
}},
|
||||
ChatSizeMax: 200,
|
||||
MegagroupSizeMax: 200000,
|
||||
ForwardedCountMax: 100,
|
||||
|
|
@ -58,13 +68,3 @@ func BuildConfig(dc int, ip string, port int, now time.Time, publicBaseURL strin
|
|||
config.SetReactionsDefault(&tg.ReactionEmoji{Emoticon: DefaultReactionEmoticon})
|
||||
return config
|
||||
}
|
||||
|
||||
// NearestDC 构造 help.getNearestDc 返回值。
|
||||
func NearestDC(dc int) *tg.NearestDC {
|
||||
return &tg.NearestDC{
|
||||
// 默认国家=中国:DrKLO/TDesktop 登录页据此预选区号(+86)。
|
||||
Country: "CN",
|
||||
ThisDC: dc,
|
||||
NearestDC: dc,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,3 +18,31 @@ func TestBuildConfigIncludesDefaultReaction(t *testing.T) {
|
|||
t.Fatalf("reactions_default = %#v, want %q emoji", reaction, DefaultReactionEmoticon)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigAdvertisesCanonicalPrimaryDC(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
want string
|
||||
ipv6 bool
|
||||
}{
|
||||
{name: "ipv4", ip: "192.0.2.10", want: "192.0.2.10"},
|
||||
{name: "ipv6", ip: "2001:0db8::1", want: "2001:db8::1", ipv6: true},
|
||||
{name: "mapped ipv4", ip: "::ffff:192.0.2.10", want: "192.0.2.10"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := BuildConfig(2, tt.ip, 2398, time.Unix(1, 0), "https://telesrv.net")
|
||||
if len(config.DCOptions) != 1 {
|
||||
t.Fatalf("len(DCOptions) = %d, want 1", len(config.DCOptions))
|
||||
}
|
||||
option := config.DCOptions[0]
|
||||
if option.ID != 2 || option.IPAddress != tt.want || option.Port != 2398 || option.Ipv6 != tt.ipv6 {
|
||||
t.Fatalf("DCOptions[0] = %+v, want dc=2 ip=%q port=2398 ipv6=%v", option, tt.want, tt.ipv6)
|
||||
}
|
||||
if option.MediaOnly || option.CDN || option.TCPObfuscatedOnly || option.Static || option.ThisPortOnly {
|
||||
t.Fatalf("DCOptions[0] has unexpected restrictive flags: %+v", option)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ package tdesktop
|
|||
import (
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
compatandroid "telesrv/internal/compat/android"
|
||||
"telesrv/internal/seed/catalog"
|
||||
)
|
||||
|
||||
const (
|
||||
appConfigHash = 17 // app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
appConfigHash = 18 // app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
countriesListHash = 1
|
||||
timezonesListHash = 1
|
||||
)
|
||||
|
|
@ -38,8 +39,15 @@ func readMarkAppConfig(mapboxToken string) *tg.JSONObject {
|
|||
// premiumCanBuy()=!premium_purchase_blocked 耦合,置 true 会同时隐藏送礼入口
|
||||
// (详见 app/help/service.go 主配置注释)。这里是无 HelpService 时的最小回退。
|
||||
{Key: "premium_purchase_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
{Key: "stars_purchase_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
// stargifts_blocked=false:DrKLO 缺省 stargiftsBlocked=true 会隐藏 star gift 送礼网格。
|
||||
{Key: "stargifts_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
{Key: "giveaway_gifts_purchase_available", Value: &tg.JSONBool{Value: true}},
|
||||
{Key: "giveaway_boosts_per_premium", Value: &tg.JSONNumber{Value: 4}},
|
||||
{Key: "giveaway_countries_max", Value: &tg.JSONNumber{Value: 10}},
|
||||
{Key: "giveaway_add_peers_max", Value: &tg.JSONNumber{Value: 10}},
|
||||
{Key: "giveaway_period_max", Value: &tg.JSONNumber{Value: 604800}},
|
||||
{Key: "premium_playmarket_direct_currency_list", Value: tgStringArray(compatandroid.DirectInvoiceCurrencies())},
|
||||
{Key: "reactions_user_max_premium", Value: &tg.JSONNumber{Value: 3}},
|
||||
// DrKLO 频道自定义 reaction 编辑页用它作为可选 reaction 数量上限。
|
||||
{Key: "boosts_channel_level_max", Value: &tg.JSONNumber{Value: 100}},
|
||||
|
|
@ -88,6 +96,14 @@ func readMarkAppConfig(mapboxToken string) *tg.JSONObject {
|
|||
return &tg.JSONObject{Value: values}
|
||||
}
|
||||
|
||||
func tgStringArray(values []string) *tg.JSONArray {
|
||||
result := &tg.JSONArray{Value: make([]tg.JSONValueClass, 0, len(values))}
|
||||
for _, value := range values {
|
||||
result.Value = append(result.Value, &tg.JSONString{Value: value})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// fallbackTimezones 是 catalog 未 seed 时的内置最小时区集。
|
||||
var fallbackTimezones = []tg.Timezone{
|
||||
{ID: "Etc/UTC", Name: "UTC", UtcOffset: 0},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -182,6 +182,31 @@ func DefaultThemeList() []tg.Theme {
|
|||
return themes
|
||||
}
|
||||
|
||||
// LookupDefaultTheme resolves an InputTheme against the exact identity emitted
|
||||
// by DefaultThemeList. ID references must carry the matching access hash; slug
|
||||
// references are public and match by their exact non-empty slug.
|
||||
//
|
||||
// These themes are an immutable server catalog rather than rows in the custom
|
||||
// cloud-theme store. RPCs which accept a Theme returned by account.getThemes
|
||||
// use this lookup before consulting that store.
|
||||
func LookupDefaultTheme(input tg.InputThemeClass) (tg.Theme, bool) {
|
||||
for _, theme := range DefaultThemeList() {
|
||||
switch in := input.(type) {
|
||||
case *tg.InputTheme:
|
||||
if in.ID == theme.ID && in.AccessHash == theme.AccessHash {
|
||||
return theme, true
|
||||
}
|
||||
case *tg.InputThemeSlug:
|
||||
if in.Slug != "" && in.Slug == theme.Slug {
|
||||
return theme, true
|
||||
}
|
||||
default:
|
||||
return tg.Theme{}, false
|
||||
}
|
||||
}
|
||||
return tg.Theme{}, false
|
||||
}
|
||||
|
||||
func UniqueGiftChatThemes(hash int64) tg.AccountChatThemesClass {
|
||||
if hash == uniqueGiftChatThemesHash {
|
||||
return &tg.AccountChatThemesNotModified{}
|
||||
|
|
@ -208,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.
|
||||
|
|
@ -220,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 {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/seed/appearance"
|
||||
)
|
||||
|
||||
func TestNotifySettingsDefaultIsAudible(t *testing.T) {
|
||||
|
|
@ -41,6 +43,8 @@ func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
|||
values := make(map[string]float64)
|
||||
strings := make(map[string]string)
|
||||
arrays := make(map[string]*tg.JSONArray)
|
||||
bools := make(map[string]bool)
|
||||
boolSeen := make(map[string]bool)
|
||||
if object, ok := got.Config.(*tg.JSONObject); ok && object != nil {
|
||||
for _, entry := range object.Value {
|
||||
if number, ok := entry.Value.(*tg.JSONNumber); ok {
|
||||
|
|
@ -52,12 +56,20 @@ func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
|||
if array, ok := entry.Value.(*tg.JSONArray); ok {
|
||||
arrays[entry.Key] = array
|
||||
}
|
||||
if boolean, ok := entry.Value.(*tg.JSONBool); ok {
|
||||
bools[entry.Key] = boolean.Value
|
||||
boolSeen[entry.Key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
want := map[string]float64{
|
||||
"stories_stealth_future_period": 1500,
|
||||
"stories_stealth_past_period": 300,
|
||||
"stories_stealth_cooldown_period": 10800,
|
||||
"giveaway_boosts_per_premium": 4,
|
||||
"giveaway_countries_max": 10,
|
||||
"giveaway_add_peers_max": 10,
|
||||
"giveaway_period_max": 604800,
|
||||
}
|
||||
for key, expected := range want {
|
||||
if values[key] != expected {
|
||||
|
|
@ -67,6 +79,10 @@ func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
|||
if strings["rich_message_posting"] != "enabled" {
|
||||
t.Fatalf("AppConfig[rich_message_posting] = %q, want enabled", strings["rich_message_posting"])
|
||||
}
|
||||
if !boolSeen["stars_purchase_blocked"] || bools["stars_purchase_blocked"] ||
|
||||
!boolSeen["giveaway_gifts_purchase_available"] || !bools["giveaway_gifts_purchase_available"] {
|
||||
t.Fatalf("AppConfig purchase flags = stars_blocked:%v giveaway_available:%v", bools["stars_purchase_blocked"], bools["giveaway_gifts_purchase_available"])
|
||||
}
|
||||
fragmentPrefixes := arrays["fragment_prefixes"]
|
||||
if fragmentPrefixes == nil || len(fragmentPrefixes.Value) != 1 {
|
||||
t.Fatalf("AppConfig[fragment_prefixes] = %#v, want one-element array", fragmentPrefixes)
|
||||
|
|
@ -75,6 +91,10 @@ func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
|||
if !ok || prefix.Value != "888" {
|
||||
t.Fatalf("AppConfig[fragment_prefixes][0] = %#v, want \"888\"", fragmentPrefixes.Value[0])
|
||||
}
|
||||
directCurrencies := arrays["premium_playmarket_direct_currency_list"]
|
||||
if directCurrencies == nil || !tgJSONArrayContainsString(directCurrencies, "USD") {
|
||||
t.Fatalf("AppConfig[premium_playmarket_direct_currency_list] = %#v, want USD", directCurrencies)
|
||||
}
|
||||
if _, ok := AppConfig(got.Hash).(*tg.HelpAppConfigNotModified); !ok {
|
||||
t.Fatalf("AppConfig(hash) = %#v, want notModified", AppConfig(got.Hash))
|
||||
}
|
||||
|
|
@ -83,6 +103,15 @@ func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func tgJSONArrayContainsString(array *tg.JSONArray, want string) bool {
|
||||
for _, value := range array.Value {
|
||||
if text, ok := value.(*tg.JSONString); ok && text.Value == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestFallbackAppConfigOmitsMapboxToken(t *testing.T) {
|
||||
got, ok := AppConfig(0).(*tg.HelpAppConfig)
|
||||
if !ok {
|
||||
|
|
@ -216,6 +245,68 @@ 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 {
|
||||
t.Fatal("DefaultThemeList is empty")
|
||||
}
|
||||
for _, theme := range themes {
|
||||
byID, ok := LookupDefaultTheme(&tg.InputTheme{
|
||||
ID: theme.ID,
|
||||
AccessHash: theme.AccessHash,
|
||||
})
|
||||
if !ok || byID.ID != theme.ID {
|
||||
t.Fatalf("lookup id %d = id %d ok=%v, want exact theme", theme.ID, byID.ID, ok)
|
||||
}
|
||||
bySlug, ok := LookupDefaultTheme(&tg.InputThemeSlug{Slug: theme.Slug})
|
||||
if !ok || bySlug.ID != theme.ID {
|
||||
t.Fatalf("lookup slug %q = id %d ok=%v, want %d", theme.Slug, bySlug.ID, ok, theme.ID)
|
||||
}
|
||||
if _, ok := LookupDefaultTheme(&tg.InputTheme{
|
||||
ID: theme.ID,
|
||||
AccessHash: theme.AccessHash + 1,
|
||||
}); ok {
|
||||
t.Fatalf("lookup id %d accepted forged access hash", theme.ID)
|
||||
}
|
||||
}
|
||||
if _, ok := LookupDefaultTheme(&tg.InputThemeSlug{}); ok {
|
||||
t.Fatal("lookup accepted empty slug")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueGiftChatThemesIsEmptyHashableStub(t *testing.T) {
|
||||
got, ok := UniqueGiftChatThemes(0).(*tg.AccountChatThemes)
|
||||
if !ok {
|
||||
|
|
@ -299,6 +390,76 @@ func TestLookupWallPaperByIDAndSlug(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDefaultThemeWallpaperReferencesResolve(t *testing.T) {
|
||||
themes := DefaultThemeList()
|
||||
var checked int
|
||||
for _, theme := range themes {
|
||||
settings, ok := theme.GetSettings()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, setting := range settings {
|
||||
wallpaperClass, ok := setting.GetWallpaper()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
wallpaper, ok := wallpaperClass.(*tg.WallPaper)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
checked++
|
||||
byID, ok := LookupWallPaper(&tg.InputWallPaper{
|
||||
ID: wallpaper.ID,
|
||||
AccessHash: wallpaper.AccessHash,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatalf("theme %d settings[%d] wallpaper id %d was not resolvable", theme.ID, i, wallpaper.ID)
|
||||
}
|
||||
gotByID, ok := byID.(*tg.WallPaper)
|
||||
if !ok || gotByID.ID != wallpaper.ID || gotByID.AccessHash != wallpaper.AccessHash {
|
||||
t.Fatalf("theme %d settings[%d] wallpaper id lookup = %#v", theme.ID, i, byID)
|
||||
}
|
||||
bySlug, ok := LookupWallPaper(&tg.InputWallPaperSlug{Slug: wallpaper.Slug})
|
||||
if !ok {
|
||||
t.Fatalf("theme %d settings[%d] wallpaper slug %q was not resolvable", theme.ID, i, wallpaper.Slug)
|
||||
}
|
||||
gotBySlug, ok := bySlug.(*tg.WallPaper)
|
||||
if !ok || gotBySlug.Slug != wallpaper.Slug {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue