chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
281
internal/compat/tdesktop/appearance_seed.go
Normal file
281
internal/compat/tdesktop/appearance_seed.go
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
package tdesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/seed/appearance"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
const appearanceSeedDCID = 2
|
||||
const maxPeerColorBoostLevel = 100
|
||||
|
||||
var peerColorOptionsCache = struct {
|
||||
regularOnce sync.Once
|
||||
profileOnce sync.Once
|
||||
regular []tg.HelpPeerColorOption
|
||||
profile []tg.HelpPeerColorOption
|
||||
}{}
|
||||
|
||||
func seedWallPapers() []tg.WallPaperClass {
|
||||
catalog := appearance.Default()
|
||||
out := make([]tg.WallPaperClass, 0, len(catalog.Wallpapers))
|
||||
for _, wallpaper := range catalog.Wallpapers {
|
||||
out = append(out, seedWallPaper(wallpaper))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LookupWallPaper resolves a cloud wallpaper from the default seed catalog.
|
||||
func LookupWallPaper(input tg.InputWallPaperClass) (tg.WallPaperClass, bool) {
|
||||
if in, ok := input.(*tg.InputWallPaperNoFile); ok {
|
||||
return &tg.WallPaperNoFile{ID: in.ID}, true
|
||||
}
|
||||
catalog := appearance.Default()
|
||||
for _, wallpaper := range catalog.Wallpapers {
|
||||
if inputWallPaperMatches(input, wallpaper) {
|
||||
return seedWallPaper(wallpaper), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// LookupWallPapers resolves multiple wallpapers from the default seed catalog.
|
||||
func LookupWallPapers(inputs []tg.InputWallPaperClass) ([]tg.WallPaperClass, bool) {
|
||||
out := make([]tg.WallPaperClass, 0, len(inputs))
|
||||
for _, input := range inputs {
|
||||
wallpaper, ok := LookupWallPaper(input)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out = append(out, wallpaper)
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func inputWallPaperMatches(input tg.InputWallPaperClass, wallpaper appearance.Wallpaper) bool {
|
||||
switch in := input.(type) {
|
||||
case *tg.InputWallPaper:
|
||||
return in.ID == wallpaper.ID && in.AccessHash == wallpaper.AccessHash
|
||||
case *tg.InputWallPaperSlug:
|
||||
return in.Slug != "" && in.Slug == wallpaper.Slug
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func seedWallPaper(in appearance.Wallpaper) tg.WallPaperClass {
|
||||
if in.Type == 1 || in.Document.ID == 0 {
|
||||
out := &tg.WallPaperNoFile{ID: in.ID}
|
||||
out.SetDefault(in.Default)
|
||||
out.SetDark(in.Dark)
|
||||
out.SetSettings(seedWallPaperSettings(in.Settings))
|
||||
return out
|
||||
}
|
||||
out := &tg.WallPaper{
|
||||
ID: in.ID,
|
||||
AccessHash: in.AccessHash,
|
||||
Slug: in.Slug,
|
||||
Document: seedDocument(in.Document),
|
||||
}
|
||||
out.SetDefault(in.Default)
|
||||
out.SetPattern(in.Pattern)
|
||||
out.SetDark(in.Dark)
|
||||
out.SetSettings(seedWallPaperSettings(in.Settings))
|
||||
return out
|
||||
}
|
||||
|
||||
func seedWallPaperSettings(in appearance.WallpaperSettings) tg.WallPaperSettings {
|
||||
var out tg.WallPaperSettings
|
||||
out.SetBlur(in.Blur)
|
||||
out.SetMotion(in.Motion)
|
||||
if in.BackgroundColor != 0 {
|
||||
out.SetBackgroundColor(in.BackgroundColor)
|
||||
}
|
||||
if in.SecondBackgroundColor != 0 {
|
||||
out.SetSecondBackgroundColor(in.SecondBackgroundColor)
|
||||
}
|
||||
if in.ThirdBackgroundColor != 0 {
|
||||
out.SetThirdBackgroundColor(in.ThirdBackgroundColor)
|
||||
}
|
||||
if in.FourthBackgroundColor != 0 {
|
||||
out.SetFourthBackgroundColor(in.FourthBackgroundColor)
|
||||
}
|
||||
if in.Intensity != 0 {
|
||||
out.SetIntensity(in.Intensity)
|
||||
}
|
||||
if in.Rotation != 0 {
|
||||
out.SetRotation(in.Rotation)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func seedDocument(in appearance.Document) tg.DocumentClass {
|
||||
if in.ID == 0 {
|
||||
return &tg.DocumentEmpty{}
|
||||
}
|
||||
return &tg.Document{
|
||||
ID: in.ID,
|
||||
AccessHash: in.AccessHash,
|
||||
Date: in.Date,
|
||||
MimeType: in.MimeType,
|
||||
Size: in.Size,
|
||||
Thumbs: seedPhotoSizes(in.Thumbs),
|
||||
DCID: appearanceSeedDCID,
|
||||
Attributes: seedDocumentAttributes(in.Attributes),
|
||||
FileReference: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func seedPhotoSizes(in []appearance.PhotoSize) []tg.PhotoSizeClass {
|
||||
out := make([]tg.PhotoSizeClass, 0, len(in))
|
||||
for _, size := range in {
|
||||
switch size.Kind {
|
||||
case "size":
|
||||
if size.Type != "" && size.W > 0 && size.H > 0 && size.Size > 0 {
|
||||
out = append(out, &tg.PhotoSize{Type: size.Type, W: size.W, H: size.H, Size: size.Size})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func seedDocumentAttributes(in []appearance.DocumentAttribute) []tg.DocumentAttributeClass {
|
||||
out := make([]tg.DocumentAttributeClass, 0, len(in))
|
||||
for _, attr := range in {
|
||||
switch attr.Kind {
|
||||
case "image_size":
|
||||
out = append(out, &tg.DocumentAttributeImageSize{W: attr.W, H: attr.H})
|
||||
case "filename":
|
||||
if attr.FileName != "" {
|
||||
out = append(out, &tg.DocumentAttributeFilename{FileName: attr.FileName})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func seedPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
|
||||
if profile {
|
||||
peerColorOptionsCache.profileOnce.Do(func() {
|
||||
peerColorOptionsCache.profile = buildSeedPeerColorOptions(true)
|
||||
})
|
||||
return clonePeerColorOptions(peerColorOptionsCache.profile)
|
||||
}
|
||||
peerColorOptionsCache.regularOnce.Do(func() {
|
||||
peerColorOptionsCache.regular = buildSeedPeerColorOptions(false)
|
||||
})
|
||||
return clonePeerColorOptions(peerColorOptionsCache.regular)
|
||||
}
|
||||
|
||||
func buildSeedPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
|
||||
catalog := appearance.Default()
|
||||
source := catalog.PeerColors
|
||||
if profile {
|
||||
source = catalog.PeerProfileColors
|
||||
}
|
||||
out := make([]tg.HelpPeerColorOption, 0, len(source))
|
||||
for _, color := range source {
|
||||
option := tg.HelpPeerColorOption{ColorID: color.ID}
|
||||
option.SetHidden(color.Hidden)
|
||||
channelMin := boundedPeerColorMinLevel(color.ChannelMinLevel)
|
||||
if channelMin > 0 {
|
||||
option.SetChannelMinLevel(channelMin)
|
||||
}
|
||||
groupMin := boundedPeerColorMinLevel(color.GroupMinLevel)
|
||||
if groupMin == 0 && profile {
|
||||
groupMin = channelMin
|
||||
}
|
||||
if groupMin > 0 {
|
||||
option.SetGroupMinLevel(groupMin)
|
||||
}
|
||||
if colors := seedPeerColorSet(color.Colors); colors != nil {
|
||||
option.SetColors(colors)
|
||||
}
|
||||
if colors := seedPeerColorSet(color.DarkColors); colors != nil {
|
||||
option.SetDarkColors(colors)
|
||||
}
|
||||
out = append(out, option)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func clonePeerColorOptions(in []tg.HelpPeerColorOption) []tg.HelpPeerColorOption {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]tg.HelpPeerColorOption, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
if in[i].Colors != nil {
|
||||
out[i].Colors = clonePeerColorSet(in[i].Colors)
|
||||
}
|
||||
if in[i].DarkColors != nil {
|
||||
out[i].DarkColors = clonePeerColorSet(in[i].DarkColors)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func clonePeerColorSet(in tg.HelpPeerColorSetClass) tg.HelpPeerColorSetClass {
|
||||
switch set := in.(type) {
|
||||
case *tg.HelpPeerColorSet:
|
||||
return &tg.HelpPeerColorSet{Colors: append([]int(nil), set.Colors...)}
|
||||
case *tg.HelpPeerColorProfileSet:
|
||||
return &tg.HelpPeerColorProfileSet{
|
||||
PaletteColors: append([]int(nil), set.PaletteColors...),
|
||||
BgColors: append([]int(nil), set.BgColors...),
|
||||
StoryColors: append([]int(nil), set.StoryColors...),
|
||||
}
|
||||
default:
|
||||
return in
|
||||
}
|
||||
}
|
||||
|
||||
func boundedPeerColorMinLevel(level int) int {
|
||||
if level <= 0 {
|
||||
return 0
|
||||
}
|
||||
if level > maxPeerColorBoostLevel {
|
||||
return maxPeerColorBoostLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func seedPeerColorID(id int, profile bool) (bool, bool) {
|
||||
catalog := appearance.Default()
|
||||
source := catalog.PeerColors
|
||||
if profile {
|
||||
source = catalog.PeerProfileColors
|
||||
}
|
||||
if len(source) == 0 {
|
||||
return false, false
|
||||
}
|
||||
for _, color := range source {
|
||||
if color.ID == id {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
|
||||
func seedPeerColorSet(in *appearance.ColorSet) tg.HelpPeerColorSetClass {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
if len(in.PaletteColors) > 0 || len(in.BgColors) > 0 || len(in.StoryColors) > 0 {
|
||||
return &tg.HelpPeerColorProfileSet{
|
||||
PaletteColors: append([]int(nil), in.PaletteColors...),
|
||||
BgColors: append([]int(nil), in.BgColors...),
|
||||
StoryColors: append([]int(nil), in.StoryColors...),
|
||||
}
|
||||
}
|
||||
if len(in.Colors) > 0 {
|
||||
return &tg.HelpPeerColorSet{Colors: append([]int(nil), in.Colors...)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -16,9 +16,12 @@ func BuildConfig(dc int, ip string, port int, now time.Time) *tg.Config {
|
|||
Expires: int(now.Add(time.Hour).Unix()),
|
||||
TestMode: false,
|
||||
ThisDC: dc,
|
||||
DCOptions: []tg.DCOption{
|
||||
{ID: dc, IPAddress: ip, Port: port, Static: true},
|
||||
},
|
||||
// 不下发 DCOptions:客户端(TDesktop patch / drklo fork)已写死 static DC
|
||||
// 地址,空列表会让客户端保留它——drklo ConnectionsManager.cpp 的 processConfig
|
||||
// 在 dc_options 为空时整段跳过 replaceAddresses/saveConfig,既不覆盖也不持久化。
|
||||
// 服务端因此无需配置对外可达 IP,换网络/部署只改客户端写死地址即可。ip/port
|
||||
// 参数暂留,供未来需要显式 advertise 时改回。
|
||||
DCOptions: nil,
|
||||
ChatSizeMax: 200,
|
||||
MegagroupSizeMax: 200000,
|
||||
ForwardedCountMax: 100,
|
||||
|
|
@ -31,15 +34,20 @@ func BuildConfig(dc int, ip string, port int, now time.Time) *tg.Config {
|
|||
PushChatPeriodMs: 60000,
|
||||
PushChatLimit: 2,
|
||||
EditTimeLimit: 172800,
|
||||
RevokeTimeLimit: 172800,
|
||||
RevokePmTimeLimit: 172800,
|
||||
// 官方现行 revoke 三元组:无时限 + 允许撤回对方发来的私聊消息。
|
||||
// TDesktop 的私聊 "Also delete for X" 复选框要求
|
||||
// revoke_pm_inbox=true 且 revoke_pm_time_limit=0x7FFFFFFF,
|
||||
// 否则双向删除 UI 永不出现。
|
||||
RevokeTimeLimit: 2147483647,
|
||||
RevokePmTimeLimit: 2147483647,
|
||||
RevokePmInbox: true,
|
||||
RatingEDecay: 2419200,
|
||||
StickersRecentLimit: 200,
|
||||
CallReceiveTimeoutMs: 20000,
|
||||
CallRingTimeoutMs: 90000,
|
||||
CallConnectTimeoutMs: 30000,
|
||||
CallPacketTimeoutMs: 10000,
|
||||
MeURLPrefix: "https://t.me/",
|
||||
MeURLPrefix: "https://telesrv.net/",
|
||||
CaptionLengthMax: 1024,
|
||||
MessageLengthMax: 4096,
|
||||
WebfileDCID: dc,
|
||||
|
|
@ -49,7 +57,8 @@ func BuildConfig(dc int, ip string, port int, now time.Time) *tg.Config {
|
|||
// NearestDC 构造 help.getNearestDc 返回值。
|
||||
func NearestDC(dc int) *tg.NearestDC {
|
||||
return &tg.NearestDC{
|
||||
Country: "US",
|
||||
// 默认国家=中国:DrKLO/TDesktop 登录页据此预选区号(+86)。
|
||||
Country: "CN",
|
||||
ThisDC: dc,
|
||||
NearestDC: dc,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
package tdesktop
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/seed/catalog"
|
||||
)
|
||||
|
||||
const (
|
||||
appConfigHash = 4
|
||||
appConfigHash = 12 // app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
countriesListHash = 1
|
||||
timezonesListHash = 1
|
||||
)
|
||||
|
|
@ -21,76 +19,149 @@ func AppConfig(hash int) tg.HelpAppConfigClass {
|
|||
}
|
||||
return &tg.HelpAppConfig{
|
||||
Hash: appConfigHash,
|
||||
Config: readMarkAppConfig(),
|
||||
Config: readMarkAppConfig(""),
|
||||
}
|
||||
}
|
||||
|
||||
func readMarkAppConfig() *tg.JSONObject {
|
||||
return &tg.JSONObject{Value: []tg.JSONObjectValue{
|
||||
func readMarkAppConfig(mapboxToken string) *tg.JSONObject {
|
||||
values := []tg.JSONObjectValue{
|
||||
{Key: "chat_read_mark_size_threshold", Value: &tg.JSONNumber{Value: 50}},
|
||||
{Key: "chat_read_mark_expire_period", Value: &tg.JSONNumber{Value: 604800}},
|
||||
{Key: "pm_read_date_expire_period", Value: &tg.JSONNumber{Value: 604800}},
|
||||
{Key: "quote_length_max", Value: &tg.JSONNumber{Value: 1024}},
|
||||
{Key: "telegram_antispam_group_size_min", Value: &tg.JSONNumber{Value: 200}},
|
||||
{Key: "telegram_antispam_user_id", Value: &tg.JSONString{Value: "5434988373"}},
|
||||
}}
|
||||
// premium_purchase_blocked=false:客户端把 star gift「Send a Gift」入口与
|
||||
// premiumCanBuy()=!premium_purchase_blocked 耦合,置 true 会同时隐藏送礼入口
|
||||
// (详见 app/help/service.go 主配置注释)。这里是无 HelpService 时的最小回退。
|
||||
{Key: "premium_purchase_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
// stargifts_blocked=false:DrKLO 缺省 stargiftsBlocked=true 会隐藏 star gift 送礼网格。
|
||||
{Key: "stargifts_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
{Key: "reactions_user_max_premium", Value: &tg.JSONNumber{Value: 3}},
|
||||
// dialog_filters_enabled=true:TDesktop 据此(或已有文件夹)才显示 Settings→Folders 入口。
|
||||
{Key: "dialog_filters_enabled", Value: &tg.JSONBool{Value: true}},
|
||||
{Key: "stories_stealth_future_period", Value: &tg.JSONNumber{Value: 1500}},
|
||||
{Key: "stories_stealth_past_period", Value: &tg.JSONNumber{Value: 300}},
|
||||
{Key: "stories_stealth_cooldown_period", Value: &tg.JSONNumber{Value: 10800}},
|
||||
// upload_markup_video=true 即官方默认(emoji/sticker 头像由客户端本地渲染 mp4 后随
|
||||
// markup 一起上传)。显式下发是为了把曾收到过 false 的客户端持久化配置洗回默认——
|
||||
// 客户端对缺失的 key 会保留本地旧值,仅删除 key 无法恢复。
|
||||
{Key: "upload_markup_video", Value: &tg.JSONBool{Value: true}},
|
||||
// 单 emoji 消息转 InputMediaDice 的白名单;须与 rpc 层 dice 取值表同步。
|
||||
{Key: "emojies_send_dice", Value: &tg.JSONArray{Value: []tg.JSONValueClass{
|
||||
&tg.JSONString{Value: "\U0001F3B2"}, // 🎲
|
||||
&tg.JSONString{Value: "\U0001F3AF"}, // 🎯
|
||||
&tg.JSONString{Value: "\U0001F3C0"}, // 🏀
|
||||
&tg.JSONString{Value: "⚽"},
|
||||
&tg.JSONString{Value: "⚽️"},
|
||||
&tg.JSONString{Value: "\U0001F3B3"}, // 🎳
|
||||
&tg.JSONString{Value: "\U0001F3B0"}, // 🎰
|
||||
}}},
|
||||
}
|
||||
if mapboxToken != "" {
|
||||
// TDesktop 位置选点器(WebView+Mapbox GL)与 business 位置设置的地图 token;
|
||||
// 缺失时选点器地图空白(瓦片直连 api.mapbox.com,不经服务端)。
|
||||
values = append(values, tg.JSONObjectValue{Key: "tdesktop_config_map", Value: &tg.JSONObject{Value: []tg.JSONObjectValue{
|
||||
{Key: "maps", Value: &tg.JSONString{Value: mapboxToken}},
|
||||
{Key: "geo", Value: &tg.JSONString{Value: mapboxToken}},
|
||||
{Key: "bmaps", Value: &tg.JSONString{Value: mapboxToken}},
|
||||
{Key: "bgeo", Value: &tg.JSONString{Value: mapboxToken}},
|
||||
}}})
|
||||
}
|
||||
return &tg.JSONObject{Value: values}
|
||||
}
|
||||
|
||||
// TimezonesList returns a small non-empty timezone set for TDesktop business settings preloading.
|
||||
// fallbackTimezones 是 catalog 未 seed 时的内置最小时区集。
|
||||
var fallbackTimezones = []tg.Timezone{
|
||||
{ID: "Etc/UTC", Name: "UTC", UtcOffset: 0},
|
||||
{ID: "America/New_York", Name: "Eastern Time", UtcOffset: -5 * 60 * 60},
|
||||
{ID: "America/Chicago", Name: "Central Time", UtcOffset: -6 * 60 * 60},
|
||||
{ID: "America/Denver", Name: "Mountain Time", UtcOffset: -7 * 60 * 60},
|
||||
{ID: "America/Los_Angeles", Name: "Pacific Time", UtcOffset: -8 * 60 * 60},
|
||||
{ID: "Asia/Shanghai", Name: "China Standard Time", UtcOffset: 8 * 60 * 60},
|
||||
}
|
||||
|
||||
// TimezonesList 返回时区目录:优先用 catalog 固化的官方全量(~419 个),未 seed 时回退内置最小集。
|
||||
func TimezonesList(hash int) tg.HelpTimezonesListClass {
|
||||
if hash == timezonesListHash {
|
||||
seeded, h := catalog.Timezones()
|
||||
src := seeded
|
||||
if len(src) == 0 {
|
||||
src, h = nil, timezonesListHash
|
||||
}
|
||||
if hash == h {
|
||||
return &tg.HelpTimezonesListNotModified{}
|
||||
}
|
||||
return &tg.HelpTimezonesList{
|
||||
Hash: timezonesListHash,
|
||||
Timezones: []tg.Timezone{
|
||||
{ID: "Etc/UTC", Name: "UTC", UtcOffset: 0},
|
||||
{ID: "America/New_York", Name: "Eastern Time", UtcOffset: -5 * 60 * 60},
|
||||
{ID: "America/Chicago", Name: "Central Time", UtcOffset: -6 * 60 * 60},
|
||||
{ID: "America/Denver", Name: "Mountain Time", UtcOffset: -7 * 60 * 60},
|
||||
{ID: "America/Los_Angeles", Name: "Pacific Time", UtcOffset: -8 * 60 * 60},
|
||||
{ID: "Asia/Shanghai", Name: "China Standard Time", UtcOffset: 8 * 60 * 60},
|
||||
},
|
||||
items := fallbackTimezones
|
||||
if len(src) > 0 {
|
||||
items = make([]tg.Timezone, 0, len(src))
|
||||
for _, t := range src {
|
||||
items = append(items, tg.Timezone{ID: t.ID, Name: t.Name, UtcOffset: t.UTCOffset})
|
||||
}
|
||||
}
|
||||
return &tg.HelpTimezonesList{Hash: h, Timezones: items}
|
||||
}
|
||||
|
||||
// SuggestedDialogFilters 返回新建对话文件夹时的「推荐文件夹」模板(官方固定语义:
|
||||
// Unread/Personal/Unmuted/Groups/Channels/Bots)。客户端据此一键创建对应规则的文件夹;
|
||||
// 模板 id 仅占位,用户采纳时客户端会以新 id 调 updateDialogFilter 真正创建。
|
||||
func SuggestedDialogFilters() []tg.DialogFilterSuggested {
|
||||
mk := func(id int, title, desc string, set func(*tg.DialogFilter)) tg.DialogFilterSuggested {
|
||||
df := &tg.DialogFilter{
|
||||
ID: id,
|
||||
Title: tg.TextWithEntities{Text: title, Entities: []tg.MessageEntityClass{}},
|
||||
PinnedPeers: []tg.InputPeerClass{},
|
||||
IncludePeers: []tg.InputPeerClass{},
|
||||
ExcludePeers: []tg.InputPeerClass{},
|
||||
}
|
||||
set(df)
|
||||
return tg.DialogFilterSuggested{Filter: df, Description: desc}
|
||||
}
|
||||
allTypes := func(d *tg.DialogFilter) {
|
||||
d.Contacts, d.NonContacts, d.Groups, d.Broadcasts, d.Bots = true, true, true, true, true
|
||||
}
|
||||
return []tg.DialogFilterSuggested{
|
||||
mk(2, "Unread", "New messages", func(d *tg.DialogFilter) { allTypes(d); d.ExcludeRead = true }),
|
||||
mk(3, "Personal", "Personal chats", func(d *tg.DialogFilter) { d.Contacts, d.NonContacts = true, true }),
|
||||
mk(4, "Unmuted", "Unmuted chats", func(d *tg.DialogFilter) { allTypes(d); d.ExcludeMuted = true }),
|
||||
mk(5, "Groups", "Group chats", func(d *tg.DialogFilter) { d.Groups = true }),
|
||||
mk(6, "Channels", "Channels only", func(d *tg.DialogFilter) { d.Broadcasts = true }),
|
||||
mk(7, "Bots", "Bot chats", func(d *tg.DialogFilter) { d.Bots = true }),
|
||||
}
|
||||
}
|
||||
|
||||
// CountriesList returns the fallback login country list used when HelpService is absent.
|
||||
// CountriesList 返回登录页国家区号目录:优先用 catalog 固化的官方全量(~235 国),未 seed
|
||||
// 时回退内置最小集(US/CN)。生产路径通常经 HelpService.GetCountries → defaultCountries
|
||||
// 走 catalog;此函数是 HelpService 缺省时的兜底。
|
||||
func CountriesList(hash int) tg.HelpCountriesListClass {
|
||||
if hash == countriesListHash {
|
||||
list := catalog.Countries()
|
||||
if len(list.Countries) == 0 {
|
||||
if hash == countriesListHash {
|
||||
return &tg.HelpCountriesListNotModified{}
|
||||
}
|
||||
return &tg.HelpCountriesList{
|
||||
Hash: countriesListHash,
|
||||
Countries: []tg.HelpCountry{
|
||||
{ISO2: "US", DefaultName: "United States", CountryCodes: []tg.HelpCountryCode{{CountryCode: "1", Prefixes: []string{""}, Patterns: []string{"XXX XXX XXXX"}}}},
|
||||
{ISO2: "CN", DefaultName: "China", CountryCodes: []tg.HelpCountryCode{{CountryCode: "86", Prefixes: []string{""}, Patterns: []string{"XXX XXXX XXXX"}}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
if hash == list.Hash {
|
||||
return &tg.HelpCountriesListNotModified{}
|
||||
}
|
||||
return &tg.HelpCountriesList{
|
||||
Hash: countriesListHash,
|
||||
Countries: []tg.HelpCountry{
|
||||
{
|
||||
ISO2: "US",
|
||||
DefaultName: "United States",
|
||||
CountryCodes: []tg.HelpCountryCode{
|
||||
{CountryCode: "1", Prefixes: []string{"1"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ISO2: "CN",
|
||||
DefaultName: "China",
|
||||
CountryCodes: []tg.HelpCountryCode{
|
||||
{CountryCode: "86", Prefixes: []string{"86"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LoginToken returns a short-lived placeholder token for TDesktop's QR-login
|
||||
// screen. First phase only supports phone login, but TDesktop expects this RPC
|
||||
// to produce an auth.loginToken while the QR screen is visible.
|
||||
func LoginToken(now time.Time, authKeyID [8]byte, sessionID int64) *tg.AuthLoginToken {
|
||||
var seed [len(authKeyID) + 8 + 8]byte
|
||||
copy(seed[:len(authKeyID)], authKeyID[:])
|
||||
binary.LittleEndian.PutUint64(seed[len(authKeyID):], uint64(sessionID))
|
||||
binary.LittleEndian.PutUint64(seed[len(authKeyID)+8:], uint64(now.UnixNano()))
|
||||
token := sha256.Sum256(seed[:])
|
||||
return &tg.AuthLoginToken{
|
||||
Expires: int(now.Add(30 * time.Second).Unix()),
|
||||
Token: token[:],
|
||||
out := &tg.HelpCountriesList{Hash: list.Hash, Countries: make([]tg.HelpCountry, 0, len(list.Countries))}
|
||||
for _, c := range list.Countries {
|
||||
item := tg.HelpCountry{
|
||||
Hidden: c.Hidden,
|
||||
ISO2: c.ISO2,
|
||||
DefaultName: c.DefaultName,
|
||||
Name: c.Name,
|
||||
CountryCodes: make([]tg.HelpCountryCode, 0, len(c.CountryCodes)),
|
||||
}
|
||||
for _, cc := range c.CountryCodes {
|
||||
item.CountryCodes = append(item.CountryCodes, tg.HelpCountryCode{CountryCode: cc.CountryCode, Prefixes: cc.Prefixes, Patterns: cc.Patterns})
|
||||
}
|
||||
out.Countries = append(out.Countries, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
196
internal/compat/tdesktop/dialogs.go
Normal file
196
internal/compat/tdesktop/dialogs.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package tdesktop
|
||||
|
||||
import "telesrv/internal/domain"
|
||||
|
||||
// ShouldMergePinnedIntoInitialDialogs reports whether a TDesktop main-list
|
||||
// messages.getDialogs request should carry pinned/archive rows inline.
|
||||
//
|
||||
// TDesktop starts the main list with getDialogs(exclude_pinned=true) and loads
|
||||
// pinned rows through getPinnedDialogs in parallel. If the exclude_pinned result
|
||||
// is rendered first, the first visible list temporarily lacks pinned/archive
|
||||
// rows. Keep the workaround scoped to the first main-folder page with no hash so
|
||||
// pagination and cache probes retain normal Telegram semantics.
|
||||
func ShouldMergePinnedIntoInitialDialogs(filter domain.DialogFilter) bool {
|
||||
if !filter.ExcludePinned || filter.Hash != 0 {
|
||||
return false
|
||||
}
|
||||
if filter.HasFolderID && filter.FolderID != domain.DialogMainFolderID {
|
||||
return false
|
||||
}
|
||||
if filter.OffsetID != 0 || filter.OffsetDate != 0 || filter.HasOffsetPeer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MergeInitialDialogsWithPinned prepends the pinned getPinnedDialogs view to an
|
||||
// initial getDialogs(exclude_pinned=true) main-list response. The returned list
|
||||
// remains a normal messages.dialogs/messages.dialogsSlice projection; only the
|
||||
// first response carries extra pinned/archive rows for TDesktop startup.
|
||||
func MergeInitialDialogsWithPinned(main, pinned domain.DialogList) domain.DialogList {
|
||||
if pinned.ArchiveSummary == nil && len(pinned.Dialogs) == 0 {
|
||||
return main
|
||||
}
|
||||
out := main
|
||||
out.ArchiveSummary = pinned.ArchiveSummary
|
||||
out.Dialogs = mergeDialogsByPeer(pinned.Dialogs, main.Dialogs)
|
||||
out.Messages = mergePrivateMessages(pinned.Messages, main.Messages)
|
||||
out.ChannelMessages = mergeChannelMessages(pinned.ChannelMessages, main.ChannelMessages)
|
||||
out.Users = mergeUsersByID(pinned.Users, main.Users)
|
||||
out.Channels = mergeChannelsByID(pinned.Channels, main.Channels)
|
||||
pinnedEntries := len(pinned.Dialogs)
|
||||
if pinned.ArchiveSummary != nil {
|
||||
pinnedEntries++
|
||||
}
|
||||
mainCount := main.Count
|
||||
if mainCount == 0 {
|
||||
mainCount = len(main.Dialogs)
|
||||
}
|
||||
out.Count = mainCount + pinnedEntries
|
||||
out.Hash = 0
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeDialogsByPeer(first, second []domain.Dialog) []domain.Dialog {
|
||||
out := make([]domain.Dialog, 0, len(first)+len(second))
|
||||
seen := make(map[domain.Peer]struct{}, len(first)+len(second))
|
||||
for _, d := range first {
|
||||
if d.Peer.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[d.Peer]; ok {
|
||||
continue
|
||||
}
|
||||
seen[d.Peer] = struct{}{}
|
||||
out = append(out, d)
|
||||
}
|
||||
for _, d := range second {
|
||||
if d.Peer.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[d.Peer]; ok {
|
||||
continue
|
||||
}
|
||||
seen[d.Peer] = struct{}{}
|
||||
out = append(out, d)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type privateMessageKey struct {
|
||||
peer domain.Peer
|
||||
id int
|
||||
}
|
||||
|
||||
func mergePrivateMessages(first, second []domain.Message) []domain.Message {
|
||||
out := make([]domain.Message, 0, len(first)+len(second))
|
||||
seen := make(map[privateMessageKey]struct{}, len(first)+len(second))
|
||||
for _, msg := range first {
|
||||
key := privateMessageKey{peer: msg.Peer, id: msg.ID}
|
||||
if key.peer.ID == 0 || key.id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, msg)
|
||||
}
|
||||
for _, msg := range second {
|
||||
key := privateMessageKey{peer: msg.Peer, id: msg.ID}
|
||||
if key.peer.ID == 0 || key.id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, msg)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type channelMessageKey struct {
|
||||
channelID int64
|
||||
id int
|
||||
}
|
||||
|
||||
func mergeChannelMessages(first, second []domain.ChannelMessage) []domain.ChannelMessage {
|
||||
out := make([]domain.ChannelMessage, 0, len(first)+len(second))
|
||||
seen := make(map[channelMessageKey]struct{}, len(first)+len(second))
|
||||
for _, msg := range first {
|
||||
key := channelMessageKey{channelID: msg.ChannelID, id: msg.ID}
|
||||
if key.channelID == 0 || key.id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, msg)
|
||||
}
|
||||
for _, msg := range second {
|
||||
key := channelMessageKey{channelID: msg.ChannelID, id: msg.ID}
|
||||
if key.channelID == 0 || key.id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, msg)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeUsersByID(first, second []domain.User) []domain.User {
|
||||
out := make([]domain.User, 0, len(first)+len(second))
|
||||
seen := make(map[int64]struct{}, len(first)+len(second))
|
||||
for _, user := range first {
|
||||
if user.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[user.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[user.ID] = struct{}{}
|
||||
out = append(out, user)
|
||||
}
|
||||
for _, user := range second {
|
||||
if user.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[user.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[user.ID] = struct{}{}
|
||||
out = append(out, user)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeChannelsByID(first, second []domain.Channel) []domain.Channel {
|
||||
out := make([]domain.Channel, 0, len(first)+len(second))
|
||||
seen := make(map[int64]struct{}, len(first)+len(second))
|
||||
for _, channel := range first {
|
||||
if channel.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[channel.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[channel.ID] = struct{}{}
|
||||
out = append(out, channel)
|
||||
}
|
||||
for _, channel := range second {
|
||||
if channel.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[channel.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[channel.ID] = struct{}{}
|
||||
out = append(out, channel)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/seed/appearance"
|
||||
"telesrv/internal/seed/catalog"
|
||||
)
|
||||
|
||||
// NotifySettings returns default per-peer notification settings for empty first-phase accounts.
|
||||
|
|
@ -23,17 +26,6 @@ func NotifySettings() *tg.PeerNotifySettings {
|
|||
return settings
|
||||
}
|
||||
|
||||
// ReactionsNotifySettings returns conservative defaults for reaction notifications.
|
||||
func ReactionsNotifySettings() *tg.ReactionsNotifySettings {
|
||||
return &tg.ReactionsNotifySettings{
|
||||
MessagesNotifyFrom: &tg.ReactionNotificationsFromContacts{},
|
||||
StoriesNotifyFrom: &tg.ReactionNotificationsFromContacts{},
|
||||
PollVotesNotifyFrom: &tg.ReactionNotificationsFromContacts{},
|
||||
Sound: &tg.NotificationSoundDefault{},
|
||||
ShowPreviews: true,
|
||||
}
|
||||
}
|
||||
|
||||
func PrivacyRules(key tg.InputPrivacyKeyClass) *tg.AccountPrivacyRules {
|
||||
var rule tg.PrivacyRuleClass = &tg.PrivacyValueAllowAll{}
|
||||
switch key.(type) {
|
||||
|
|
@ -61,16 +53,176 @@ func Passkeys() *tg.AccountPasskeys {
|
|||
return &tg.AccountPasskeys{Passkeys: []tg.Passkey{}}
|
||||
}
|
||||
|
||||
func ContentSettings() *tg.AccountContentSettings {
|
||||
return &tg.AccountContentSettings{}
|
||||
}
|
||||
|
||||
func GlobalPrivacySettings() *tg.GlobalPrivacySettings {
|
||||
return &tg.GlobalPrivacySettings{}
|
||||
}
|
||||
|
||||
func AccountThemes() tg.AccountThemesClass {
|
||||
return &tg.AccountThemesNotModified{}
|
||||
const chatThemesHash int64 = 2026062501
|
||||
const uniqueGiftChatThemesHash int64 = 2026061201
|
||||
const wallPapersHash int64 = 2026062502
|
||||
const peerColorsHash = 2026061202
|
||||
const peerProfileColorsHash = 2026062801
|
||||
|
||||
// IsChatThemeEmoticon reports whether token is a known official chat theme emoticon.
|
||||
func IsChatThemeEmoticon(token string) bool {
|
||||
for _, ct := range appearance.Default().ChatThemes {
|
||||
if ct.Emoticon == token {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ChatThemes(hash int64) tg.AccountThemesClass {
|
||||
if hash == chatThemesHash {
|
||||
return &tg.AccountThemesNotModified{}
|
||||
}
|
||||
return &tg.AccountThemes{
|
||||
Hash: chatThemesHash,
|
||||
Themes: chatThemeList(),
|
||||
}
|
||||
}
|
||||
|
||||
// chatThemeList 用官方外观 catalog(appearancefetch 导出)的聊天主题。
|
||||
func chatThemeList() []tg.Theme {
|
||||
return catalogChatThemes(appearance.Default().ChatThemes)
|
||||
}
|
||||
|
||||
// catalogChatThemes 把官方聊天主题转成 tg.Theme。官方每个主题只带 classic+tinted 两组
|
||||
// settings,而 DrKLO 选择器丢弃 base-theme 设置 <4 的主题
|
||||
// (MediaDataController.generateEmojiPreviewThemes),故扩展到 telesrv 宣告的 5 个 base:
|
||||
// 浅色 base(classic/day/arctic)复用官方浅色设置,深色 base(tinted/night)复用官方深色设置。
|
||||
func catalogChatThemes(cts []appearance.ChatTheme) []tg.Theme {
|
||||
out := make([]tg.Theme, 0, len(cts))
|
||||
for i, ct := range cts {
|
||||
var light, dark *appearance.ThemeSettings
|
||||
for j := range ct.Settings {
|
||||
s := &ct.Settings[j]
|
||||
if s.BaseTheme == "tinted" || s.BaseTheme == "night" {
|
||||
if dark == nil {
|
||||
dark = s
|
||||
}
|
||||
} else if light == nil {
|
||||
light = s
|
||||
}
|
||||
}
|
||||
if light == nil {
|
||||
light = dark
|
||||
}
|
||||
if dark == nil {
|
||||
dark = light
|
||||
}
|
||||
if light == nil {
|
||||
continue
|
||||
}
|
||||
theme := tg.Theme{ID: ct.ID, AccessHash: ct.AccessHash, Slug: ct.Slug, Title: ct.Title}
|
||||
theme.SetForChat(true)
|
||||
if ct.Emoticon != "" {
|
||||
theme.SetEmoticon(ct.Emoticon)
|
||||
}
|
||||
theme.SetInstallsCount(1)
|
||||
settings := make([]tg.ThemeSettings, 0, len(chatThemeBaseThemes))
|
||||
for _, b := range chatThemeBaseThemes {
|
||||
src := light
|
||||
if b.dark {
|
||||
src = dark
|
||||
}
|
||||
settings = append(settings, catalogThemeSettings(*src, b.make()))
|
||||
}
|
||||
theme.SetSettings(settings)
|
||||
if i == 0 || ct.Default {
|
||||
theme.SetDefault(true)
|
||||
}
|
||||
out = append(out, theme)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func catalogThemeSettings(s appearance.ThemeSettings, base tg.BaseThemeClass) tg.ThemeSettings {
|
||||
ts := tg.ThemeSettings{BaseTheme: base, AccentColor: s.AccentColor}
|
||||
if s.OutboxAccentColor != 0 {
|
||||
ts.SetOutboxAccentColor(s.OutboxAccentColor)
|
||||
}
|
||||
if len(s.MessageColors) > 0 {
|
||||
ts.SetMessageColors(append([]int(nil), s.MessageColors...))
|
||||
}
|
||||
if s.Wallpaper.ID != 0 || s.Wallpaper.Document.ID != 0 {
|
||||
ts.SetWallpaper(seedWallPaper(s.Wallpaper))
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
// DefaultThemes serves the built-in chat theme catalog for account.getThemes.
|
||||
//
|
||||
// DrKLO's "Color theme" preview strip on the Chat Settings page
|
||||
// (DefaultThemesPreviewCell) is populated exclusively from account.getThemes
|
||||
// results flagged is_default=true — it does not consult account.getChatThemes.
|
||||
// A fresh client has no cached emoji themes and therefore sends getThemes with
|
||||
// hash 0; returning themesNotModified to that request leaves defaultEmojiThemes
|
||||
// empty, so the strip stays stuck on its FlickerLoadingView shimmer forever.
|
||||
// We return the same catalog as ChatThemes with every entry marked default so
|
||||
// the strip resolves into real preview cards.
|
||||
func DefaultThemes(hash int64) tg.AccountThemesClass {
|
||||
if hash == chatThemesHash {
|
||||
return &tg.AccountThemesNotModified{}
|
||||
}
|
||||
return &tg.AccountThemes{
|
||||
Hash: chatThemesHash,
|
||||
Themes: DefaultThemeList(),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultThemeList 返回内置默认主题切片(全标 is_default=true)。account.getThemes
|
||||
// 把它与用户自定义主题合并(rpc 层),故单独暴露列表。
|
||||
func DefaultThemeList() []tg.Theme {
|
||||
themes := chatThemeList()
|
||||
for i := range themes {
|
||||
themes[i].SetDefault(true)
|
||||
}
|
||||
return themes
|
||||
}
|
||||
|
||||
func UniqueGiftChatThemes(hash int64) tg.AccountChatThemesClass {
|
||||
if hash == uniqueGiftChatThemesHash {
|
||||
return &tg.AccountChatThemesNotModified{}
|
||||
}
|
||||
return &tg.AccountChatThemes{
|
||||
Hash: uniqueGiftChatThemesHash,
|
||||
Themes: []tg.ChatThemeClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
}
|
||||
|
||||
// WallPapers returns the read-only default wallpaper catalog. User wallpaper
|
||||
// upload/save/install remains outside the current TDesktop compatibility scope.
|
||||
func WallPapers(hash int64) tg.AccountWallPapersClass {
|
||||
if hash == wallPapersHash {
|
||||
return &tg.AccountWallPapersNotModified{}
|
||||
}
|
||||
wallpapers := seedWallPapers()
|
||||
return &tg.AccountWallPapers{
|
||||
Hash: wallPapersHash,
|
||||
Wallpapers: wallpapers,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// (MediaDataController.generateEmojiPreviewThemes drops anything whose
|
||||
// items.size() < 4, one item per ThemeSettings). dark selects the bubble/accent
|
||||
// palette used for that base theme.
|
||||
var chatThemeBaseThemes = []struct {
|
||||
make func() tg.BaseThemeClass
|
||||
dark bool
|
||||
}{
|
||||
{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 AutoDownloadSettings() *tg.AccountAutoDownloadSettings {
|
||||
|
|
@ -101,10 +253,6 @@ func DefaultGroupPhotoEmojis() tg.EmojiListClass {
|
|||
return &tg.EmojiList{Hash: 0, DocumentID: []int64{}}
|
||||
}
|
||||
|
||||
func ConnectedBots() *tg.AccountConnectedBots {
|
||||
return &tg.AccountConnectedBots{ConnectedBots: []tg.ConnectedBot{}, Users: []tg.UserClass{}}
|
||||
}
|
||||
|
||||
const availableReactionsHash = 20260602
|
||||
const emptyStickerSetHash = 20260602
|
||||
|
||||
|
|
@ -161,10 +309,7 @@ func availableReaction(reaction defaultReaction, index int) tg.AvailableReaction
|
|||
}
|
||||
}
|
||||
|
||||
func Stickers() tg.MessagesStickersClass {
|
||||
return &tg.MessagesStickersNotModified{}
|
||||
}
|
||||
|
||||
// Stickers 返回空的全量 messages.stickers。禁止回 stickersNotModified:DrKLO 对
|
||||
func StickerSet(req *tg.MessagesGetStickerSetRequest) tg.MessagesStickerSetClass {
|
||||
if req != nil && req.Hash == emptyStickerSetHash {
|
||||
return &tg.MessagesStickerSetNotModified{}
|
||||
|
|
@ -206,8 +351,23 @@ func StickerSet(req *tg.MessagesGetStickerSetRequest) tg.MessagesStickerSetClass
|
|||
}
|
||||
}
|
||||
|
||||
func EmojiGroups() tg.MessagesEmojiGroupsClass {
|
||||
return &tg.MessagesEmojiGroupsNotModified{}
|
||||
// EmojiGroups 返回 emoji 分类目录(客户端 emoji 面板顶部的主题快捷标签)。优先用 catalog
|
||||
// 固化的官方分组,未 seed 时回 NotModified(客户端保留本地/无分组)。icon_emoji_id 指向
|
||||
// custom-emoji 文档,本地缺该文档时分类图标回退占位,不影响分组与按词搜索。
|
||||
func EmojiGroups(hash int) tg.MessagesEmojiGroupsClass {
|
||||
groups, h := catalog.EmojiGroups()
|
||||
if len(groups) == 0 || hash == h {
|
||||
return &tg.MessagesEmojiGroupsNotModified{}
|
||||
}
|
||||
out := make([]tg.EmojiGroupClass, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
out = append(out, &tg.EmojiGroup{Title: g.Title, IconEmojiID: g.IconEmojiID, Emoticons: g.Emoticons})
|
||||
}
|
||||
return &tg.MessagesEmojiGroups{Hash: h, Groups: out}
|
||||
}
|
||||
|
||||
func EmojiStatusGroups() tg.MessagesEmojiGroupsClass {
|
||||
return &tg.MessagesEmojiGroups{Hash: 0, Groups: []tg.EmojiGroupClass{}}
|
||||
}
|
||||
|
||||
func EmojiProfilePhotoGroups() tg.MessagesEmojiGroupsClass {
|
||||
|
|
@ -215,11 +375,20 @@ func EmojiProfilePhotoGroups() tg.MessagesEmojiGroupsClass {
|
|||
}
|
||||
|
||||
func AttachMenuBots() tg.AttachMenuBotsClass {
|
||||
return &tg.AttachMenuBotsNotModified{}
|
||||
return &tg.AttachMenuBots{
|
||||
Hash: 0,
|
||||
Bots: []tg.AttachMenuBot{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
}
|
||||
|
||||
func QuickReplies() tg.MessagesQuickRepliesClass {
|
||||
return &tg.MessagesQuickRepliesNotModified{}
|
||||
return &tg.MessagesQuickReplies{
|
||||
QuickReplies: []tg.QuickReply{},
|
||||
Messages: []tg.MessageClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
}
|
||||
|
||||
func TopPeers() tg.ContactsTopPeersClass {
|
||||
|
|
@ -234,8 +403,93 @@ func BlockedContacts() tg.ContactsBlockedClass {
|
|||
}
|
||||
}
|
||||
|
||||
func PeerColors() tg.HelpPeerColorsClass {
|
||||
return &tg.HelpPeerColorsNotModified{}
|
||||
type defaultPeerColor struct {
|
||||
id int
|
||||
light []int
|
||||
dark []int
|
||||
bg []int
|
||||
story []int
|
||||
hidden bool
|
||||
}
|
||||
|
||||
var defaultPeerColors = []defaultPeerColor{
|
||||
{id: 0, light: []int{0xcc4d4d}, dark: []int{0xff7a7a}, bg: []int{0xffd6d6, 0xfff0f0}, story: []int{0xff7a7a, 0xf24b4b}},
|
||||
{id: 1, light: []int{0xd97829}, dark: []int{0xffa15c}, bg: []int{0xffdfbd, 0xfff2e2}, story: []int{0xffa15c, 0xf07a2b}},
|
||||
{id: 2, light: []int{0x8756d9}, dark: []int{0xb38cff}, bg: []int{0xe3d8ff, 0xf4eeff}, story: []int{0xb38cff, 0x8756d9}},
|
||||
{id: 3, light: []int{0x3ca66b}, dark: []int{0x74d99b}, bg: []int{0xd5f5df, 0xedfff2}, story: []int{0x74d99b, 0x35a86a}},
|
||||
{id: 4, light: []int{0x2d9fc6}, dark: []int{0x6fd8f5}, bg: []int{0xd4f4ff, 0xf0fbff}, story: []int{0x6fd8f5, 0x2d9fc6}},
|
||||
{id: 5, light: []int{0x4a86d9}, dark: []int{0x78aef5}, bg: []int{0xd8e8ff, 0xf2f7ff}, story: []int{0x78aef5, 0x4a86d9}},
|
||||
{id: 6, light: []int{0xd85b93}, dark: []int{0xff8abb}, bg: []int{0xffd7e8, 0xfff5fb}, story: []int{0xff8abb, 0xd85b93}},
|
||||
{id: 7, light: []int{0x5f6a7a, 0x8c96a6}, dark: []int{0x9aa4b4, 0xc0c8d4}, bg: []int{0xe3e7ee, 0xf7f9fc}, story: []int{0x9aa4b4, 0x6f7b8d}},
|
||||
}
|
||||
|
||||
// IsPeerColorID reports whether id is in the TDesktop-compatible peer color palette.
|
||||
func IsPeerColorID(id int) bool {
|
||||
if found, seeded := seedPeerColorID(id, false); seeded {
|
||||
return found
|
||||
}
|
||||
for _, color := range defaultPeerColors {
|
||||
if color.id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsPeerProfileColorID reports whether id is in the profile background palette.
|
||||
func IsPeerProfileColorID(id int) bool {
|
||||
if found, seeded := seedPeerColorID(id, true); seeded {
|
||||
return found
|
||||
}
|
||||
return IsPeerColorID(id)
|
||||
}
|
||||
|
||||
func PeerColors(hash int) tg.HelpPeerColorsClass {
|
||||
if hash == peerColorsHash {
|
||||
return &tg.HelpPeerColorsNotModified{}
|
||||
}
|
||||
colors := seedPeerColorOptions(false)
|
||||
if len(colors) == 0 {
|
||||
colors = make([]tg.HelpPeerColorOption, 0, len(defaultPeerColors))
|
||||
for _, color := range defaultPeerColors {
|
||||
option := tg.HelpPeerColorOption{ColorID: color.id}
|
||||
option.SetColors(&tg.HelpPeerColorSet{Colors: append([]int(nil), color.light...)})
|
||||
option.SetDarkColors(&tg.HelpPeerColorSet{Colors: append([]int(nil), color.dark...)})
|
||||
if color.hidden {
|
||||
option.SetHidden(true)
|
||||
}
|
||||
colors = append(colors, option)
|
||||
}
|
||||
}
|
||||
return &tg.HelpPeerColors{Hash: peerColorsHash, Colors: colors}
|
||||
}
|
||||
|
||||
func PeerProfileColors(hash int) tg.HelpPeerColorsClass {
|
||||
if hash == peerProfileColorsHash {
|
||||
return &tg.HelpPeerColorsNotModified{}
|
||||
}
|
||||
colors := seedPeerColorOptions(true)
|
||||
if len(colors) == 0 {
|
||||
colors = make([]tg.HelpPeerColorOption, 0, len(defaultPeerColors))
|
||||
for _, color := range defaultPeerColors {
|
||||
option := tg.HelpPeerColorOption{ColorID: color.id}
|
||||
option.SetColors(&tg.HelpPeerColorProfileSet{
|
||||
PaletteColors: append([]int(nil), color.light...),
|
||||
BgColors: append([]int(nil), color.bg...),
|
||||
StoryColors: append([]int(nil), color.story...),
|
||||
})
|
||||
option.SetDarkColors(&tg.HelpPeerColorProfileSet{
|
||||
PaletteColors: append([]int(nil), color.dark...),
|
||||
BgColors: append([]int(nil), color.bg...),
|
||||
StoryColors: append([]int(nil), color.story...),
|
||||
})
|
||||
if color.hidden {
|
||||
option.SetHidden(true)
|
||||
}
|
||||
colors = append(colors, option)
|
||||
}
|
||||
}
|
||||
return &tg.HelpPeerColors{Hash: peerProfileColorsHash, Colors: colors}
|
||||
}
|
||||
|
||||
func PromoData(now time.Time) tg.HelpPromoDataClass {
|
||||
|
|
@ -246,17 +500,6 @@ func TermsOfServiceUpdate(now time.Time) tg.HelpTermsOfServiceUpdateClass {
|
|||
return &tg.HelpTermsOfServiceUpdateEmpty{Expires: int(now.Add(24 * time.Hour).Unix())}
|
||||
}
|
||||
|
||||
func PremiumPromo() *tg.HelpPremiumPromo {
|
||||
return &tg.HelpPremiumPromo{}
|
||||
}
|
||||
|
||||
func AllStories() tg.StoriesAllStoriesClass {
|
||||
return &tg.StoriesAllStories{
|
||||
State: "",
|
||||
StealthMode: tg.StoriesStealthMode{},
|
||||
}
|
||||
}
|
||||
|
||||
func StoriesArchive() *tg.StoriesStories {
|
||||
return &tg.StoriesStories{}
|
||||
}
|
||||
|
|
@ -273,6 +516,14 @@ func StarGiftActiveAuctions() tg.PaymentsStarGiftActiveAuctionsClass {
|
|||
return &tg.PaymentsStarGiftActiveAuctionsNotModified{}
|
||||
}
|
||||
|
||||
func StarGifts() tg.PaymentsStarGiftsClass {
|
||||
return &tg.PaymentsStarGifts{
|
||||
Gifts: []tg.StarGiftClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
}
|
||||
|
||||
func SavedStarGifts() *tg.PaymentsSavedStarGifts {
|
||||
return &tg.PaymentsSavedStarGifts{
|
||||
Gifts: []tg.SavedStarGift{},
|
||||
|
|
@ -281,6 +532,29 @@ func SavedStarGifts() *tg.PaymentsSavedStarGifts {
|
|||
}
|
||||
}
|
||||
|
||||
func StarGiftCollections() tg.PaymentsStarGiftCollectionsClass {
|
||||
return &tg.PaymentsStarGiftCollections{
|
||||
Collections: []tg.StarGiftCollection{},
|
||||
}
|
||||
}
|
||||
|
||||
func StarsRevenueStats(ton bool) *tg.PaymentsStarsRevenueStats {
|
||||
zeroAmount := tg.StarsAmountClass(&tg.StarsAmount{})
|
||||
if ton {
|
||||
zeroAmount = &tg.StarsTonAmount{}
|
||||
}
|
||||
return &tg.PaymentsStarsRevenueStats{
|
||||
RevenueGraph: &tg.StatsGraphError{Error: "Not enough data to display."},
|
||||
Status: tg.StarsRevenueStatus{
|
||||
CurrentBalance: zeroAmount,
|
||||
AvailableBalance: zeroAmount,
|
||||
OverallRevenue: zeroAmount,
|
||||
WithdrawalEnabled: false,
|
||||
},
|
||||
UsdRate: 0.013,
|
||||
}
|
||||
}
|
||||
|
||||
func AiComposeTones() tg.AicomposeTonesClass {
|
||||
return &tg.AicomposeTonesNotModified{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package tdesktop
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
|
@ -32,6 +33,48 @@ func TestTimezonesListIsNonEmptyAndHashable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAppConfigIncludesStoryStealthPeriods(t *testing.T) {
|
||||
got, ok := AppConfig(0).(*tg.HelpAppConfig)
|
||||
if !ok || got.Hash == 0 {
|
||||
t.Fatalf("AppConfig(0) = %#v, want modified config with hash", got)
|
||||
}
|
||||
values := make(map[string]float64)
|
||||
if object, ok := got.Config.(*tg.JSONObject); ok && object != nil {
|
||||
for _, entry := range object.Value {
|
||||
if number, ok := entry.Value.(*tg.JSONNumber); ok {
|
||||
values[entry.Key] = number.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
want := map[string]float64{
|
||||
"stories_stealth_future_period": 1500,
|
||||
"stories_stealth_past_period": 300,
|
||||
"stories_stealth_cooldown_period": 10800,
|
||||
}
|
||||
for key, expected := range want {
|
||||
if values[key] != expected {
|
||||
t.Fatalf("AppConfig[%q] = %v, want %v", key, values[key], expected)
|
||||
}
|
||||
}
|
||||
if _, ok := AppConfig(got.Hash).(*tg.HelpAppConfigNotModified); !ok {
|
||||
t.Fatalf("AppConfig(hash) = %#v, want notModified", AppConfig(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackAppConfigOmitsMapboxToken(t *testing.T) {
|
||||
got, ok := AppConfig(0).(*tg.HelpAppConfig)
|
||||
if !ok {
|
||||
t.Fatalf("AppConfig(0) = %T, want modified config", got)
|
||||
}
|
||||
if object, ok := got.Config.(*tg.JSONObject); ok && object != nil {
|
||||
for _, entry := range object.Value {
|
||||
if entry.Key == "tdesktop_config_map" {
|
||||
t.Fatal("fallback AppConfig leaked tdesktop_config_map without runtime token")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableReactionsCatalogIsNonEmptyAndHashable(t *testing.T) {
|
||||
got, ok := AvailableReactions(0).(*tg.MessagesAvailableReactions)
|
||||
if !ok {
|
||||
|
|
@ -66,6 +109,195 @@ func TestAvailableReactionsCatalogIsNonEmptyAndHashable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChatThemesCatalogIsNonEmptyHashableAndHasQRBackgroundColors(t *testing.T) {
|
||||
got, ok := ChatThemes(0).(*tg.AccountThemes)
|
||||
if !ok {
|
||||
t.Fatalf("ChatThemes(0) = %T, want modified list", got)
|
||||
}
|
||||
if got.Hash == 0 {
|
||||
t.Fatal("ChatThemes(0).Hash = 0, want stable cache hash")
|
||||
}
|
||||
if len(got.Themes) == 0 {
|
||||
t.Fatal("ChatThemes(0).Themes is empty")
|
||||
}
|
||||
for i, theme := range got.Themes {
|
||||
if !theme.GetForChat() {
|
||||
t.Fatalf("theme[%d].for_chat = false, want true", i)
|
||||
}
|
||||
if emoticon, ok := theme.GetEmoticon(); !ok || emoticon == "" || !IsChatThemeEmoticon(emoticon) {
|
||||
t.Fatalf("theme[%d].emoticon = %q ok=%v, want supported token", i, emoticon, ok)
|
||||
}
|
||||
settings, ok := theme.GetSettings()
|
||||
// DrKLO only surfaces an emoji chat theme in the picker when it ships
|
||||
// settings for at least four base themes
|
||||
// (MediaDataController.generateEmojiPreviewThemes drops items.size() < 4,
|
||||
// one item per ThemeSettings); fewer leaves the strip stuck on its
|
||||
// loading shimmer.
|
||||
if !ok || len(settings) < 4 {
|
||||
t.Fatalf("theme[%d].settings = %#v ok=%v, want >=4 base-theme settings", i, settings, ok)
|
||||
}
|
||||
var hasDay, hasNight bool
|
||||
for j, setting := range settings {
|
||||
if setting.BaseTheme == nil {
|
||||
t.Fatalf("theme[%d].settings[%d].base_theme missing", i, j)
|
||||
}
|
||||
switch setting.BaseTheme.(type) {
|
||||
case *tg.BaseThemeDay:
|
||||
hasDay = true
|
||||
case *tg.BaseThemeNight:
|
||||
hasNight = true
|
||||
}
|
||||
// 官方聊天主题用文档型主题背景墙纸(非旧的纯渐变 WallPaperNoFile),
|
||||
// 这里只要求每组设置带一个墙纸。
|
||||
if wallpaper, ok := setting.GetWallpaper(); !ok || wallpaper == nil {
|
||||
t.Fatalf("theme[%d].settings[%d].wallpaper = %#v ok=%v, want wallpaper", i, j, wallpaper, ok)
|
||||
}
|
||||
}
|
||||
if !hasDay || !hasNight {
|
||||
t.Fatalf("theme[%d] settings missing day/night coverage (day=%v night=%v)", i, hasDay, hasNight)
|
||||
}
|
||||
}
|
||||
if _, ok := ChatThemes(got.Hash).(*tg.AccountThemesNotModified); !ok {
|
||||
t.Fatalf("ChatThemes(hash) = %#v, want notModified", ChatThemes(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultThemesAreDefaultFlaggedForPicker(t *testing.T) {
|
||||
// account.getThemes feeds DrKLO's "Color theme" preview strip
|
||||
// (DefaultThemesPreviewCell), which only consumes themes flagged
|
||||
// is_default=true. Returning themesNotModified to a fresh client (hash 0)
|
||||
// leaves the strip stuck on a loading shimmer, so a fresh request must
|
||||
// return a populated, default-flagged catalog.
|
||||
got, ok := DefaultThemes(0).(*tg.AccountThemes)
|
||||
if !ok {
|
||||
t.Fatalf("DefaultThemes(0) = %T, want modified list", got)
|
||||
}
|
||||
if got.Hash == 0 {
|
||||
t.Fatal("DefaultThemes(0).Hash = 0, want stable cache hash")
|
||||
}
|
||||
if len(got.Themes) == 0 {
|
||||
t.Fatal("DefaultThemes(0).Themes is empty")
|
||||
}
|
||||
for i, theme := range got.Themes {
|
||||
if !theme.GetDefault() {
|
||||
t.Fatalf("theme[%d].is_default = false, want true so the picker shows it", i)
|
||||
}
|
||||
if !theme.GetForChat() {
|
||||
t.Fatalf("theme[%d].for_chat = false, want true", i)
|
||||
}
|
||||
if settings, ok := theme.GetSettings(); !ok || len(settings) < 4 {
|
||||
t.Fatalf("theme[%d].settings len=%d ok=%v, want >=4 for the picker", i, len(settings), ok)
|
||||
}
|
||||
}
|
||||
if _, ok := DefaultThemes(got.Hash).(*tg.AccountThemesNotModified); !ok {
|
||||
t.Fatalf("DefaultThemes(hash) = %#v, want notModified", DefaultThemes(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueGiftChatThemesIsEmptyHashableStub(t *testing.T) {
|
||||
got, ok := UniqueGiftChatThemes(0).(*tg.AccountChatThemes)
|
||||
if !ok {
|
||||
t.Fatalf("UniqueGiftChatThemes(0) = %T, want modified list", got)
|
||||
}
|
||||
if got.Hash == 0 {
|
||||
t.Fatal("UniqueGiftChatThemes(0).Hash = 0, want stable cache hash")
|
||||
}
|
||||
if len(got.Themes) != 0 || len(got.Chats) != 0 || len(got.Users) != 0 {
|
||||
t.Fatalf("UniqueGiftChatThemes(0) = themes %d chats %d users %d, want empty vectors",
|
||||
len(got.Themes), len(got.Chats), len(got.Users))
|
||||
}
|
||||
if _, ok := UniqueGiftChatThemes(got.Hash).(*tg.AccountChatThemesNotModified); !ok {
|
||||
t.Fatalf("UniqueGiftChatThemes(hash) = %#v, want notModified", UniqueGiftChatThemes(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWallPapersUsesOrangeFileCatalog(t *testing.T) {
|
||||
got, ok := WallPapers(0).(*tg.AccountWallPapers)
|
||||
if !ok {
|
||||
t.Fatalf("WallPapers(0) = %T, want modified list", got)
|
||||
}
|
||||
if got.Hash == 0 {
|
||||
t.Fatal("WallPapers(0).Hash = 0, want stable cache hash")
|
||||
}
|
||||
if len(got.Wallpapers) == 0 {
|
||||
t.Fatal("WallPapers(0).Wallpapers is empty")
|
||||
}
|
||||
wallpaper, ok := got.Wallpapers[0].(*tg.WallPaper)
|
||||
if !ok {
|
||||
t.Fatalf("WallPapers(0).Wallpapers[0] = %T, want *tg.WallPaper", got.Wallpapers[0])
|
||||
}
|
||||
if wallpaper.ID == 0 || wallpaper.AccessHash == 0 || wallpaper.Slug == "" {
|
||||
t.Fatalf("wallpaper identity = id %d hash %d slug %q, want seed ids", wallpaper.ID, wallpaper.AccessHash, wallpaper.Slug)
|
||||
}
|
||||
doc, ok := wallpaper.Document.(*tg.Document)
|
||||
if !ok {
|
||||
t.Fatalf("wallpaper document = %T, want *tg.Document", wallpaper.Document)
|
||||
}
|
||||
if doc.ID == 0 || doc.AccessHash == 0 || doc.Size == 0 || doc.MimeType == "" || doc.DCID != appearanceSeedDCID {
|
||||
t.Fatalf("wallpaper document = id %d hash %d size %d mime %q dc %d, want downloadable seed document",
|
||||
doc.ID, doc.AccessHash, doc.Size, doc.MimeType, doc.DCID)
|
||||
}
|
||||
if len(doc.Thumbs) == 0 {
|
||||
t.Fatal("wallpaper document has no thumbnails")
|
||||
}
|
||||
if _, ok := doc.Thumbs[0].(*tg.PhotoSize); !ok {
|
||||
t.Fatalf("wallpaper thumb = %T, want *tg.PhotoSize", doc.Thumbs[0])
|
||||
}
|
||||
if _, ok := WallPapers(got.Hash).(*tg.AccountWallPapersNotModified); !ok {
|
||||
t.Fatalf("WallPapers(hash) = %#v, want notModified", WallPapers(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupWallPaperByIDAndSlug(t *testing.T) {
|
||||
catalog := WallPapers(0).(*tg.AccountWallPapers)
|
||||
first := catalog.Wallpapers[0].(*tg.WallPaper)
|
||||
byID, ok := LookupWallPaper(&tg.InputWallPaper{ID: first.ID, AccessHash: first.AccessHash})
|
||||
if !ok {
|
||||
t.Fatal("LookupWallPaper(inputWallPaper) = false, want true")
|
||||
}
|
||||
if got := byID.(*tg.WallPaper).Slug; got != first.Slug {
|
||||
t.Fatalf("LookupWallPaper(inputWallPaper).Slug = %q, want %q", got, first.Slug)
|
||||
}
|
||||
bySlug, ok := LookupWallPaper(&tg.InputWallPaperSlug{Slug: first.Slug})
|
||||
if !ok {
|
||||
t.Fatal("LookupWallPaper(inputWallPaperSlug) = false, want true")
|
||||
}
|
||||
if got := bySlug.(*tg.WallPaper).ID; got != first.ID {
|
||||
t.Fatalf("LookupWallPaper(inputWallPaperSlug).ID = %d, want %d", got, first.ID)
|
||||
}
|
||||
if _, ok := LookupWallPaper(&tg.InputWallPaper{ID: first.ID, AccessHash: first.AccessHash + 1}); ok {
|
||||
t.Fatal("LookupWallPaper(wrong access hash) = true, want false")
|
||||
}
|
||||
multi, ok := LookupWallPapers([]tg.InputWallPaperClass{
|
||||
&tg.InputWallPaper{ID: first.ID, AccessHash: first.AccessHash},
|
||||
&tg.InputWallPaperSlug{Slug: catalog.Wallpapers[1].(*tg.WallPaper).Slug},
|
||||
})
|
||||
if !ok || len(multi) != 2 {
|
||||
t.Fatalf("LookupWallPapers = len %d ok %v, want 2 true", len(multi), ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarsRevenueStatsIsZeroBalanceCompatStub(t *testing.T) {
|
||||
got := StarsRevenueStats(false)
|
||||
if got == nil {
|
||||
t.Fatal("StarsRevenueStats(false) = nil")
|
||||
}
|
||||
if _, ok := got.RevenueGraph.(*tg.StatsGraphError); !ok {
|
||||
t.Fatalf("RevenueGraph = %T, want *tg.StatsGraphError", got.RevenueGraph)
|
||||
}
|
||||
if got.Status.WithdrawalEnabled {
|
||||
t.Fatal("WithdrawalEnabled = true, want false")
|
||||
}
|
||||
if _, ok := got.Status.CurrentBalance.(*tg.StarsAmount); !ok {
|
||||
t.Fatalf("CurrentBalance = %T, want *tg.StarsAmount", got.Status.CurrentBalance)
|
||||
}
|
||||
|
||||
ton := StarsRevenueStats(true)
|
||||
if _, ok := ton.Status.CurrentBalance.(*tg.StarsTonAmount); !ok {
|
||||
t.Fatalf("TON CurrentBalance = %T, want *tg.StarsTonAmount", ton.Status.CurrentBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectibleEmojiStatusesIsEmptyModifiedList(t *testing.T) {
|
||||
got, ok := CollectibleEmojiStatuses().(*tg.AccountEmojiStatuses)
|
||||
if !ok {
|
||||
|
|
@ -105,13 +337,169 @@ func TestEmojiProfilePhotoGroupsIsEmptyModifiedList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConnectedBotsIsEmptyList(t *testing.T) {
|
||||
got := ConnectedBots()
|
||||
if got == nil {
|
||||
t.Fatal("ConnectedBots() = nil")
|
||||
func TestEmojiStatusGroupsIsEmptyModifiedList(t *testing.T) {
|
||||
got, ok := EmojiStatusGroups().(*tg.MessagesEmojiGroups)
|
||||
if !ok {
|
||||
t.Fatalf("EmojiStatusGroups() = %T, want empty modified list", got)
|
||||
}
|
||||
if len(got.ConnectedBots) != 0 || len(got.Users) != 0 {
|
||||
t.Fatalf("ConnectedBots() = bots %d users %d, want empty vectors", len(got.ConnectedBots), len(got.Users))
|
||||
if got.Hash != 0 {
|
||||
t.Fatalf("EmojiStatusGroups().Hash = %d, want 0", got.Hash)
|
||||
}
|
||||
if len(got.Groups) != 0 {
|
||||
t.Fatalf("EmojiStatusGroups().Groups length = %d, want 0", len(got.Groups))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuickRepliesIsEmptyModifiedList(t *testing.T) {
|
||||
got, ok := QuickReplies().(*tg.MessagesQuickReplies)
|
||||
if !ok {
|
||||
t.Fatalf("QuickReplies() = %T, want empty modified list", got)
|
||||
}
|
||||
if len(got.QuickReplies) != 0 || len(got.Messages) != 0 || len(got.Chats) != 0 || len(got.Users) != 0 {
|
||||
t.Fatalf("QuickReplies() = shortcuts %d messages %d chats %d users %d, want empty vectors",
|
||||
len(got.QuickReplies), len(got.Messages), len(got.Chats), len(got.Users))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerColorsAreNonEmptyHashableAccentSets(t *testing.T) {
|
||||
got, ok := PeerColors(0).(*tg.HelpPeerColors)
|
||||
if !ok {
|
||||
t.Fatalf("PeerColors(0) = %T, want modified colors", got)
|
||||
}
|
||||
if got.Hash == 0 || len(got.Colors) == 0 {
|
||||
t.Fatalf("PeerColors(0) = hash %d colors %d, want non-empty stable list", got.Hash, len(got.Colors))
|
||||
}
|
||||
if len(got.Colors) != 21 {
|
||||
t.Fatalf("PeerColors(0).Colors length = %d, want seed palette count 21", len(got.Colors))
|
||||
}
|
||||
withExplicitColors := 0
|
||||
for i, option := range got.Colors {
|
||||
if !IsPeerColorID(option.ColorID) {
|
||||
t.Fatalf("PeerColors()[%d].ColorID = %d, want supported id", i, option.ColorID)
|
||||
}
|
||||
colors, ok := option.GetColors()
|
||||
if !ok {
|
||||
if option.ColorID > 6 {
|
||||
t.Fatalf("PeerColors()[%d].Colors missing for non-default id %d", i, option.ColorID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := colors.(*tg.HelpPeerColorSet); !ok {
|
||||
t.Fatalf("PeerColors()[%d].Colors = %T, want *tg.HelpPeerColorSet", i, colors)
|
||||
}
|
||||
withExplicitColors++
|
||||
}
|
||||
if withExplicitColors == 0 {
|
||||
t.Fatal("PeerColors() has no explicit seed color sets")
|
||||
}
|
||||
if _, ok := PeerColors(got.Hash).(*tg.HelpPeerColorsNotModified); !ok {
|
||||
t.Fatalf("PeerColors(hash) = %#v, want notModified", PeerColors(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerProfileColorsAreNonEmptyHashableProfileSets(t *testing.T) {
|
||||
got, ok := PeerProfileColors(0).(*tg.HelpPeerColors)
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors(0) = %T, want modified colors", got)
|
||||
}
|
||||
if got.Hash == 0 || len(got.Colors) == 0 {
|
||||
t.Fatalf("PeerProfileColors(0) = hash %d colors %d, want non-empty stable list", got.Hash, len(got.Colors))
|
||||
}
|
||||
if len(got.Colors) != 16 {
|
||||
t.Fatalf("PeerProfileColors(0).Colors length = %d, want seed profile palette count 16", len(got.Colors))
|
||||
}
|
||||
for i, option := range got.Colors {
|
||||
if !IsPeerProfileColorID(option.ColorID) {
|
||||
t.Fatalf("PeerProfileColors()[%d].ColorID = %d, want supported id", i, option.ColorID)
|
||||
}
|
||||
groupMin, ok := option.GetGroupMinLevel()
|
||||
if !ok || groupMin <= 0 || groupMin > maxPeerColorBoostLevel {
|
||||
t.Fatalf("PeerProfileColors()[%d].group_min_level = %d ok %v, want bounded positive level", i, groupMin, ok)
|
||||
}
|
||||
if channelMin, ok := option.GetChannelMinLevel(); ok && (channelMin <= 0 || channelMin > maxPeerColorBoostLevel) {
|
||||
t.Fatalf("PeerProfileColors()[%d].channel_min_level = %d, want bounded positive level", i, channelMin)
|
||||
}
|
||||
colors, ok := option.GetColors()
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors()[%d].Colors missing", i)
|
||||
}
|
||||
profile, ok := colors.(*tg.HelpPeerColorProfileSet)
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors()[%d].Colors = %T, want *tg.HelpPeerColorProfileSet", i, colors)
|
||||
}
|
||||
if len(profile.PaletteColors) == 0 || len(profile.BgColors) == 0 || len(profile.StoryColors) != 2 {
|
||||
t.Fatalf("PeerProfileColors()[%d] = palette %d bg %d story %d, want usable profile palette",
|
||||
i, len(profile.PaletteColors), len(profile.BgColors), len(profile.StoryColors))
|
||||
}
|
||||
}
|
||||
if _, ok := PeerProfileColors(got.Hash).(*tg.HelpPeerColorsNotModified); !ok {
|
||||
t.Fatalf("PeerProfileColors(hash) = %#v, want notModified", PeerProfileColors(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerProfileColorsKeepTDesktopBoostFeatureLevelsBounded(t *testing.T) {
|
||||
got, ok := PeerProfileColors(0).(*tg.HelpPeerColors)
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors(0) = %T, want modified colors", got)
|
||||
}
|
||||
levels := make([]int, 0, len(got.Colors))
|
||||
lowestNonZeroLevel := math.MaxInt
|
||||
for _, option := range got.Colors {
|
||||
level, ok := option.GetGroupMinLevel()
|
||||
if !ok {
|
||||
level = 0
|
||||
}
|
||||
levels = append(levels, level)
|
||||
if level != 0 && level < lowestNonZeroLevel {
|
||||
lowestNonZeroLevel = level
|
||||
}
|
||||
}
|
||||
if lowestNonZeroLevel == math.MaxInt {
|
||||
t.Fatal("profile color group levels are all zero; TDesktop would aggregate them at MaxInt in BoostBox")
|
||||
}
|
||||
maxFeatureLevel := 0
|
||||
for _, level := range levels {
|
||||
if level < lowestNonZeroLevel {
|
||||
level = lowestNonZeroLevel
|
||||
}
|
||||
if level > maxFeatureLevel {
|
||||
maxFeatureLevel = level
|
||||
}
|
||||
}
|
||||
if maxFeatureLevel <= 0 || maxFeatureLevel > maxPeerColorBoostLevel {
|
||||
t.Fatalf("TDesktop profile feature max level = %d, want 1..%d", maxFeatureLevel, maxPeerColorBoostLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerProfileColorsReturnsIndependentOptions(t *testing.T) {
|
||||
first, ok := PeerProfileColors(0).(*tg.HelpPeerColors)
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors(0) first = %T, want modified colors", first)
|
||||
}
|
||||
second, ok := PeerProfileColors(0).(*tg.HelpPeerColors)
|
||||
if !ok {
|
||||
t.Fatalf("PeerProfileColors(0) second = %T, want modified colors", second)
|
||||
}
|
||||
if len(first.Colors) == 0 || len(second.Colors) == 0 {
|
||||
t.Fatal("PeerProfileColors returned empty colors")
|
||||
}
|
||||
first.Colors[0].SetGroupMinLevel(maxPeerColorBoostLevel + 1)
|
||||
if groupMin, ok := second.Colors[0].GetGroupMinLevel(); !ok || groupMin > maxPeerColorBoostLevel {
|
||||
t.Fatalf("second PeerProfileColors group_min_level = %d ok %v, want independent bounded copy", groupMin, ok)
|
||||
}
|
||||
|
||||
firstSet, ok := first.Colors[0].Colors.(*tg.HelpPeerColorProfileSet)
|
||||
if !ok || len(firstSet.PaletteColors) == 0 {
|
||||
t.Fatalf("first profile colors = %T %+v, want profile set with palette", first.Colors[0].Colors, first.Colors[0].Colors)
|
||||
}
|
||||
secondSet, ok := second.Colors[0].Colors.(*tg.HelpPeerColorProfileSet)
|
||||
if !ok || len(secondSet.PaletteColors) == 0 {
|
||||
t.Fatalf("second profile colors = %T %+v, want profile set with palette", second.Colors[0].Colors, second.Colors[0].Colors)
|
||||
}
|
||||
original := secondSet.PaletteColors[0]
|
||||
firstSet.PaletteColors[0] = original + 1
|
||||
if secondSet.PaletteColors[0] != original {
|
||||
t.Fatalf("second profile palette mutated through first response: got %d want %d", secondSet.PaletteColors[0], original)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue