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 拉取墙纸 + 聊天主题,下载文档/缩略图,
|
||||
// 生成 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 沿用。
|
||||
//
|
||||
// 需登录(墙纸/主题接口非免登)。api 凭据用 TDesktop 开源公开的 id/hash。
|
||||
|
|
@ -114,8 +114,8 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
|||
}
|
||||
|
||||
api := client.API()
|
||||
docsDir := filepath.Join(outDir, "Default_wallpapers", "documents")
|
||||
thumbsDir := filepath.Join(outDir, "Default_wallpapers", "thumbs", "m")
|
||||
docsDir := filepath.Join(outDir, "default_wallpapers", "documents")
|
||||
thumbsDir := filepath.Join(outDir, "default_wallpapers", "thumbs", "m")
|
||||
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
|||
return appearance.Document{}, err
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
out.Path = "Default_wallpapers/documents/" + name
|
||||
out.Path = "default_wallpapers/documents/" + name
|
||||
out.SHA256 = hex.EncodeToString(sum[:])
|
||||
// "m" 缩略图
|
||||
for _, t := range doc.Thumbs {
|
||||
|
|
@ -187,7 +187,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
|||
tsum := sha256.Sum256(tdata)
|
||||
out.Thumbs = append(out.Thumbs, appearance.PhotoSize{
|
||||
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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
catalog := appearance.Catalog{
|
||||
Source: "official telegram (appearancefetch)",
|
||||
Source: "upstream appearance snapshot",
|
||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Notes: appearance.Notes{
|
||||
Server: "official",
|
||||
|
|
@ -390,7 +390,7 @@ func doFetch(ctx context.Context, client *telegram.Client, outDir string) error
|
|||
if err != nil {
|
||||
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 {
|
||||
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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||
"collectibles.backdrop": "Backdrop",
|
||||
"collectibles.rarity": "Rarity ‰",
|
||||
"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.remove": "Remove attribute",
|
||||
"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.rarity": "稀有度 ‰",
|
||||
"collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。",
|
||||
"collectibles.colorHint": "颜色会按 Telegram 24 位 RGB 数值保存。",
|
||||
"collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。",
|
||||
"collectibles.addAttribute": "添加",
|
||||
"collectibles.remove": "删除属性",
|
||||
"collectibles.fileRequired": "每个模型和图案都必须选择 TGS 或 Lottie 文件。",
|
||||
|
|
@ -1068,7 +1068,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||
"collectibles.backdrop": "Фон",
|
||||
"collectibles.rarity": "Редкость ‰",
|
||||
"collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.",
|
||||
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения Telegram.",
|
||||
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
|
||||
"collectibles.addAttribute": "Добавить",
|
||||
"collectibles.remove": "Удалить атрибут",
|
||||
"collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.",
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ func run(logger *zap.Logger) error {
|
|||
cfg.UploadPartGCInterval,
|
||||
cfg.UploadPartGCBatch,
|
||||
).Run(ctx)
|
||||
langPackService := langpack.NewService(langPackStore)
|
||||
langPackService := langpack.NewService(langPackStore, langpack.WithPublicBaseURL(cfg.PublicBaseURL))
|
||||
privacyService := privacyapp.NewService(privacyStore, contactStore)
|
||||
contactsService := contacts.NewService(contactStore, userStore).Configure(
|
||||
contacts.WithPhotoProvider(cachedPhotos),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ type appServer struct {
|
|||
func main() {
|
||||
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.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.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")
|
||||
|
|
|
|||
|
|
@ -5554,7 +5554,7 @@ SET row_security = off;
|
|||
-- 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);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/otpdelivery"
|
||||
"telesrv/internal/store"
|
||||
|
|
@ -79,7 +80,7 @@ func (s *Service) DeleteAccount(ctx context.Context, userID int64, authKeyID [8]
|
|||
}
|
||||
executeAt := now.Add(accountDeletionDelay)
|
||||
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),
|
||||
)
|
||||
pending, _, err := s.lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/iamxvbaba/td/bin"
|
||||
mtcrypto "github.com/iamxvbaba/td/crypto"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/otpdelivery"
|
||||
"telesrv/internal/store"
|
||||
|
|
@ -1435,9 +1436,9 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error
|
|||
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.`
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ const (
|
|||
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:
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"hash"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/seed/appearance"
|
||||
)
|
||||
|
|
@ -253,7 +254,7 @@ func appearanceDocumentAttributes(in []appearance.DocumentAttribute) []domain.Do
|
|||
if attr.FileName != "" {
|
||||
out = append(out, domain.DocumentAttribute{
|
||||
Kind: domain.DocAttrFilename,
|
||||
FileName: attr.FileName,
|
||||
FileName: branding.UserVisibleText(attr.FileName, ""),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"golang.org/x/sync/singleflight"
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
|
@ -18,16 +19,34 @@ type Service struct {
|
|||
languageCache *languageListCache
|
||||
packLoads 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 服务。
|
||||
func NewService(packs store.LangPackStore) *Service {
|
||||
return newServiceWithCacheLimits(
|
||||
func NewService(packs store.LangPackStore, opts ...Option) *Service {
|
||||
s := newServiceWithCacheLimits(
|
||||
packs,
|
||||
defaultLangPackCacheMaxBytes,
|
||||
defaultLangPackCacheMaxEntries,
|
||||
defaultLanguageListCacheMaxEntries,
|
||||
)
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
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,
|
||||
packCache: newLangPackCache(maxBytes, maxEntries),
|
||||
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) {
|
||||
key := langPackCacheKey{pack: langPack, code: langCode, kind: langPackCacheRaw}
|
||||
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 {
|
||||
languages[i] = completeLanguageMetadata(langPack, languages[i])
|
||||
languages[i] = s.brandLanguage(languages[i])
|
||||
}
|
||||
return cachedLanguagesLoadResult{
|
||||
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() {
|
||||
if s == nil {
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
packs := memory.NewLangPackStore()
|
||||
|
|
@ -286,6 +346,15 @@ type countingLangPackStore struct {
|
|||
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) {
|
||||
s.mu.Lock()
|
||||
s.getPack++
|
||||
|
|
@ -334,3 +403,12 @@ func stringValue(strings []domain.LangPackString, key string) string {
|
|||
}
|
||||
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"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/webauthn"
|
||||
|
|
@ -40,7 +41,7 @@ type Option func(*Service)
|
|||
func WithRPName(name string) Option { return func(s *Service) { s.rpName = name } }
|
||||
|
||||
// WithAllowedOrigins 设置允许的 WebAuthn origin 白名单;为空表示不强校验 origin
|
||||
//(服务端通常不预知 Android apk-key-hash origin)。
|
||||
// (服务端通常不预知 Android apk-key-hash origin)。
|
||||
func WithAllowedOrigins(origins []string) Option {
|
||||
return func(s *Service) { s.allowedOrigins = append([]string(nil), origins...) }
|
||||
}
|
||||
|
|
@ -70,7 +71,7 @@ func NewService(creds store.PasskeyStore, challenges store.PasskeyChallengeStore
|
|||
creds: creds,
|
||||
challenges: challenges,
|
||||
rpID: rpID,
|
||||
rpName: "Telegram",
|
||||
rpName: branding.ProductName,
|
||||
dcID: dcID,
|
||||
challengeTTL: defaultChallengeTTL,
|
||||
now: time.Now,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("create first: %v", err)
|
||||
|
|
@ -84,7 +84,7 @@ func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) {
|
|||
t.Fatalf("current=%+v found=%v", current, found)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if _, err := svc.SetCatalogEnabled(ctx, first.Gift.ID+999, false); !errors.Is(err, domain.ErrStarGiftNotFound) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
"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 {
|
||||
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 ||
|
||||
write.Animation.Width != 512 || write.Animation.Height != 512 || len(write.Animation.TGS) == 0 ||
|
||||
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 {
|
||||
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 ||
|
||||
write.Catalog.Animation.Width != 512 || write.Catalog.Animation.Height != 512 || len(write.Catalog.Animation.TGS) == 0 ||
|
||||
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 {
|
||||
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 ||
|
||||
!bytes.Equal(write.Collectible.SourceManifestSHA256, write.Catalog.SourceManifestSHA256) {
|
||||
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 {
|
||||
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)
|
||||
if err == nil {
|
||||
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")
|
||||
}
|
||||
write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix))
|
||||
brandCollectibleAttributes(write.Models)
|
||||
brandCollectibleAttributes(write.Patterns)
|
||||
brandCollectibleAttributes(write.Backdrops)
|
||||
if err := domain.ValidateStarGiftCollectibleDraft(write); err != nil {
|
||||
return domain.StarGiftCollectibleRevision{}, err
|
||||
}
|
||||
|
|
@ -342,6 +353,12 @@ func (s *Service) CreateCollectibleRevision(ctx context.Context, write domain.St
|
|||
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 {
|
||||
for i := range attributes {
|
||||
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"
|
||||
"math"
|
||||
"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.`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package domain
|
||||
|
||||
import "telesrv/internal/branding"
|
||||
|
||||
const (
|
||||
// OfficialSystemUserID 是 Telegram 兼容客户端识别的官方系统账号。
|
||||
OfficialSystemUserID int64 = 777000
|
||||
|
|
@ -27,8 +29,8 @@ func OfficialSystemUser() User {
|
|||
ID: OfficialSystemUserID,
|
||||
AccessHash: 6599886787491911851,
|
||||
Phone: "42777",
|
||||
FirstName: "Telegram",
|
||||
Username: "telegram",
|
||||
FirstName: branding.ProductName,
|
||||
Username: branding.ProductUsername,
|
||||
Verified: true,
|
||||
Support: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
)
|
||||
|
||||
const manifestSchema = 2
|
||||
|
|
@ -299,7 +301,7 @@ func (c *Catalog) List(ctx context.Context) ([]GiftSummary, error) {
|
|||
gift := snap.gifts[id]
|
||||
doc := snap.documents[gift.DocumentIDs[0]]
|
||||
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,
|
||||
AvailabilityRemains: gift.AvailabilityRemains, AvailabilityResale: gift.AvailabilityResale,
|
||||
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,
|
||||
ResellMinStars: gift.ResellMinStars, PerUserTotal: gift.PerUserTotal,
|
||||
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,
|
||||
Background: cloneBackground(gift.Background), DocumentID: doc.ID64,
|
||||
AnimationValidated: doc.AnimationValidated,
|
||||
|
|
@ -342,7 +344,7 @@ func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible b
|
|||
out := Bundle{
|
||||
ManifestSHA256: append([]byte(nil), snap.manifestSHA...),
|
||||
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,
|
||||
AvailabilityRemains: gift.AvailabilityRemains, Limited: gift.Limited, SoldOut: gift.SoldOut,
|
||||
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,
|
||||
LastSaleDate: gift.LastSaleDate, ResellMinStars: gift.ResellMinStars,
|
||||
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,
|
||||
UpgradeVariants: gift.UpgradeVariants, Background: cloneBackground(gift.Background),
|
||||
DocumentID: gift.DocumentIDs[0]},
|
||||
|
|
@ -372,17 +374,17 @@ func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible b
|
|||
if err != nil {
|
||||
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 {
|
||||
doc, err := c.readDocument(ctx, snap.documents[value.DocumentID])
|
||||
if err != nil {
|
||||
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 {
|
||||
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,
|
||||
TextColor: value.TextColor, Rarity: value.Rarity})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func TestCatalogVerifiesSelectedDocument(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
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[:])}}},
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
|
|
@ -33,11 +33,11 @@ func TestCatalogVerifiesSelectedDocument(t *testing.T) {
|
|||
}
|
||||
catalog := New(root)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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/tlprofile"
|
||||
"telesrv/internal/branding"
|
||||
ioscompat "telesrv/internal/compat/ios"
|
||||
"telesrv/internal/compat/tdesktop"
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -1889,12 +1890,12 @@ func tgAuthorization(a domain.Authorization, currentAuthKeyID [8]byte, now int)
|
|||
Current: a.AuthKeyID == currentAuthKeyID,
|
||||
OfficialApp: true,
|
||||
Hash: a.Hash,
|
||||
DeviceModel: a.DeviceModel,
|
||||
Platform: a.Platform,
|
||||
SystemVersion: a.SystemVersion,
|
||||
DeviceModel: branding.UserVisibleText(a.DeviceModel, ""),
|
||||
Platform: branding.UserVisibleClientPlatform(a.Platform),
|
||||
SystemVersion: branding.UserVisibleText(a.SystemVersion, ""),
|
||||
APIID: a.APIID,
|
||||
AppName: "Telegram Desktop",
|
||||
AppVersion: a.AppVersion,
|
||||
AppName: branding.ClientAppName(a.Platform),
|
||||
AppVersion: branding.UserVisibleText(a.AppVersion, ""),
|
||||
DateCreated: created,
|
||||
DateActive: active,
|
||||
IP: a.IP,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ func (r *Router) resolveAIComposeStyleWebPage(ctx context.Context, rawURL string
|
|||
Hash: aiComposeToneWebPageHash(tone),
|
||||
Date: int(now.Unix()),
|
||||
Type: aiComposeToneWebPageType,
|
||||
SiteName: "Telegram",
|
||||
SiteName: branding.ProductName,
|
||||
Title: tone.Title,
|
||||
Description: tone.Prompt,
|
||||
ComposeToneEmojiID: tone.EmojiID,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
"telesrv/internal/app/auth"
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -926,13 +927,13 @@ func (r *Router) tgSignInServiceNotification(ctx context.Context, u domain.User,
|
|||
if ci, ok := ClientInfoFrom(ctx); ok {
|
||||
parts := []string{}
|
||||
if ci.DeviceModel != "" {
|
||||
parts = append(parts, ci.DeviceModel)
|
||||
parts = append(parts, branding.UserVisibleText(ci.DeviceModel, ""))
|
||||
}
|
||||
if ci.SystemVersion != "" {
|
||||
parts = append(parts, ci.SystemVersion)
|
||||
parts = append(parts, branding.UserVisibleText(ci.SystemVersion, ""))
|
||||
}
|
||||
if ci.AppVersion != "" {
|
||||
parts = append(parts, ci.AppVersion)
|
||||
parts = append(parts, branding.UserVisibleText(ci.AppVersion, ""))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
client = strings.Join(parts, " / ")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
"telesrv/internal/branding"
|
||||
androidcompat "telesrv/internal/compat/android"
|
||||
ioscompat "telesrv/internal/compat/ios"
|
||||
"telesrv/internal/compat/tdesktop"
|
||||
|
|
@ -21,7 +22,7 @@ func (r *Router) registerHelp(d *tlprofile.Dispatcher) {
|
|||
return tdesktop.NearestDC(r.cfg.DC), nil
|
||||
})
|
||||
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) {
|
||||
return r.onHelpSaveAppLog(ctx)
|
||||
|
|
@ -182,7 +183,7 @@ func (r *Router) onHelpDismissSuggestion(ctx context.Context, req *tg.HelpDismis
|
|||
// 六个字段全是 TL 必填项,空值也必须给出空集合而非缺失。
|
||||
func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumPromo, error) {
|
||||
promo := &tg.HelpPremiumPromo{
|
||||
StatusText: "Telegram Premium is not active on this account.",
|
||||
StatusText: branding.PremiumName + " is not active on this account.",
|
||||
StatusEntities: []tg.MessageEntityClass{},
|
||||
VideoSections: []string{},
|
||||
Videos: []tg.DocumentClass{},
|
||||
|
|
@ -199,7 +200,7 @@ func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumProm
|
|||
}
|
||||
if u.PremiumActiveAt(r.clock.Now().Unix()) {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ func TestPaymentsGetStarsRevenueAdsAccountURLReturnsCompatURLAndValidatesPeer(t
|
|||
if !ok {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil {
|
||||
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) {
|
||||
return r.onPaymentsGetStarsRevenueStats(ctx, req)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/iamxvbaba/td/tgerr"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -324,11 +325,11 @@ func (r *Router) starsTopupPaymentForm(userID int64, purpose *tg.InputStorePayme
|
|||
return &tg.PaymentsPaymentFormStars{
|
||||
FormID: starsTopupFormID(userID, purpose.Stars, purpose.Currency, purpose.Amount),
|
||||
BotID: domain.OfficialSystemUserID,
|
||||
Title: "Telegram Stars",
|
||||
Title: branding.StarsName,
|
||||
Description: "telesrv dev Stars top-up",
|
||||
Invoice: tg.Invoice{
|
||||
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()}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ func TestSignInServiceNotificationMatchesEnterpriseShape(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
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) {
|
||||
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",
|
||||
"notes": {
|
||||
"server": "official",
|
||||
|
|
@ -3788,7 +3788,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "filename",
|
||||
"file_name": "background telegram 003.jpg"
|
||||
"file_name": "background telesrv 003.jpg"
|
||||
}
|
||||
],
|
||||
"thumbs": [
|
||||
|
|
@ -5834,4 +5834,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,9 +66,29 @@ func ensureOfficialSystemUserWithDB(ctx context.Context, db sqlcgen.DBTX, msg do
|
|||
return nil
|
||||
}
|
||||
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)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
WITH upserted AS (
|
||||
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support, about, is_bot, bot_info_version)
|
||||
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 {
|
||||
return fmt.Errorf("ensure official system user: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,28 +23,30 @@ import (
|
|||
"math/big"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
)
|
||||
|
||||
// 验证错误。调用方据此映射到 TL 错误码/拒绝登录。
|
||||
var (
|
||||
ErrClientDataInvalid = errors.New("webauthn: client data invalid")
|
||||
ErrChallengeMismatch = errors.New("webauthn: challenge mismatch")
|
||||
ErrOriginNotAllowed = errors.New("webauthn: origin not allowed")
|
||||
ErrRPIDMismatch = errors.New("webauthn: rp id hash mismatch")
|
||||
ErrUserNotPresent = errors.New("webauthn: user-present flag not set")
|
||||
ErrAuthDataInvalid = errors.New("webauthn: authenticator data invalid")
|
||||
ErrAttestationInvalid = errors.New("webauthn: attestation object invalid")
|
||||
ErrClientDataInvalid = errors.New("webauthn: client data invalid")
|
||||
ErrChallengeMismatch = errors.New("webauthn: challenge mismatch")
|
||||
ErrOriginNotAllowed = errors.New("webauthn: origin not allowed")
|
||||
ErrRPIDMismatch = errors.New("webauthn: rp id hash mismatch")
|
||||
ErrUserNotPresent = errors.New("webauthn: user-present flag not set")
|
||||
ErrAuthDataInvalid = errors.New("webauthn: authenticator data invalid")
|
||||
ErrAttestationInvalid = errors.New("webauthn: attestation object invalid")
|
||||
ErrPublicKeyUnsupported = errors.New("webauthn: unsupported public key algorithm")
|
||||
ErrSignatureInvalid = errors.New("webauthn: signature invalid")
|
||||
ErrCounterRegressed = errors.New("webauthn: sign counter regressed (possible cloned authenticator)")
|
||||
ErrSignatureInvalid = errors.New("webauthn: signature invalid")
|
||||
ErrCounterRegressed = errors.New("webauthn: sign counter regressed (possible cloned authenticator)")
|
||||
)
|
||||
|
||||
// authenticatorData 标志位(WebAuthn §6.1)。
|
||||
const (
|
||||
flagUserPresent = 0x01
|
||||
flagUserVerified = 0x04
|
||||
flagAttestedCredData = 0x40
|
||||
flagExtensionData = 0x80
|
||||
flagUserPresent = 0x01
|
||||
flagUserVerified = 0x04
|
||||
flagAttestedCredData = 0x40
|
||||
flagExtensionData = 0x80
|
||||
)
|
||||
|
||||
// COSE algorithm identifiers。
|
||||
|
|
@ -55,8 +57,8 @@ const (
|
|||
|
||||
// COSE key type / curve。
|
||||
const (
|
||||
coseKtyOKP = 1
|
||||
coseKtyEC2 = 2
|
||||
coseKtyOKP = 1
|
||||
coseKtyEC2 = 2
|
||||
coseCrvP256 = 1
|
||||
coseCrvEd25519 = 6
|
||||
)
|
||||
|
|
@ -76,7 +78,7 @@ func decodeB64URL(s string) ([]byte, error) {
|
|||
type RegistrationParams struct {
|
||||
RPID string
|
||||
RPName string
|
||||
UserID []byte // 通常是 "dcId:userId" 的字节
|
||||
UserID []byte // 通常是 "dcId:userId" 的字节
|
||||
UserName string
|
||||
UserDisplay string
|
||||
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)})
|
||||
}
|
||||
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)},
|
||||
"challenge": b64.EncodeToString(p.Challenge),
|
||||
"pubKeyCredParams": []map[string]any{
|
||||
|
|
@ -199,12 +201,12 @@ func ChallengeFromClientData(clientDataJSON []byte) ([]byte, error) {
|
|||
|
||||
// parsedAuthData 是 authenticatorData 的解析结果。
|
||||
type parsedAuthData struct {
|
||||
rpIDHash []byte
|
||||
flags byte
|
||||
signCount uint32
|
||||
aaguid []byte
|
||||
credID []byte
|
||||
credPubKey []byte // COSE 公钥原始字节(仅注册时存在)
|
||||
rpIDHash []byte
|
||||
flags byte
|
||||
signCount uint32
|
||||
aaguid []byte
|
||||
credID []byte
|
||||
credPubKey []byte // COSE 公钥原始字节(仅注册时存在)
|
||||
}
|
||||
|
||||
func parseAuthData(authData []byte) (parsedAuthData, error) {
|
||||
|
|
@ -351,8 +353,8 @@ func VerifyAssertion(in VerifyAssertionInput) (uint32, error) {
|
|||
|
||||
// publicKey 抽象 ES256/EdDSA 验签。
|
||||
type publicKey struct {
|
||||
ec *ecdsa.PublicKey
|
||||
ed ed25519.PublicKey
|
||||
ec *ecdsa.PublicKey
|
||||
ed ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (p publicKey) verify(signed, sig []byte) error {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue