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:
A 2026-07-20 16:49:17 +08:00
parent da6a57e1a3
commit 2848ff0987
34 changed files with 449 additions and 91 deletions

View file

@ -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{

View file

@ -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.`

View file

@ -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:

View file

@ -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, ""),
})
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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,

View file

@ -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) {

View file

@ -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