feat: sync visible product branding
Sync telesrv c779c48 (feat(branding): unify visible product naming). Skipped private www files and preserved public README files; mapped orange appearance seed names to public default names.
This commit is contained in:
parent
da6a57e1a3
commit
2848ff0987
34 changed files with 449 additions and 91 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
// Command appearancefetch 从官方 Telegram 拉取墙纸 + 聊天主题,下载文档/缩略图,
|
// Command appearancefetch 从官方 Telegram 拉取墙纸 + 聊天主题,下载文档/缩略图,
|
||||||
// 生成 telesrv 外观 seed(Default_appearance_seed.json + Default_wallpapers/{documents,thumbs/m}/*.dat)。
|
// 生成 telesrv 外观 seed(default_appearance_seed.json + default_wallpapers/{documents,thumbs/m}/*.dat)。
|
||||||
// 复用 internal/seed/appearance 的结构体保证 schema 完全一致。peer_colors 从现有 JSON 沿用。
|
// 复用 internal/seed/appearance 的结构体保证 schema 完全一致。peer_colors 从现有 JSON 沿用。
|
||||||
//
|
//
|
||||||
// 需登录(墙纸/主题接口非免登)。api 凭据用 TDesktop 开源公开的 id/hash。
|
// 需登录(墙纸/主题接口非免登)。api 凭据用 TDesktop 开源公开的 id/hash。
|
||||||
|
|
@ -114,8 +114,8 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
api := client.API()
|
api := client.API()
|
||||||
docsDir := filepath.Join(outDir, "Default_wallpapers", "documents")
|
docsDir := filepath.Join(outDir, "default_wallpapers", "documents")
|
||||||
thumbsDir := filepath.Join(outDir, "Default_wallpapers", "thumbs", "m")
|
thumbsDir := filepath.Join(outDir, "default_wallpapers", "thumbs", "m")
|
||||||
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
||||||
return appearance.Document{}, err
|
return appearance.Document{}, err
|
||||||
}
|
}
|
||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
out.Path = "Default_wallpapers/documents/" + name
|
out.Path = "default_wallpapers/documents/" + name
|
||||||
out.SHA256 = hex.EncodeToString(sum[:])
|
out.SHA256 = hex.EncodeToString(sum[:])
|
||||||
// "m" 缩略图
|
// "m" 缩略图
|
||||||
for _, t := range doc.Thumbs {
|
for _, t := range doc.Thumbs {
|
||||||
|
|
@ -187,7 +187,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
||||||
tsum := sha256.Sum256(tdata)
|
tsum := sha256.Sum256(tdata)
|
||||||
out.Thumbs = append(out.Thumbs, appearance.PhotoSize{
|
out.Thumbs = append(out.Thumbs, appearance.PhotoSize{
|
||||||
Kind: "size", Type: "m", W: ps.W, H: ps.H, Size: ps.Size,
|
Kind: "size", Type: "m", W: ps.W, H: ps.H, Size: ps.Size,
|
||||||
Path: "Default_wallpapers/thumbs/m/" + name, SHA256: hex.EncodeToString(tsum[:]),
|
Path: "default_wallpapers/thumbs/m/" + name, SHA256: hex.EncodeToString(tsum[:]),
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +373,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
||||||
fmt.Printf("[peer_colors] %d / [peer_profile_colors] %d\n", len(peerColors), len(peerProfileColors))
|
fmt.Printf("[peer_colors] %d / [peer_profile_colors] %d\n", len(peerColors), len(peerProfileColors))
|
||||||
|
|
||||||
catalog := appearance.Catalog{
|
catalog := appearance.Catalog{
|
||||||
Source: "official telegram (appearancefetch)",
|
Source: "upstream appearance snapshot",
|
||||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
Notes: appearance.Notes{
|
Notes: appearance.Notes{
|
||||||
Server: "official",
|
Server: "official",
|
||||||
|
|
@ -390,7 +390,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
jsonPath := filepath.Join(outDir, "Default_appearance_seed.json")
|
jsonPath := filepath.Join(outDir, "default_appearance_seed.json")
|
||||||
if err := os.WriteFile(jsonPath, out, 0o644); err != nil {
|
if err := os.WriteFile(jsonPath, out, 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
2
cmd/telesrv-admin/web/dist/index.html
vendored
2
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>telesrv admin</title>
|
<title>telesrv admin</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DKmJO2ZY.js"></script>
|
<script type="module" crossorigin src="/assets/index-D9dH2J7N.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||||
"collectibles.backdrop": "Backdrop",
|
"collectibles.backdrop": "Backdrop",
|
||||||
"collectibles.rarity": "Rarity ‰",
|
"collectibles.rarity": "Rarity ‰",
|
||||||
"collectibles.rarityHint": "Permille values are relative regular-upgrade weights; their total does not need to equal 1000.",
|
"collectibles.rarityHint": "Permille values are relative regular-upgrade weights; their total does not need to equal 1000.",
|
||||||
"collectibles.colorHint": "Colors are stored as Telegram 24-bit RGB values.",
|
"collectibles.colorHint": "Colors are stored as 24-bit RGB values.",
|
||||||
"collectibles.addAttribute": "Add",
|
"collectibles.addAttribute": "Add",
|
||||||
"collectibles.remove": "Remove attribute",
|
"collectibles.remove": "Remove attribute",
|
||||||
"collectibles.fileRequired": "Every model and pattern needs a TGS or Lottie file.",
|
"collectibles.fileRequired": "Every model and pattern needs a TGS or Lottie file.",
|
||||||
|
|
@ -698,7 +698,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||||
"collectibles.backdrop": "背景",
|
"collectibles.backdrop": "背景",
|
||||||
"collectibles.rarity": "稀有度 ‰",
|
"collectibles.rarity": "稀有度 ‰",
|
||||||
"collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。",
|
"collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。",
|
||||||
"collectibles.colorHint": "颜色会按 Telegram 24 位 RGB 数值保存。",
|
"collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。",
|
||||||
"collectibles.addAttribute": "添加",
|
"collectibles.addAttribute": "添加",
|
||||||
"collectibles.remove": "删除属性",
|
"collectibles.remove": "删除属性",
|
||||||
"collectibles.fileRequired": "每个模型和图案都必须选择 TGS 或 Lottie 文件。",
|
"collectibles.fileRequired": "每个模型和图案都必须选择 TGS 或 Lottie 文件。",
|
||||||
|
|
@ -1068,7 +1068,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||||
"collectibles.backdrop": "Фон",
|
"collectibles.backdrop": "Фон",
|
||||||
"collectibles.rarity": "Редкость ‰",
|
"collectibles.rarity": "Редкость ‰",
|
||||||
"collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.",
|
"collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.",
|
||||||
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения Telegram.",
|
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
|
||||||
"collectibles.addAttribute": "Добавить",
|
"collectibles.addAttribute": "Добавить",
|
||||||
"collectibles.remove": "Удалить атрибут",
|
"collectibles.remove": "Удалить атрибут",
|
||||||
"collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.",
|
"collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.",
|
||||||
|
|
|
||||||
|
|
@ -495,7 +495,7 @@ func run(logger *zap.Logger) error {
|
||||||
cfg.UploadPartGCInterval,
|
cfg.UploadPartGCInterval,
|
||||||
cfg.UploadPartGCBatch,
|
cfg.UploadPartGCBatch,
|
||||||
).Run(ctx)
|
).Run(ctx)
|
||||||
langPackService := langpack.NewService(langPackStore)
|
langPackService := langpack.NewService(langPackStore, langpack.WithPublicBaseURL(cfg.PublicBaseURL))
|
||||||
privacyService := privacyapp.NewService(privacyStore, contactStore)
|
privacyService := privacyapp.NewService(privacyStore, contactStore)
|
||||||
contactsService := contacts.NewService(contactStore, userStore).Configure(
|
contactsService := contacts.NewService(contactStore, userStore).Configure(
|
||||||
contacts.WithPhotoProvider(cachedPhotos),
|
contacts.WithPhotoProvider(cachedPhotos),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ type appServer struct {
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config{}
|
cfg := config{}
|
||||||
flag.StringVar(&cfg.listen, "listen", envOr("TELESRV_WALLET_LISTEN", "127.0.0.1:8091"), "wallet mini app HTTP listen address")
|
flag.StringVar(&cfg.listen, "listen", envOr("TELESRV_WALLET_LISTEN", "127.0.0.1:8091"), "wallet mini app HTTP listen address")
|
||||||
flag.StringVar(&cfg.publicURL, "public-url", os.Getenv("TELESRV_WALLET_PUBLIC_URL"), "public HTTPS URL used in the Telegram menu button")
|
flag.StringVar(&cfg.publicURL, "public-url", os.Getenv("TELESRV_WALLET_PUBLIC_URL"), "public HTTPS URL used in the Telesrv menu button")
|
||||||
flag.StringVar(&cfg.botAPI, "bot-api", envOr("TELESRV_BOT_API_URL", "http://127.0.0.1:8081"), "telesrv Bot API base URL")
|
flag.StringVar(&cfg.botAPI, "bot-api", envOr("TELESRV_BOT_API_URL", "http://127.0.0.1:8081"), "telesrv Bot API base URL")
|
||||||
flag.StringVar(&cfg.token, "token", os.Getenv("TELESRV_BOT_TOKEN"), "bot token <bot_id>:<secret>")
|
flag.StringVar(&cfg.token, "token", os.Getenv("TELESRV_BOT_TOKEN"), "bot token <bot_id>:<secret>")
|
||||||
flag.StringVar(&cfg.menuText, "menu-text", envOr("TELESRV_WALLET_MENU_TEXT", "Wallet"), "menu button label")
|
flag.StringVar(&cfg.menuText, "menu-text", envOr("TELESRV_WALLET_MENU_TEXT", "Wallet"), "menu button label")
|
||||||
|
|
|
||||||
|
|
@ -5554,7 +5554,7 @@ SET row_security = off;
|
||||||
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: -
|
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO public.users (id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id) VALUES (777000, 6599886787491911851, '42777', 'Telegram', '', 'telegram', '', '2026-06-19 13:35:51.253491+00', '2026-06-19 13:35:51.253491+00', true, true, '', 0, 0, false, 0, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
INSERT INTO public.users (id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id) VALUES (777000, 6599886787491911851, '42777', 'Telesrv', '', 'telesrv', '', '2026-06-19 13:35:51.253491+00', '2026-06-19 13:35:51.253491+00', true, true, '', 0, 0, false, 0, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
||||||
INSERT INTO public.users (id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id) VALUES (93372553, 7421896403922962293, '', 'BotFather', '', 'BotFather', '', '2026-06-19 13:35:52.688367+00', '2026-06-19 13:35:52.688367+00', true, false, '', 0, 0, true, 1, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
INSERT INTO public.users (id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id) VALUES (93372553, 7421896403922962293, '', 'BotFather', '', 'BotFather', '', '2026-06-19 13:35:52.688367+00', '2026-06-19 13:35:52.688367+00', true, false, '', 0, 0, true, 1, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/otpdelivery"
|
"telesrv/internal/otpdelivery"
|
||||||
"telesrv/internal/store"
|
"telesrv/internal/store"
|
||||||
|
|
@ -79,7 +80,7 @@ func (s *Service) DeleteAccount(ctx context.Context, userID int64, authKeyID [8]
|
||||||
}
|
}
|
||||||
executeAt := now.Add(accountDeletionDelay)
|
executeAt := now.Add(accountDeletionDelay)
|
||||||
message := fmt.Sprintf(
|
message := fmt.Sprintf(
|
||||||
"A request was made to delete your Telegram account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s",
|
"A request was made to delete your "+branding.ProductName+" account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s",
|
||||||
url.QueryEscape(snapshot.User.Phone), url.QueryEscape(rawToken),
|
url.QueryEscape(snapshot.User.Phone), url.QueryEscape(rawToken),
|
||||||
)
|
)
|
||||||
pending, _, err := s.lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{
|
pending, _, err := s.lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/bin"
|
"github.com/iamxvbaba/td/bin"
|
||||||
mtcrypto "github.com/iamxvbaba/td/crypto"
|
mtcrypto "github.com/iamxvbaba/td/crypto"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/otpdelivery"
|
"telesrv/internal/otpdelivery"
|
||||||
"telesrv/internal/store"
|
"telesrv/internal/store"
|
||||||
|
|
@ -1435,9 +1436,9 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error
|
||||||
return found && settings.HasPassword, nil
|
return found && settings.HasPassword, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from Telegram!
|
const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||||
|
|
||||||
This code can be used to log in to your Telegram account. We never ask it for anything else.
|
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
|
||||||
|
|
||||||
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ const (
|
||||||
botFatherDraftBotUsername = "bot_username"
|
botFatherDraftBotUsername = "bot_username"
|
||||||
)
|
)
|
||||||
|
|
||||||
const botFatherHelpText = `I can help you create and manage Telegram bots.
|
const botFatherHelpText = `I can help you create and manage ` + branding.ProductName + ` bots.
|
||||||
|
|
||||||
You can control me by sending these commands:
|
You can control me by sending these commands:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/seed/appearance"
|
"telesrv/internal/seed/appearance"
|
||||||
)
|
)
|
||||||
|
|
@ -253,7 +254,7 @@ func appearanceDocumentAttributes(in []appearance.DocumentAttribute) []domain.Do
|
||||||
if attr.FileName != "" {
|
if attr.FileName != "" {
|
||||||
out = append(out, domain.DocumentAttribute{
|
out = append(out, domain.DocumentAttribute{
|
||||||
Kind: domain.DocAttrFilename,
|
Kind: domain.DocAttrFilename,
|
||||||
FileName: attr.FileName,
|
FileName: branding.UserVisibleText(attr.FileName, ""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
"golang.org/x/text/unicode/bidi"
|
"golang.org/x/text/unicode/bidi"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/store"
|
"telesrv/internal/store"
|
||||||
)
|
)
|
||||||
|
|
@ -18,16 +19,34 @@ type Service struct {
|
||||||
languageCache *languageListCache
|
languageCache *languageListCache
|
||||||
packLoads singleflight.Group
|
packLoads singleflight.Group
|
||||||
languageLoads singleflight.Group
|
languageLoads singleflight.Group
|
||||||
|
publicBaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option configures user-visible language-pack projection.
|
||||||
|
type Option func(*Service)
|
||||||
|
|
||||||
|
// WithPublicBaseURL replaces official public hosts embedded in upstream
|
||||||
|
// language-pack values with this deployment's public link root.
|
||||||
|
func WithPublicBaseURL(value string) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
s.publicBaseURL = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService 创建 langpack 服务。
|
// NewService 创建 langpack 服务。
|
||||||
func NewService(packs store.LangPackStore) *Service {
|
func NewService(packs store.LangPackStore, opts ...Option) *Service {
|
||||||
return newServiceWithCacheLimits(
|
s := newServiceWithCacheLimits(
|
||||||
packs,
|
packs,
|
||||||
defaultLangPackCacheMaxBytes,
|
defaultLangPackCacheMaxBytes,
|
||||||
defaultLangPackCacheMaxEntries,
|
defaultLangPackCacheMaxEntries,
|
||||||
defaultLanguageListCacheMaxEntries,
|
defaultLanguageListCacheMaxEntries,
|
||||||
)
|
)
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEntries, languageEntries int) *Service {
|
func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEntries, languageEntries int) *Service {
|
||||||
|
|
@ -35,6 +54,7 @@ func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEnt
|
||||||
packs: packs,
|
packs: packs,
|
||||||
packCache: newLangPackCache(maxBytes, maxEntries),
|
packCache: newLangPackCache(maxBytes, maxEntries),
|
||||||
languageCache: newLanguageListCache(languageEntries),
|
languageCache: newLanguageListCache(languageEntries),
|
||||||
|
publicBaseURL: branding.DefaultPublicURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,7 +155,11 @@ func shouldOverlayWebA(langPack string) bool {
|
||||||
func (s *Service) rawPack(ctx context.Context, langPack, langCode string) (domain.LangPack, error) {
|
func (s *Service) rawPack(ctx context.Context, langPack, langCode string) (domain.LangPack, error) {
|
||||||
key := langPackCacheKey{pack: langPack, code: langCode, kind: langPackCacheRaw}
|
key := langPackCacheKey{pack: langPack, code: langCode, kind: langPackCacheRaw}
|
||||||
return s.cachedPack(ctx, key, func() (domain.LangPack, error) {
|
return s.cachedPack(ctx, key, func() (domain.LangPack, error) {
|
||||||
return s.packs.GetPack(ctx, langPack, langCode, 0)
|
pack, err := s.packs.GetPack(ctx, langPack, langCode, 0)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LangPack{}, err
|
||||||
|
}
|
||||||
|
return s.brandPack(pack), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +242,7 @@ func (s *Service) cachedLanguages(ctx context.Context, langPack string) ([]domai
|
||||||
}
|
}
|
||||||
for i := range languages {
|
for i := range languages {
|
||||||
languages[i] = completeLanguageMetadata(langPack, languages[i])
|
languages[i] = completeLanguageMetadata(langPack, languages[i])
|
||||||
|
languages[i] = s.brandLanguage(languages[i])
|
||||||
}
|
}
|
||||||
return cachedLanguagesLoadResult{
|
return cachedLanguagesLoadResult{
|
||||||
languages: languages,
|
languages: languages,
|
||||||
|
|
@ -237,6 +262,27 @@ func (s *Service) cachedLanguages(ctx context.Context, langPack string) ([]domai
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) brandPack(pack domain.LangPack) domain.LangPack {
|
||||||
|
for i := range pack.Strings {
|
||||||
|
item := &pack.Strings[i]
|
||||||
|
item.Value = branding.UserVisibleText(item.Value, s.publicBaseURL)
|
||||||
|
item.ZeroValue = branding.UserVisibleText(item.ZeroValue, s.publicBaseURL)
|
||||||
|
item.OneValue = branding.UserVisibleText(item.OneValue, s.publicBaseURL)
|
||||||
|
item.TwoValue = branding.UserVisibleText(item.TwoValue, s.publicBaseURL)
|
||||||
|
item.FewValue = branding.UserVisibleText(item.FewValue, s.publicBaseURL)
|
||||||
|
item.ManyValue = branding.UserVisibleText(item.ManyValue, s.publicBaseURL)
|
||||||
|
item.OtherValue = branding.UserVisibleText(item.OtherValue, s.publicBaseURL)
|
||||||
|
}
|
||||||
|
return pack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) brandLanguage(lang domain.LangPackLanguage) domain.LangPackLanguage {
|
||||||
|
lang.Name = branding.UserVisibleText(lang.Name, s.publicBaseURL)
|
||||||
|
lang.NativeName = branding.UserVisibleText(lang.NativeName, s.publicBaseURL)
|
||||||
|
lang.TranslationsURL = branding.UserVisibleText(lang.TranslationsURL, s.publicBaseURL)
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) flushCaches() {
|
func (s *Service) flushCaches() {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,66 @@ func TestServiceNormalizesWebARawLangCode(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceRebrandsEveryLanguagePackProjection(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
base := memory.NewLangPackStore()
|
||||||
|
seed := domain.LangPack{
|
||||||
|
LangPack: "weba",
|
||||||
|
LangCode: "en",
|
||||||
|
Version: 7,
|
||||||
|
Strings: []domain.LangPackString{
|
||||||
|
{Key: "AppName", Value: "Telegram", Pluralized: true, ZeroValue: "No Telegram accounts", OneValue: "One Telegram account", TwoValue: "Two Telegram accounts", FewValue: "Few Telegram accounts", ManyValue: "Many Telegram accounts", OtherValue: "Other Telegram accounts"},
|
||||||
|
{Key: "TranslationLink", Value: "https://translations.telegram.org/en"},
|
||||||
|
{Key: "RuntimeIdentifier", Value: "org.telegram.messenger"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := base.UpsertPack(ctx, seed); err != nil {
|
||||||
|
t.Fatalf("seed langpack: %v", err)
|
||||||
|
}
|
||||||
|
storeWithMetadata := &metadataLangPackStore{
|
||||||
|
LangPackStore: base,
|
||||||
|
languages: []domain.LangPackLanguage{{
|
||||||
|
LangPack: "weba",
|
||||||
|
LangCode: "en",
|
||||||
|
Name: "Telegram English",
|
||||||
|
NativeName: "Telegram English",
|
||||||
|
TranslationsURL: "https://translations.telegram.org/en",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
svc := NewService(storeWithMetadata, WithPublicBaseURL("https://chat.example/root/"))
|
||||||
|
|
||||||
|
for name, load := range map[string]func() (domain.LangPack, error){
|
||||||
|
"full": func() (domain.LangPack, error) { return svc.GetLangPack(ctx, "weba", "en") },
|
||||||
|
"difference": func() (domain.LangPack, error) { return svc.GetDifference(ctx, "weba", "en", 1) },
|
||||||
|
"keys": func() (domain.LangPack, error) {
|
||||||
|
return svc.GetStrings(ctx, "weba", "en", []string{"AppName", "TranslationLink", "RuntimeIdentifier"})
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
pack, err := load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s projection: %v", name, err)
|
||||||
|
}
|
||||||
|
appName := findLangPackString(pack.Strings, "AppName")
|
||||||
|
if appName == nil || appName.Value != "Telesrv" || appName.ZeroValue != "No Telesrv accounts" || appName.OneValue != "One Telesrv account" || appName.TwoValue != "Two Telesrv accounts" || appName.FewValue != "Few Telesrv accounts" || appName.ManyValue != "Many Telesrv accounts" || appName.OtherValue != "Other Telesrv accounts" {
|
||||||
|
t.Fatalf("%s AppName = %+v, want all value forms rebranded", name, appName)
|
||||||
|
}
|
||||||
|
if got := stringValue(pack.Strings, "TranslationLink"); got != "https://chat.example/root/en" {
|
||||||
|
t.Fatalf("%s TranslationLink = %q", name, got)
|
||||||
|
}
|
||||||
|
if got := stringValue(pack.Strings, "RuntimeIdentifier"); got != "org.telegram.messenger" {
|
||||||
|
t.Fatalf("%s RuntimeIdentifier = %q, want protocol identifier unchanged", name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
languages, err := svc.ListLanguages(ctx, "weba")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list languages: %v", err)
|
||||||
|
}
|
||||||
|
if len(languages) != 1 || languages[0].Name != "Telesrv English" || languages[0].NativeName != "Telesrv English" || languages[0].TranslationsURL != "https://chat.example/root/en" {
|
||||||
|
t.Fatalf("languages = %+v, want branded metadata", languages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListLanguagesUsesSeededPacks(t *testing.T) {
|
func TestListLanguagesUsesSeededPacks(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
packs := memory.NewLangPackStore()
|
packs := memory.NewLangPackStore()
|
||||||
|
|
@ -286,6 +346,15 @@ type countingLangPackStore struct {
|
||||||
listLanguages int
|
listLanguages int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type metadataLangPackStore struct {
|
||||||
|
store.LangPackStore
|
||||||
|
languages []domain.LangPackLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *metadataLangPackStore) ListLanguages(context.Context, string) ([]domain.LangPackLanguage, error) {
|
||||||
|
return append([]domain.LangPackLanguage(nil), s.languages...), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *countingLangPackStore) GetPack(ctx context.Context, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
|
func (s *countingLangPackStore) GetPack(ctx context.Context, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.getPack++
|
s.getPack++
|
||||||
|
|
@ -334,3 +403,12 @@ func stringValue(strings []domain.LangPackString, key string) string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findLangPackString(strings []domain.LangPackString, key string) *domain.LangPackString {
|
||||||
|
for i := range strings {
|
||||||
|
if strings[i].Key == key {
|
||||||
|
return &strings[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/store"
|
"telesrv/internal/store"
|
||||||
"telesrv/internal/webauthn"
|
"telesrv/internal/webauthn"
|
||||||
|
|
@ -40,7 +41,7 @@ type Option func(*Service)
|
||||||
func WithRPName(name string) Option { return func(s *Service) { s.rpName = name } }
|
func WithRPName(name string) Option { return func(s *Service) { s.rpName = name } }
|
||||||
|
|
||||||
// WithAllowedOrigins 设置允许的 WebAuthn origin 白名单;为空表示不强校验 origin
|
// WithAllowedOrigins 设置允许的 WebAuthn origin 白名单;为空表示不强校验 origin
|
||||||
//(服务端通常不预知 Android apk-key-hash origin)。
|
// (服务端通常不预知 Android apk-key-hash origin)。
|
||||||
func WithAllowedOrigins(origins []string) Option {
|
func WithAllowedOrigins(origins []string) Option {
|
||||||
return func(s *Service) { s.allowedOrigins = append([]string(nil), origins...) }
|
return func(s *Service) { s.allowedOrigins = append([]string(nil), origins...) }
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +71,7 @@ func NewService(creds store.PasskeyStore, challenges store.PasskeyChallengeStore
|
||||||
creds: creds,
|
creds: creds,
|
||||||
challenges: challenges,
|
challenges: challenges,
|
||||||
rpID: rpID,
|
rpID: rpID,
|
||||||
rpName: "Telegram",
|
rpName: branding.ProductName,
|
||||||
dcID: dcID,
|
dcID: dcID,
|
||||||
challengeTTL: defaultChallengeTTL,
|
challengeTTL: defaultChallengeTTL,
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
first, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
first, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
||||||
Stars: 50, ConvertStars: 25, Enabled: true, SortOrder: 1, Title: "First", Animation: animation,
|
Stars: 50, ConvertStars: 25, Enabled: true, SortOrder: 1, Title: "Telegram Pin", Animation: animation,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create first: %v", err)
|
t.Fatalf("create first: %v", err)
|
||||||
|
|
@ -84,7 +84,7 @@ func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) {
|
||||||
t.Fatalf("current=%+v found=%v", current, found)
|
t.Fatalf("current=%+v found=%v", current, found)
|
||||||
}
|
}
|
||||||
historical, found, _ := svc.GiftRevisionByID(ctx, first.Gift.RevisionID)
|
historical, found, _ := svc.GiftRevisionByID(ctx, first.Gift.RevisionID)
|
||||||
if !found || historical.Stars != 50 || historical.Title != "First" {
|
if !found || historical.Stars != 50 || historical.Title != "Telesrv Pin" {
|
||||||
t.Fatalf("historical=%+v found=%v", historical, found)
|
t.Fatalf("historical=%+v found=%v", historical, found)
|
||||||
}
|
}
|
||||||
if _, err := svc.SetCatalogEnabled(ctx, first.Gift.ID+999, false); !errors.Is(err, domain.ErrStarGiftNotFound) {
|
if _, err := svc.SetCatalogEnabled(ctx, first.Gift.ID+999, false); !errors.Is(err, domain.ErrStarGiftNotFound) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/store"
|
"telesrv/internal/store"
|
||||||
)
|
)
|
||||||
|
|
@ -173,7 +174,7 @@ func (s *Service) CreateCatalogRevision(ctx context.Context, write domain.StarGi
|
||||||
if s == nil || s.store == nil || s.blobs == nil {
|
if s == nil || s.store == nil || s.blobs == nil {
|
||||||
return domain.StarGiftCatalogEntry{}, fmt.Errorf("star gift catalog importer is not configured")
|
return domain.StarGiftCatalogEntry{}, fmt.Errorf("star gift catalog importer is not configured")
|
||||||
}
|
}
|
||||||
write.Title = strings.TrimSpace(write.Title)
|
write.Title = branding.UserVisibleText(strings.TrimSpace(write.Title), "")
|
||||||
if write.Stars <= 0 || write.ConvertStars < 0 || write.ConvertStars > write.Stars ||
|
if write.Stars <= 0 || write.ConvertStars < 0 || write.ConvertStars > write.Stars ||
|
||||||
write.Animation.Width != 512 || write.Animation.Height != 512 || len(write.Animation.TGS) == 0 ||
|
write.Animation.Width != 512 || write.Animation.Height != 512 || len(write.Animation.TGS) == 0 ||
|
||||||
len([]rune(write.Title)) > domain.MaxStarGiftTitleRunes {
|
len([]rune(write.Title)) > domain.MaxStarGiftTitleRunes {
|
||||||
|
|
@ -239,7 +240,8 @@ func (s *Service) CreateCatalogBundle(ctx context.Context, write domain.StarGift
|
||||||
if s == nil || s.store == nil || s.blobs == nil {
|
if s == nil || s.store == nil || s.blobs == nil {
|
||||||
return domain.StarGiftCatalogBundleResult{}, fmt.Errorf("star gift catalog importer is not configured")
|
return domain.StarGiftCatalogBundleResult{}, fmt.Errorf("star gift catalog importer is not configured")
|
||||||
}
|
}
|
||||||
write.Catalog.Title = strings.TrimSpace(write.Catalog.Title)
|
write.Catalog.Title = branding.UserVisibleText(strings.TrimSpace(write.Catalog.Title), "")
|
||||||
|
write.Catalog.AuctionSlug = branding.UserVisibleText(strings.TrimSpace(write.Catalog.AuctionSlug), "")
|
||||||
if write.Catalog.Stars <= 0 || write.Catalog.ConvertStars < 0 || write.Catalog.ConvertStars > write.Catalog.Stars ||
|
if write.Catalog.Stars <= 0 || write.Catalog.ConvertStars < 0 || write.Catalog.ConvertStars > write.Catalog.Stars ||
|
||||||
write.Catalog.Animation.Width != 512 || write.Catalog.Animation.Height != 512 || len(write.Catalog.Animation.TGS) == 0 ||
|
write.Catalog.Animation.Width != 512 || write.Catalog.Animation.Height != 512 || len(write.Catalog.Animation.TGS) == 0 ||
|
||||||
len([]rune(write.Catalog.Title)) > domain.MaxStarGiftTitleRunes {
|
len([]rune(write.Catalog.Title)) > domain.MaxStarGiftTitleRunes {
|
||||||
|
|
@ -258,6 +260,9 @@ func (s *Service) CreateCatalogBundle(ctx context.Context, write domain.StarGift
|
||||||
}
|
}
|
||||||
if write.Collectible != nil {
|
if write.Collectible != nil {
|
||||||
write.Collectible.SlugPrefix = strings.ToLower(strings.TrimSpace(write.Collectible.SlugPrefix))
|
write.Collectible.SlugPrefix = strings.ToLower(strings.TrimSpace(write.Collectible.SlugPrefix))
|
||||||
|
brandCollectibleAttributes(write.Collectible.Models)
|
||||||
|
brandCollectibleAttributes(write.Collectible.Patterns)
|
||||||
|
brandCollectibleAttributes(write.Collectible.Backdrops)
|
||||||
if write.Collectible.OfficialGiftID != write.Catalog.OfficialGiftID ||
|
if write.Collectible.OfficialGiftID != write.Catalog.OfficialGiftID ||
|
||||||
!bytes.Equal(write.Collectible.SourceManifestSHA256, write.Catalog.SourceManifestSHA256) {
|
!bytes.Equal(write.Collectible.SourceManifestSHA256, write.Catalog.SourceManifestSHA256) {
|
||||||
return domain.StarGiftCatalogBundleResult{}, domain.ErrStarGiftCollectibleInvalid
|
return domain.StarGiftCatalogBundleResult{}, domain.ErrStarGiftCollectibleInvalid
|
||||||
|
|
@ -315,6 +320,9 @@ func (s *Service) PublishCollectibleRevision(ctx context.Context, write domain.S
|
||||||
if s == nil || s.store == nil {
|
if s == nil || s.store == nil {
|
||||||
return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible store is not configured")
|
return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible store is not configured")
|
||||||
}
|
}
|
||||||
|
brandCollectibleAttributes(write.Models)
|
||||||
|
brandCollectibleAttributes(write.Patterns)
|
||||||
|
brandCollectibleAttributes(write.Backdrops)
|
||||||
revision, err := s.store.PublishCollectibleRevision(ctx, write)
|
revision, err := s.store.PublishCollectibleRevision(ctx, write)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.InvalidateStarGiftCatalog()
|
s.InvalidateStarGiftCatalog()
|
||||||
|
|
@ -330,6 +338,9 @@ func (s *Service) CreateCollectibleRevision(ctx context.Context, write domain.St
|
||||||
return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible importer is not configured")
|
return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible importer is not configured")
|
||||||
}
|
}
|
||||||
write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix))
|
write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix))
|
||||||
|
brandCollectibleAttributes(write.Models)
|
||||||
|
brandCollectibleAttributes(write.Patterns)
|
||||||
|
brandCollectibleAttributes(write.Backdrops)
|
||||||
if err := domain.ValidateStarGiftCollectibleDraft(write); err != nil {
|
if err := domain.ValidateStarGiftCollectibleDraft(write); err != nil {
|
||||||
return domain.StarGiftCollectibleRevision{}, err
|
return domain.StarGiftCollectibleRevision{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -342,6 +353,12 @@ func (s *Service) CreateCollectibleRevision(ctx context.Context, write domain.St
|
||||||
return s.PublishCollectibleRevision(ctx, write)
|
return s.PublishCollectibleRevision(ctx, write)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func brandCollectibleAttributes(attributes []domain.StarGiftCollectibleAttribute) {
|
||||||
|
for i := range attributes {
|
||||||
|
attributes[i].Name = branding.UserVisibleText(strings.TrimSpace(attributes[i].Name), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) materializeCollectibleAttributes(ctx context.Context, attributes []domain.StarGiftCollectibleAttribute) error {
|
func (s *Service) materializeCollectibleAttributes(ctx context.Context, attributes []domain.StarGiftCollectibleAttribute) error {
|
||||||
for i := range attributes {
|
for i := range attributes {
|
||||||
animation := attributes[i].Animation
|
animation := attributes[i].Animation
|
||||||
|
|
|
||||||
95
internal/branding/branding.go
Normal file
95
internal/branding/branding.go
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Package branding owns the user-visible telesrv product identity.
|
||||||
|
//
|
||||||
|
// Protocol identifiers, client detection tokens and third-party compatibility
|
||||||
|
// headers do not belong here: callers must only pass text that is rendered to
|
||||||
|
// an end user.
|
||||||
|
package branding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProductName = "Telesrv"
|
||||||
|
ProductUsername = "telesrv"
|
||||||
|
DesktopAppName = "Telesrv Desktop"
|
||||||
|
AndroidAppName = "Telesrv Android"
|
||||||
|
IOSAppName = "Telesrv iOS"
|
||||||
|
MacOSAppName = "Telesrv macOS"
|
||||||
|
WebAAppName = "Telesrv Web A"
|
||||||
|
WebKAppName = "Telesrv Web K"
|
||||||
|
PremiumName = "Telesrv Premium"
|
||||||
|
StarsName = "Telesrv Stars"
|
||||||
|
DefaultPublicURL = "https://telesrv.net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientAppName returns the branded display name for a stored client platform.
|
||||||
|
// Stored detection tokens remain unchanged; this is only used at presentation
|
||||||
|
// boundaries such as account.getAuthorizations.
|
||||||
|
func ClientAppName(platform string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(platform)) {
|
||||||
|
case "android":
|
||||||
|
return AndroidAppName
|
||||||
|
case "ios":
|
||||||
|
return IOSAppName
|
||||||
|
case "macos":
|
||||||
|
return MacOSAppName
|
||||||
|
case "telegram-tt", "weba":
|
||||||
|
return WebAAppName
|
||||||
|
case "tweb", "webk":
|
||||||
|
return WebKAppName
|
||||||
|
case "tdesktop", "desktop", "windows":
|
||||||
|
return DesktopAppName
|
||||||
|
default:
|
||||||
|
return ProductName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserVisibleClientPlatform hides internal compatibility tokens from the
|
||||||
|
// authorization UI without changing their durable representation.
|
||||||
|
func UserVisibleClientPlatform(platform string) string {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(platform), "telegram-tt") {
|
||||||
|
return "weba"
|
||||||
|
}
|
||||||
|
return UserVisibleText(platform, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
officialHTTPHostRE = regexp.MustCompile(`(?i)https?://(?:[a-z0-9-]+\.)*(?:telegram\.(?:org|me|com|dog)|t\.me)([^a-z0-9]|$)`)
|
||||||
|
officialBareHostRE = regexp.MustCompile(`(?i)(?:(?:[a-z0-9-]+\.)*telegram\.(?:org|me|com|dog)|\bt\.me)([^a-z0-9]|$)`)
|
||||||
|
officialBrandRE = regexp.MustCompile(`(?i)telegram|телеграм[\p{L}]*|تيليجرام|تلگرام|텔레그램|טלגרם`)
|
||||||
|
technicalIDRE = regexp.MustCompile(`^[A-Za-z0-9-]+(?:[._][A-Za-z0-9-]+)+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserVisibleText replaces the official product brand and its public hosts in
|
||||||
|
// text returned to clients. Placeholder syntax, markup and string keys are
|
||||||
|
// deliberately untouched by callers; only values should pass through here.
|
||||||
|
func UserVisibleText(value, publicBaseURL string) string {
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
baseURL, publicHost := publicDestination(publicBaseURL)
|
||||||
|
value = officialHTTPHostRE.ReplaceAllString(value, baseURL+"${1}")
|
||||||
|
value = officialBareHostRE.ReplaceAllString(value, publicHost+"${1}")
|
||||||
|
// Some platform packs carry dotted or underscored runtime identifiers as
|
||||||
|
// values. They are not copy and changing them can break client navigation.
|
||||||
|
if technicalIDRE.MatchString(value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return officialBrandRE.ReplaceAllString(value, ProductName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicDestination(raw string) (string, string) {
|
||||||
|
raw = strings.TrimRight(strings.TrimSpace(raw), "/")
|
||||||
|
if raw == "" {
|
||||||
|
raw = DefaultPublicURL
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(raw)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Hostname() == "" {
|
||||||
|
raw = DefaultPublicURL
|
||||||
|
parsed, _ = url.Parse(raw)
|
||||||
|
}
|
||||||
|
return raw, parsed.Host
|
||||||
|
}
|
||||||
64
internal/branding/branding_test.go
Normal file
64
internal/branding/branding_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package branding
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestUserVisibleTextRebrandsWordsAndOfficialHosts(t *testing.T) {
|
||||||
|
got := UserVisibleText(
|
||||||
|
"Telegram telegram TELEGRAM Telegram-like https://translations.telegram.org/en t.me/example desktop.telegram.org",
|
||||||
|
"https://chat.example/root/",
|
||||||
|
)
|
||||||
|
want := "Telesrv Telesrv Telesrv Telesrv-like https://chat.example/root/en chat.example/example chat.example"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("UserVisibleText() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserVisibleTextPreservesTechnicalIdentifiers(t *testing.T) {
|
||||||
|
for _, value := range []string{
|
||||||
|
"org.telegram.messenger",
|
||||||
|
"telegram_antispam_user_id",
|
||||||
|
"telegram_aicomposetone",
|
||||||
|
} {
|
||||||
|
if got := UserVisibleText(value, ""); got != value {
|
||||||
|
t.Fatalf("UserVisibleText(%q) = %q, want unchanged", value, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserVisibleTextRebrandsBareOfficialHostsWithoutTouchingDottedIdentifiers(t *testing.T) {
|
||||||
|
for input, want := range map[string]string{
|
||||||
|
"telegram.org": "telesrv.net",
|
||||||
|
"desktop.telegram.org": "telesrv.net",
|
||||||
|
"t.me/example": "telesrv.net/example",
|
||||||
|
"org.telegram.messenger": "org.telegram.messenger",
|
||||||
|
} {
|
||||||
|
if got := UserVisibleText(input, ""); got != want {
|
||||||
|
t.Fatalf("UserVisibleText(%q) = %q, want %q", input, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserVisibleTextRebrandsLocalizedProductNames(t *testing.T) {
|
||||||
|
got := UserVisibleText("Телеграмом تيليجرام تلگرام 텔레그램 טלגרם", "")
|
||||||
|
if want := "Telesrv Telesrv Telesrv Telesrv Telesrv"; got != want {
|
||||||
|
t.Fatalf("UserVisibleText() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientPresentationNames(t *testing.T) {
|
||||||
|
for platform, want := range map[string]string{
|
||||||
|
"tdesktop": DesktopAppName,
|
||||||
|
"android": AndroidAppName,
|
||||||
|
"ios": IOSAppName,
|
||||||
|
"macos": MacOSAppName,
|
||||||
|
"telegram-tt": WebAAppName,
|
||||||
|
"tweb": WebKAppName,
|
||||||
|
} {
|
||||||
|
if got := ClientAppName(platform); got != want {
|
||||||
|
t.Fatalf("ClientAppName(%q) = %q, want %q", platform, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := UserVisibleClientPlatform("telegram-tt"); got != "weba" {
|
||||||
|
t.Fatalf("UserVisibleClientPlatform() = %q, want weba", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
internal/domain/branding_test.go
Normal file
20
internal/domain/branding_test.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServiceIdentityAndLoginMessageUseTelesrvBrand(t *testing.T) {
|
||||||
|
serviceUser := OfficialSystemUser()
|
||||||
|
if serviceUser.FirstName != "Telesrv" || serviceUser.Username != "telesrv" {
|
||||||
|
t.Fatalf("service user = %+v, want Telesrv identity", serviceUser)
|
||||||
|
}
|
||||||
|
message, err := OfficialLoginCodeMessage(42, "12345", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("build login message: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(message.Body, "Telesrv") || strings.Contains(strings.ToLower(message.Body), "telegram") {
|
||||||
|
t.Fatalf("login message exposes wrong brand: %q", message.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
)
|
)
|
||||||
|
|
||||||
const officialLoginCodeMessageTemplate = `Login code: %s. Do not give this code to anyone, even if they say they are from Telegram!
|
const officialLoginCodeMessageTemplate = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||||
|
|
||||||
This code can be used to log in to your Telegram account. We never ask it for anything else.
|
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
|
||||||
|
|
||||||
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import "telesrv/internal/branding"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// OfficialSystemUserID 是 Telegram 兼容客户端识别的官方系统账号。
|
// OfficialSystemUserID 是 Telegram 兼容客户端识别的官方系统账号。
|
||||||
OfficialSystemUserID int64 = 777000
|
OfficialSystemUserID int64 = 777000
|
||||||
|
|
@ -27,8 +29,8 @@ func OfficialSystemUser() User {
|
||||||
ID: OfficialSystemUserID,
|
ID: OfficialSystemUserID,
|
||||||
AccessHash: 6599886787491911851,
|
AccessHash: 6599886787491911851,
|
||||||
Phone: "42777",
|
Phone: "42777",
|
||||||
FirstName: "Telegram",
|
FirstName: branding.ProductName,
|
||||||
Username: "telegram",
|
Username: branding.ProductUsername,
|
||||||
Verified: true,
|
Verified: true,
|
||||||
Support: true,
|
Support: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
)
|
)
|
||||||
|
|
||||||
const manifestSchema = 2
|
const manifestSchema = 2
|
||||||
|
|
@ -299,7 +301,7 @@ func (c *Catalog) List(ctx context.Context) ([]GiftSummary, error) {
|
||||||
gift := snap.gifts[id]
|
gift := snap.gifts[id]
|
||||||
doc := snap.documents[gift.DocumentIDs[0]]
|
doc := snap.documents[gift.DocumentIDs[0]]
|
||||||
summary := GiftSummary{
|
summary := GiftSummary{
|
||||||
ID: id, Title: gift.Title, Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
ID: id, Title: branding.UserVisibleText(gift.Title, ""), Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
||||||
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
||||||
AvailabilityRemains: gift.AvailabilityRemains, AvailabilityResale: gift.AvailabilityResale,
|
AvailabilityRemains: gift.AvailabilityRemains, AvailabilityResale: gift.AvailabilityResale,
|
||||||
Limited: gift.Limited, SoldOut: gift.SoldOut, Birthday: gift.Birthday,
|
Limited: gift.Limited, SoldOut: gift.SoldOut, Birthday: gift.Birthday,
|
||||||
|
|
@ -308,7 +310,7 @@ func (c *Catalog) List(ctx context.Context) ([]GiftSummary, error) {
|
||||||
FirstSaleDate: gift.FirstSaleDate, LastSaleDate: gift.LastSaleDate,
|
FirstSaleDate: gift.FirstSaleDate, LastSaleDate: gift.LastSaleDate,
|
||||||
ResellMinStars: gift.ResellMinStars, PerUserTotal: gift.PerUserTotal,
|
ResellMinStars: gift.ResellMinStars, PerUserTotal: gift.PerUserTotal,
|
||||||
PerUserRemains: gift.PerUserRemains, LockedUntilDate: gift.LockedUntilDate,
|
PerUserRemains: gift.PerUserRemains, LockedUntilDate: gift.LockedUntilDate,
|
||||||
AuctionSlug: gift.AuctionSlug, GiftsPerRound: gift.GiftsPerRound,
|
AuctionSlug: branding.UserVisibleText(gift.AuctionSlug, ""), GiftsPerRound: gift.GiftsPerRound,
|
||||||
AuctionStartDate: gift.AuctionStartDate, UpgradeVariants: gift.UpgradeVariants,
|
AuctionStartDate: gift.AuctionStartDate, UpgradeVariants: gift.UpgradeVariants,
|
||||||
Background: cloneBackground(gift.Background), DocumentID: doc.ID64,
|
Background: cloneBackground(gift.Background), DocumentID: doc.ID64,
|
||||||
AnimationValidated: doc.AnimationValidated,
|
AnimationValidated: doc.AnimationValidated,
|
||||||
|
|
@ -342,7 +344,7 @@ func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible b
|
||||||
out := Bundle{
|
out := Bundle{
|
||||||
ManifestSHA256: append([]byte(nil), snap.manifestSHA...),
|
ManifestSHA256: append([]byte(nil), snap.manifestSHA...),
|
||||||
SourceJSON: append([]byte(nil), gift.SourceJSON...),
|
SourceJSON: append([]byte(nil), gift.SourceJSON...),
|
||||||
Gift: Gift{ID: gift.ID, Title: gift.Title, Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
Gift: Gift{ID: gift.ID, Title: branding.UserVisibleText(gift.Title, ""), Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
||||||
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
||||||
AvailabilityRemains: gift.AvailabilityRemains, Limited: gift.Limited, SoldOut: gift.SoldOut,
|
AvailabilityRemains: gift.AvailabilityRemains, Limited: gift.Limited, SoldOut: gift.SoldOut,
|
||||||
Birthday: gift.Birthday, RequirePremium: gift.RequirePremium, LimitedPerUser: gift.LimitedPerUser,
|
Birthday: gift.Birthday, RequirePremium: gift.RequirePremium, LimitedPerUser: gift.LimitedPerUser,
|
||||||
|
|
@ -350,7 +352,7 @@ func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible b
|
||||||
AvailabilityResale: gift.AvailabilityResale, FirstSaleDate: gift.FirstSaleDate,
|
AvailabilityResale: gift.AvailabilityResale, FirstSaleDate: gift.FirstSaleDate,
|
||||||
LastSaleDate: gift.LastSaleDate, ResellMinStars: gift.ResellMinStars,
|
LastSaleDate: gift.LastSaleDate, ResellMinStars: gift.ResellMinStars,
|
||||||
PerUserTotal: gift.PerUserTotal, PerUserRemains: gift.PerUserRemains,
|
PerUserTotal: gift.PerUserTotal, PerUserRemains: gift.PerUserRemains,
|
||||||
LockedUntilDate: gift.LockedUntilDate, AuctionSlug: gift.AuctionSlug,
|
LockedUntilDate: gift.LockedUntilDate, AuctionSlug: branding.UserVisibleText(gift.AuctionSlug, ""),
|
||||||
GiftsPerRound: gift.GiftsPerRound, AuctionStartDate: gift.AuctionStartDate,
|
GiftsPerRound: gift.GiftsPerRound, AuctionStartDate: gift.AuctionStartDate,
|
||||||
UpgradeVariants: gift.UpgradeVariants, Background: cloneBackground(gift.Background),
|
UpgradeVariants: gift.UpgradeVariants, Background: cloneBackground(gift.Background),
|
||||||
DocumentID: gift.DocumentIDs[0]},
|
DocumentID: gift.DocumentIDs[0]},
|
||||||
|
|
@ -372,17 +374,17 @@ func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible b
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Bundle{}, fmt.Errorf("model %q document %d: %w", value.Name, value.DocumentID, err)
|
return Bundle{}, fmt.Errorf("model %q document %d: %w", value.Name, value.DocumentID, err)
|
||||||
}
|
}
|
||||||
collectible.Models = append(collectible.Models, Model{Name: value.Name, DocumentID: value.DocumentID, Crafted: value.Crafted, Rarity: value.Rarity, Document: doc})
|
collectible.Models = append(collectible.Models, Model{Name: branding.UserVisibleText(value.Name, ""), DocumentID: value.DocumentID, Crafted: value.Crafted, Rarity: value.Rarity, Document: doc})
|
||||||
}
|
}
|
||||||
for _, value := range set.Patterns {
|
for _, value := range set.Patterns {
|
||||||
doc, err := c.readDocument(ctx, snap.documents[value.DocumentID])
|
doc, err := c.readDocument(ctx, snap.documents[value.DocumentID])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Bundle{}, fmt.Errorf("pattern %q document %d: %w", value.Name, value.DocumentID, err)
|
return Bundle{}, fmt.Errorf("pattern %q document %d: %w", value.Name, value.DocumentID, err)
|
||||||
}
|
}
|
||||||
collectible.Patterns = append(collectible.Patterns, Pattern{Name: value.Name, DocumentID: value.DocumentID, Rarity: value.Rarity, Document: doc})
|
collectible.Patterns = append(collectible.Patterns, Pattern{Name: branding.UserVisibleText(value.Name, ""), DocumentID: value.DocumentID, Rarity: value.Rarity, Document: doc})
|
||||||
}
|
}
|
||||||
for _, value := range set.Backdrops {
|
for _, value := range set.Backdrops {
|
||||||
collectible.Backdrops = append(collectible.Backdrops, Backdrop{Name: value.Name, BackdropID: value.BackdropID,
|
collectible.Backdrops = append(collectible.Backdrops, Backdrop{Name: branding.UserVisibleText(value.Name, ""), BackdropID: value.BackdropID,
|
||||||
CenterColor: value.CenterColor, EdgeColor: value.EdgeColor, PatternColor: value.PatternColor,
|
CenterColor: value.CenterColor, EdgeColor: value.EdgeColor, PatternColor: value.PatternColor,
|
||||||
TextColor: value.TextColor, Rarity: value.Rarity})
|
TextColor: value.TextColor, Rarity: value.Rarity})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func TestCatalogVerifiesSelectedDocument(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
value := manifest{Schema: manifestSchema, GiftCount: 1,
|
value := manifest{Schema: manifestSchema, GiftCount: 1,
|
||||||
Gifts: []giftManifest{{Index: 0, Kind: "regular", ID: 1, Stars: 10, ConvertStars: 5, DocumentIDs: []int64{10}}},
|
Gifts: []giftManifest{{Index: 0, Kind: "regular", ID: 1, Title: "Telegram Pin", Stars: 10, ConvertStars: 5, DocumentIDs: []int64{10}}},
|
||||||
Documents: []documentManifest{{ID64: 10, FileName: "gift.tgs", File: fileArtifact{Path: "documents/10.tgs", Size: int64(len(data)), SHA256: hex.EncodeToString(sum[:])}}},
|
Documents: []documentManifest{{ID64: 10, FileName: "gift.tgs", File: fileArtifact{Path: "documents/10.tgs", Size: int64(len(data)), SHA256: hex.EncodeToString(sum[:])}}},
|
||||||
}
|
}
|
||||||
raw, err := json.Marshal(value)
|
raw, err := json.Marshal(value)
|
||||||
|
|
@ -33,11 +33,11 @@ func TestCatalogVerifiesSelectedDocument(t *testing.T) {
|
||||||
}
|
}
|
||||||
catalog := New(root)
|
catalog := New(root)
|
||||||
items, err := catalog.List(context.Background())
|
items, err := catalog.List(context.Background())
|
||||||
if err != nil || len(items) != 1 || items[0].ID != 1 {
|
if err != nil || len(items) != 1 || items[0].ID != 1 || items[0].Title != "Telesrv Pin" {
|
||||||
t.Fatalf("items=%+v err=%v", items, err)
|
t.Fatalf("items=%+v err=%v", items, err)
|
||||||
}
|
}
|
||||||
bundle, err := catalog.Bundle(context.Background(), 1, false)
|
bundle, err := catalog.Bundle(context.Background(), 1, false)
|
||||||
if err != nil || string(bundle.BaseDocument.Data) != string(data) {
|
if err != nil || string(bundle.BaseDocument.Data) != string(data) || bundle.Gift.Title != "Telesrv Pin" {
|
||||||
t.Fatalf("bundle=%+v err=%v", bundle, err)
|
t.Fatalf("bundle=%+v err=%v", bundle, err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(root, "documents", "10.tgs"), []byte("tampered---"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(root, "documents", "10.tgs"), []byte("tampered---"), 0o644); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
|
|
||||||
"github.com/iamxvbaba/td/tlprofile"
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
|
"telesrv/internal/branding"
|
||||||
ioscompat "telesrv/internal/compat/ios"
|
ioscompat "telesrv/internal/compat/ios"
|
||||||
"telesrv/internal/compat/tdesktop"
|
"telesrv/internal/compat/tdesktop"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
|
|
@ -1889,12 +1890,12 @@ func tgAuthorization(a domain.Authorization, currentAuthKeyID [8]byte, now int)
|
||||||
Current: a.AuthKeyID == currentAuthKeyID,
|
Current: a.AuthKeyID == currentAuthKeyID,
|
||||||
OfficialApp: true,
|
OfficialApp: true,
|
||||||
Hash: a.Hash,
|
Hash: a.Hash,
|
||||||
DeviceModel: a.DeviceModel,
|
DeviceModel: branding.UserVisibleText(a.DeviceModel, ""),
|
||||||
Platform: a.Platform,
|
Platform: branding.UserVisibleClientPlatform(a.Platform),
|
||||||
SystemVersion: a.SystemVersion,
|
SystemVersion: branding.UserVisibleText(a.SystemVersion, ""),
|
||||||
APIID: a.APIID,
|
APIID: a.APIID,
|
||||||
AppName: "Telegram Desktop",
|
AppName: branding.ClientAppName(a.Platform),
|
||||||
AppVersion: a.AppVersion,
|
AppVersion: branding.UserVisibleText(a.AppVersion, ""),
|
||||||
DateCreated: created,
|
DateCreated: created,
|
||||||
DateActive: active,
|
DateActive: active,
|
||||||
IP: a.IP,
|
IP: a.IP,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ func (r *Router) resolveAIComposeStyleWebPage(ctx context.Context, rawURL string
|
||||||
Hash: aiComposeToneWebPageHash(tone),
|
Hash: aiComposeToneWebPageHash(tone),
|
||||||
Date: int(now.Unix()),
|
Date: int(now.Unix()),
|
||||||
Type: aiComposeToneWebPageType,
|
Type: aiComposeToneWebPageType,
|
||||||
SiteName: "Telegram",
|
SiteName: branding.ProductName,
|
||||||
Title: tone.Title,
|
Title: tone.Title,
|
||||||
Description: tone.Prompt,
|
Description: tone.Prompt,
|
||||||
ComposeToneEmojiID: tone.EmojiID,
|
ComposeToneEmojiID: tone.EmojiID,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
"github.com/iamxvbaba/td/tlprofile"
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
"telesrv/internal/app/auth"
|
"telesrv/internal/app/auth"
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -926,13 +927,13 @@ func (r *Router) tgSignInServiceNotification(ctx context.Context, u domain.User,
|
||||||
if ci, ok := ClientInfoFrom(ctx); ok {
|
if ci, ok := ClientInfoFrom(ctx); ok {
|
||||||
parts := []string{}
|
parts := []string{}
|
||||||
if ci.DeviceModel != "" {
|
if ci.DeviceModel != "" {
|
||||||
parts = append(parts, ci.DeviceModel)
|
parts = append(parts, branding.UserVisibleText(ci.DeviceModel, ""))
|
||||||
}
|
}
|
||||||
if ci.SystemVersion != "" {
|
if ci.SystemVersion != "" {
|
||||||
parts = append(parts, ci.SystemVersion)
|
parts = append(parts, branding.UserVisibleText(ci.SystemVersion, ""))
|
||||||
}
|
}
|
||||||
if ci.AppVersion != "" {
|
if ci.AppVersion != "" {
|
||||||
parts = append(parts, ci.AppVersion)
|
parts = append(parts, branding.UserVisibleText(ci.AppVersion, ""))
|
||||||
}
|
}
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
client = strings.Join(parts, " / ")
|
client = strings.Join(parts, " / ")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
|
|
||||||
"github.com/iamxvbaba/td/tlprofile"
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
|
"telesrv/internal/branding"
|
||||||
androidcompat "telesrv/internal/compat/android"
|
androidcompat "telesrv/internal/compat/android"
|
||||||
ioscompat "telesrv/internal/compat/ios"
|
ioscompat "telesrv/internal/compat/ios"
|
||||||
"telesrv/internal/compat/tdesktop"
|
"telesrv/internal/compat/tdesktop"
|
||||||
|
|
@ -21,7 +22,7 @@ func (r *Router) registerHelp(d *tlprofile.Dispatcher) {
|
||||||
return tdesktop.NearestDC(r.cfg.DC), nil
|
return tdesktop.NearestDC(r.cfg.DC), nil
|
||||||
})
|
})
|
||||||
registerRPC[*tg.HelpGetInviteTextRequest](d, tlprofile.SemanticMethodHelpGetInviteText, func(ctx context.Context, layerRequest *tg.HelpGetInviteTextRequest) (any, error) {
|
registerRPC[*tg.HelpGetInviteTextRequest](d, tlprofile.SemanticMethodHelpGetInviteText, func(ctx context.Context, layerRequest *tg.HelpGetInviteTextRequest) (any, error) {
|
||||||
return &tg.HelpInviteText{Message: "Join me on Telegram."}, nil
|
return &tg.HelpInviteText{Message: "Join me on " + branding.ProductName + "."}, nil
|
||||||
})
|
})
|
||||||
registerRPC[*tg.HelpSaveAppLogRequest](d, tlprofile.SemanticMethodHelpSaveAppLog, func(ctx context.Context, _ *tg.HelpSaveAppLogRequest) (any, error) {
|
registerRPC[*tg.HelpSaveAppLogRequest](d, tlprofile.SemanticMethodHelpSaveAppLog, func(ctx context.Context, _ *tg.HelpSaveAppLogRequest) (any, error) {
|
||||||
return r.onHelpSaveAppLog(ctx)
|
return r.onHelpSaveAppLog(ctx)
|
||||||
|
|
@ -182,7 +183,7 @@ func (r *Router) onHelpDismissSuggestion(ctx context.Context, req *tg.HelpDismis
|
||||||
// 六个字段全是 TL 必填项,空值也必须给出空集合而非缺失。
|
// 六个字段全是 TL 必填项,空值也必须给出空集合而非缺失。
|
||||||
func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumPromo, error) {
|
func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumPromo, error) {
|
||||||
promo := &tg.HelpPremiumPromo{
|
promo := &tg.HelpPremiumPromo{
|
||||||
StatusText: "Telegram Premium is not active on this account.",
|
StatusText: branding.PremiumName + " is not active on this account.",
|
||||||
StatusEntities: []tg.MessageEntityClass{},
|
StatusEntities: []tg.MessageEntityClass{},
|
||||||
VideoSections: []string{},
|
VideoSections: []string{},
|
||||||
Videos: []tg.DocumentClass{},
|
Videos: []tg.DocumentClass{},
|
||||||
|
|
@ -199,7 +200,7 @@ func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumProm
|
||||||
}
|
}
|
||||||
if u.PremiumActiveAt(r.clock.Now().Unix()) {
|
if u.PremiumActiveAt(r.clock.Now().Unix()) {
|
||||||
until := time.Unix(int64(u.PremiumUntil), 0)
|
until := time.Unix(int64(u.PremiumUntil), 0)
|
||||||
promo.StatusText = "Telegram Premium is active until " + until.Format("2006-01-02") + "."
|
promo.StatusText = branding.PremiumName + " is active until " + until.Format("2006-01-02") + "."
|
||||||
}
|
}
|
||||||
return promo, nil
|
return promo, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ func TestPaymentsGetStarsRevenueAdsAccountURLReturnsCompatURLAndValidatesPeer(t
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("response type = %T, want *tg.PaymentsStarsRevenueAdsAccountURL", got)
|
t.Fatalf("response type = %T, want *tg.PaymentsStarsRevenueAdsAccountURL", got)
|
||||||
}
|
}
|
||||||
if url.URL != "https://ads.telegram.org/" {
|
if url.URL != "https://telesrv.net" {
|
||||||
t.Fatalf("url = %q, want ads compat URL", url.URL)
|
t.Fatalf("url = %q, want ads compat URL", url.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
||||||
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
|
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &tg.PaymentsStarsRevenueAdsAccountURL{URL: "https://ads.telegram.org/"}, nil
|
return &tg.PaymentsStarsRevenueAdsAccountURL{URL: r.publicLink("")}, nil
|
||||||
})
|
})
|
||||||
registerRPC[*tg.PaymentsGetStarsRevenueStatsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsRevenueStats, func(ctx context.Context, req *tg.PaymentsGetStarsRevenueStatsRequest) (any, error) {
|
registerRPC[*tg.PaymentsGetStarsRevenueStatsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsRevenueStats, func(ctx context.Context, req *tg.PaymentsGetStarsRevenueStatsRequest) (any, error) {
|
||||||
return r.onPaymentsGetStarsRevenueStats(ctx, req)
|
return r.onPaymentsGetStarsRevenueStats(ctx, req)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/tgerr"
|
"github.com/iamxvbaba/td/tgerr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -324,11 +325,11 @@ func (r *Router) starsTopupPaymentForm(userID int64, purpose *tg.InputStorePayme
|
||||||
return &tg.PaymentsPaymentFormStars{
|
return &tg.PaymentsPaymentFormStars{
|
||||||
FormID: starsTopupFormID(userID, purpose.Stars, purpose.Currency, purpose.Amount),
|
FormID: starsTopupFormID(userID, purpose.Stars, purpose.Currency, purpose.Amount),
|
||||||
BotID: domain.OfficialSystemUserID,
|
BotID: domain.OfficialSystemUserID,
|
||||||
Title: "Telegram Stars",
|
Title: branding.StarsName,
|
||||||
Description: "telesrv dev Stars top-up",
|
Description: "telesrv dev Stars top-up",
|
||||||
Invoice: tg.Invoice{
|
Invoice: tg.Invoice{
|
||||||
Currency: "XTR",
|
Currency: "XTR",
|
||||||
Prices: []tg.LabeledPrice{{Label: "Telegram Stars", Amount: purpose.Stars}},
|
Prices: []tg.LabeledPrice{{Label: branding.StarsName, Amount: purpose.Stars}},
|
||||||
},
|
},
|
||||||
Users: tgUsersForViewer(userID, []domain.User{domain.OfficialSystemUser()}),
|
Users: tgUsersForViewer(userID, []domain.User{domain.OfficialSystemUser()}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ func TestSignInServiceNotificationMatchesEnterpriseShape(t *testing.T) {
|
||||||
if update.Popup || update.InboxDate == 0 || update.Media == nil {
|
if update.Popup || update.InboxDate == 0 || update.Media == nil {
|
||||||
t.Fatalf("notification flags/media = popup %v inbox %d media %T", update.Popup, update.InboxDate, update.Media)
|
t.Fatalf("notification flags/media = popup %v inbox %d media %T", update.Popup, update.InboxDate, update.Media)
|
||||||
}
|
}
|
||||||
for _, want := range []string{"New login.", "Test User", "Telegram Desktop", "Settings > Devices"} {
|
for _, want := range []string{"New login.", "Test User", "Telesrv Desktop", "Settings > Devices"} {
|
||||||
if !strings.Contains(update.Message, want) {
|
if !strings.Contains(update.Message, want) {
|
||||||
t.Fatalf("notification message %q missing %q", update.Message, want)
|
t.Fatalf("notification message %q missing %q", update.Message, want)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"source": "official telegram (appearancefetch)",
|
"source": "upstream appearance snapshot",
|
||||||
"exported_at": "2026-06-25T14:04:03Z",
|
"exported_at": "2026-06-25T14:04:03Z",
|
||||||
"notes": {
|
"notes": {
|
||||||
"server": "official",
|
"server": "official",
|
||||||
|
|
@ -3788,7 +3788,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "filename",
|
"kind": "filename",
|
||||||
"file_name": "background telegram 003.jpg"
|
"file_name": "background telesrv 003.jpg"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbs": [
|
"thumbs": [
|
||||||
|
|
@ -5834,4 +5834,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,29 @@ func ensureOfficialSystemUserWithDB(ctx context.Context, db sqlcgen.DBTX, msg do
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := db.Exec(ctx, `
|
if _, err := db.Exec(ctx, `
|
||||||
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support, about, is_bot, bot_info_version)
|
WITH upserted AS (
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support, about, is_bot, bot_info_version)
|
||||||
ON CONFLICT (id) DO NOTHING
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
access_hash = EXCLUDED.access_hash,
|
||||||
|
phone = EXCLUDED.phone,
|
||||||
|
first_name = EXCLUDED.first_name,
|
||||||
|
last_name = EXCLUDED.last_name,
|
||||||
|
username = EXCLUDED.username,
|
||||||
|
country_code = EXCLUDED.country_code,
|
||||||
|
verified = EXCLUDED.verified,
|
||||||
|
support = EXCLUDED.support,
|
||||||
|
about = EXCLUDED.about,
|
||||||
|
is_bot = EXCLUDED.is_bot,
|
||||||
|
bot_info_version = EXCLUDED.bot_info_version,
|
||||||
|
updated_at = now()
|
||||||
|
RETURNING id, lower(username) AS username_lower
|
||||||
|
), deleted_old_username AS (
|
||||||
|
DELETE FROM peer_usernames
|
||||||
|
WHERE peer_type = 'user' AND peer_id = (SELECT id FROM upserted)
|
||||||
|
)
|
||||||
|
INSERT INTO peer_usernames (username_lower, peer_type, peer_id)
|
||||||
|
SELECT username_lower, 'user', id FROM upserted
|
||||||
`, u.ID, u.AccessHash, u.Phone, u.FirstName, u.LastName, u.Username, u.CountryCode, u.Verified, u.Support, u.About, u.Bot, u.BotInfoVersion); err != nil {
|
`, u.ID, u.AccessHash, u.Phone, u.FirstName, u.LastName, u.Username, u.CountryCode, u.Verified, u.Support, u.About, u.Bot, u.BotInfoVersion); err != nil {
|
||||||
return fmt.Errorf("ensure official system user: %w", err)
|
return fmt.Errorf("ensure official system user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,28 +23,30 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"github.com/fxamacker/cbor/v2"
|
||||||
|
|
||||||
|
"telesrv/internal/branding"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 验证错误。调用方据此映射到 TL 错误码/拒绝登录。
|
// 验证错误。调用方据此映射到 TL 错误码/拒绝登录。
|
||||||
var (
|
var (
|
||||||
ErrClientDataInvalid = errors.New("webauthn: client data invalid")
|
ErrClientDataInvalid = errors.New("webauthn: client data invalid")
|
||||||
ErrChallengeMismatch = errors.New("webauthn: challenge mismatch")
|
ErrChallengeMismatch = errors.New("webauthn: challenge mismatch")
|
||||||
ErrOriginNotAllowed = errors.New("webauthn: origin not allowed")
|
ErrOriginNotAllowed = errors.New("webauthn: origin not allowed")
|
||||||
ErrRPIDMismatch = errors.New("webauthn: rp id hash mismatch")
|
ErrRPIDMismatch = errors.New("webauthn: rp id hash mismatch")
|
||||||
ErrUserNotPresent = errors.New("webauthn: user-present flag not set")
|
ErrUserNotPresent = errors.New("webauthn: user-present flag not set")
|
||||||
ErrAuthDataInvalid = errors.New("webauthn: authenticator data invalid")
|
ErrAuthDataInvalid = errors.New("webauthn: authenticator data invalid")
|
||||||
ErrAttestationInvalid = errors.New("webauthn: attestation object invalid")
|
ErrAttestationInvalid = errors.New("webauthn: attestation object invalid")
|
||||||
ErrPublicKeyUnsupported = errors.New("webauthn: unsupported public key algorithm")
|
ErrPublicKeyUnsupported = errors.New("webauthn: unsupported public key algorithm")
|
||||||
ErrSignatureInvalid = errors.New("webauthn: signature invalid")
|
ErrSignatureInvalid = errors.New("webauthn: signature invalid")
|
||||||
ErrCounterRegressed = errors.New("webauthn: sign counter regressed (possible cloned authenticator)")
|
ErrCounterRegressed = errors.New("webauthn: sign counter regressed (possible cloned authenticator)")
|
||||||
)
|
)
|
||||||
|
|
||||||
// authenticatorData 标志位(WebAuthn §6.1)。
|
// authenticatorData 标志位(WebAuthn §6.1)。
|
||||||
const (
|
const (
|
||||||
flagUserPresent = 0x01
|
flagUserPresent = 0x01
|
||||||
flagUserVerified = 0x04
|
flagUserVerified = 0x04
|
||||||
flagAttestedCredData = 0x40
|
flagAttestedCredData = 0x40
|
||||||
flagExtensionData = 0x80
|
flagExtensionData = 0x80
|
||||||
)
|
)
|
||||||
|
|
||||||
// COSE algorithm identifiers。
|
// COSE algorithm identifiers。
|
||||||
|
|
@ -55,8 +57,8 @@ const (
|
||||||
|
|
||||||
// COSE key type / curve。
|
// COSE key type / curve。
|
||||||
const (
|
const (
|
||||||
coseKtyOKP = 1
|
coseKtyOKP = 1
|
||||||
coseKtyEC2 = 2
|
coseKtyEC2 = 2
|
||||||
coseCrvP256 = 1
|
coseCrvP256 = 1
|
||||||
coseCrvEd25519 = 6
|
coseCrvEd25519 = 6
|
||||||
)
|
)
|
||||||
|
|
@ -76,7 +78,7 @@ func decodeB64URL(s string) ([]byte, error) {
|
||||||
type RegistrationParams struct {
|
type RegistrationParams struct {
|
||||||
RPID string
|
RPID string
|
||||||
RPName string
|
RPName string
|
||||||
UserID []byte // 通常是 "dcId:userId" 的字节
|
UserID []byte // 通常是 "dcId:userId" 的字节
|
||||||
UserName string
|
UserName string
|
||||||
UserDisplay string
|
UserDisplay string
|
||||||
Challenge []byte
|
Challenge []byte
|
||||||
|
|
@ -99,7 +101,7 @@ func BuildRegistrationOptions(p RegistrationParams) ([]byte, error) {
|
||||||
exclude = append(exclude, map[string]any{"type": "public-key", "id": b64.EncodeToString(id)})
|
exclude = append(exclude, map[string]any{"type": "public-key", "id": b64.EncodeToString(id)})
|
||||||
}
|
}
|
||||||
pub := map[string]any{
|
pub := map[string]any{
|
||||||
"rp": map[string]any{"id": p.RPID, "name": orDefault(p.RPName, "Telegram")},
|
"rp": map[string]any{"id": p.RPID, "name": orDefault(p.RPName, branding.ProductName)},
|
||||||
"user": map[string]any{"id": b64.EncodeToString(p.UserID), "name": p.UserName, "displayName": orDefault(p.UserDisplay, p.UserName)},
|
"user": map[string]any{"id": b64.EncodeToString(p.UserID), "name": p.UserName, "displayName": orDefault(p.UserDisplay, p.UserName)},
|
||||||
"challenge": b64.EncodeToString(p.Challenge),
|
"challenge": b64.EncodeToString(p.Challenge),
|
||||||
"pubKeyCredParams": []map[string]any{
|
"pubKeyCredParams": []map[string]any{
|
||||||
|
|
@ -199,12 +201,12 @@ func ChallengeFromClientData(clientDataJSON []byte) ([]byte, error) {
|
||||||
|
|
||||||
// parsedAuthData 是 authenticatorData 的解析结果。
|
// parsedAuthData 是 authenticatorData 的解析结果。
|
||||||
type parsedAuthData struct {
|
type parsedAuthData struct {
|
||||||
rpIDHash []byte
|
rpIDHash []byte
|
||||||
flags byte
|
flags byte
|
||||||
signCount uint32
|
signCount uint32
|
||||||
aaguid []byte
|
aaguid []byte
|
||||||
credID []byte
|
credID []byte
|
||||||
credPubKey []byte // COSE 公钥原始字节(仅注册时存在)
|
credPubKey []byte // COSE 公钥原始字节(仅注册时存在)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAuthData(authData []byte) (parsedAuthData, error) {
|
func parseAuthData(authData []byte) (parsedAuthData, error) {
|
||||||
|
|
@ -351,8 +353,8 @@ func VerifyAssertion(in VerifyAssertionInput) (uint32, error) {
|
||||||
|
|
||||||
// publicKey 抽象 ES256/EdDSA 验签。
|
// publicKey 抽象 ES256/EdDSA 验签。
|
||||||
type publicKey struct {
|
type publicKey struct {
|
||||||
ec *ecdsa.PublicKey
|
ec *ecdsa.PublicKey
|
||||||
ed ed25519.PublicKey
|
ed ed25519.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p publicKey) verify(signed, sig []byte) error {
|
func (p publicKey) verify(signed, sig []byte) error {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue