merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -13,6 +13,7 @@ import (
"golang.org/x/text/language"
"telesrv/internal/branding"
"telesrv/internal/domain"
"telesrv/internal/links"
)
@ -62,6 +63,10 @@ type Config struct {
MTProtoRPCGlobalWorkers int
MTProtoRPCGlobalMaxTasks int
MTProtoRPCGlobalMaxBytes int64
// Delivery-hook workers execute post-response correctness transitions. The
// separate pending budget covers reserved + queued + running hooks.
MTProtoRPCDeliveryHookWorkers int
MTProtoRPCDeliveryHookMaxPending int
// Pending ownership and compact completed receipts share three-level
// global/raw-auth/session entry accounting. Result bodies are never cached
// here; the logical-session outbox owns unacknowledged wire bytes.
@ -93,6 +98,13 @@ type Config struct {
// PublicBaseURL 是所有客户端可见 telesrv 链接的公开根 URL。
// 生产默认 https://telesrv.net;本地可设为 http://127.0.0.1:2401。
PublicBaseURL string
// UpdatePublicURL is advertised to native clients through
// help.getConfig.autoupdate_url_prefix. Empty disables desktop updates.
UpdatePublicURL string
// UpdateServiceURL is the internal HTTP endpoint used to resolve
// help.getAppUpdate responses. It defaults to UpdatePublicURL when enabled.
UpdateServiceURL string
UpdateRequestTimeout time.Duration
// PublicAppScheme 是公开落地页自动唤起自建客户端时使用的 URL scheme。
// 必须与 TDesktop/Android 客户端构建时注册的 scheme 一致,且不能占用 tg/http/https。
PublicAppScheme string
@ -255,15 +267,15 @@ type Config struct {
WebPagePreviewRatePerMin int
// LangPackSeedDir 是 TDesktop 语言包 .strings 种子目录。
LangPackSeedDir string
// BlobDir 是本地磁盘 blob backend 根目录(媒体文件字节内容)。
BlobDir string
// BlobBackendKind selects the blob storage backend: "localfs" (default)
// or "s3". Transient upload parts always stay on local disk (BlobDir)
// regardless of this setting -- only the permanent blob store moves.
// BlobBackendKind 选择唯一永久 blob backend:localfs(默认)或 s3。
BlobBackendKind string
// S3Endpoint/S3Region/S3Bucket/S3AccessKeyID/S3SecretAccessKey/S3UseSSL/
// S3PathStyle configure the s3 blob backend; only used when
// BlobBackendKind == "s3". Works against self-hosted MinIO or AWS S3.
// BlobDir 是 localfs 永久媒体根目录。s3 模式不从这里回退读取。
BlobDir string
// BlobStagingDir 是 s3 模式的本地临时上传分片与写入 spool 根目录;
// 它不是永久 backend,成功组装后的媒体只存在 S3。
BlobStagingDir string
// S3* 配置 MinIO/AWS S3 兼容永久 backend。endpoint 不含 URL scheme;
// access key/secret 没有默认值,CreateBucket 默认关闭。
S3Endpoint string
S3Region string
S3Bucket string
@ -271,6 +283,7 @@ type Config struct {
S3SecretAccessKey string
S3UseSSL bool
S3PathStyle bool
S3CreateBucket bool
// StorageLowSpaceGuardEnable turns on the pre-upload free-space check.
StorageLowSpaceGuardEnable bool
// StorageMinFreeBytes: for the localfs backend, reject new uploads once
@ -338,19 +351,117 @@ type Config struct {
TranslationRateWindow time.Duration
// TempKeyResolveCacheMaxEntries 是 Router temp→perm 解析缓存容量。
TempKeyResolveCacheMaxEntries int
// TempKeyResolveCacheTTL 是 temp→perm 绑定的进程内复核周期。绑定/revoke 有精确
// 失效,TTL 作为跨进程或异常路径兜底;默认 30m 避免大连接数下每 5s 全量打 PG。
// TempKeyResolveCacheTTL 是 temp→perm 正绑定的进程内复核周期。bind 成功投影已提交
// 映射,revoke/destroy 精确失效;TTL 作为跨进程或异常路径兜底。
TempKeyResolveCacheTTL time.Duration
// ReadModelVersionCacheMaxEntries 是 durable read-model hash 的进程内 LRU
// 容量。它由统一 NOTIFY 流更新/失效;达到上限只逐项驱逐,禁止整表清空。
ReadModelVersionCacheMaxEntries int
// ReadModelVersionBatch* bounds synchronous cross-request batching for exact
// cache misses. Each caller still waits for its complete durable hash set;
// errors never fall back to independent PostgreSQL reads.
ReadModelVersionBatchMaxKeys int
ReadModelVersionBatchWait time.Duration
ReadModelVersionBatchQueue int
ReadModelVersionBatchTimeout time.Duration
// AuthKeyGetBatch* bounds synchronous first-frame auth-key lookup/touch
// batching. Every accepted connection waits for its durable activity lease;
// queue/query failures close that connection rather than bypassing the batcher.
AuthKeyGetBatchMax int
AuthKeyGetBatchWait time.Duration
AuthKeyGetBatchQueue int
AuthKeyGetBatchTimeout time.Duration
// ContactReverseBatch* bounds synchronous exact owner->viewer relationship
// batching for privacy projection. It never broadens pairs or falls back to
// per-request PostgreSQL reads when overloaded.
ContactReverseBatchMaxPairs int
ContactReverseBatchWait time.Duration
ContactReverseBatchQueue int
ContactReverseBatchTimeout time.Duration
// ContactSnapshotCacheMaxViewers bounds owner contact-list and personal-photo
// snapshots independently. Both use exact-viewer LRU eviction.
ContactSnapshotCacheMaxViewers int
// ProfilePhotoCache* bounds the owner-only profile/fallback ref LRU. Exact
// profile_photo NOTIFY is primary freshness; TTL is only a missed-event guard.
ProfilePhotoCacheMaxEntries int
ProfilePhotoCacheTTL time.Duration
// PeerIdentityCacheMaxEntries 是合并后的 peer username/third-party
// verification viewer-independent 版本化 LRU 容量。
PeerIdentityCacheMaxEntries int
// DialogPrivatePeerCache* 控制 private getPeerDialogs 结构事实缓存;频道
// projection 由独立 ChannelDialogCache 承载,禁止在 Service 复制。
DialogPrivatePeerCacheMaxEntries int
DialogPrivatePeerCacheMaxBytes int64
// DialogDraftCache* 控制按 dialog_light 版本校验的 cloud-draft 正/负缓存。
DialogDraftCacheMaxEntries int
DialogDraftCacheMaxBytes int64
// UserProjectionFactCacheMaxEntries 是 freeze 与 collectible-phone
// viewer-independent durable fact 两个 LRU 各自的条目上限。
UserProjectionFactCacheMaxEntries int
// StoryActivePeerCacheMaxEntries 控制共享 active-story candidate;hidden
// preference 按 viewer 稀疏集合缓存,并同时受条目与估算字节上限约束。
StoryActivePeerCacheMaxEntries int
StoryHiddenListCacheMaxEntries int
StoryHiddenListCacheMaxBytes int64
// DialogListSnapshotCache* 控制 materialized owner dialog 工作集。条目数与
// header-equivalent 总权重同时受限,避免 10k owner 或单个超大账号把 heap 推成无界。
DialogListSnapshotCacheMaxEntries int
DialogListSnapshotCacheMaxHeaders int64
DialogListSnapshotCacheTTL time.Duration
// DialogListSnapshotRedisTTL controls the cross-process, version-addressed
// materialized owner snapshot lifetime. Correctness comes from durable read-model
// generations rather than this TTL.
DialogListSnapshotRedisTTL time.Duration
// ActiveChannelIDs* controls the session-readiness owner membership page.
// L1 and Redis share the exact durable-generation/page identity; Redis miss
// uses a bounded synchronous multi-owner PostgreSQL batch.
ActiveChannelIDsCacheMaxEntries int
ActiveChannelIDsCacheTTL time.Duration
ActiveChannelIDsRedisTTL time.Duration
ActiveChannelIDsBatchMax int
ActiveChannelIDsBatchWait time.Duration
ActiveChannelIDsBatchQueue int
ActiveChannelIDsBatchTimeout time.Duration
// LayerAdvanceBatch* bounds synchronous PostgreSQL batching of distinct
// raw-session same-Layer watermark advances. Every selector remains an
// independent durable input and waits for its batch result before admission.
LayerAdvanceBatchMax int
LayerAdvanceBatchWait time.Duration
LayerAdvanceBatchQueue int
LayerAdvanceBatchTimeout time.Duration
// BootstrapReadyBatch* bounds synchronous post-response readiness marking.
// Every accepted selector waits for the shared PostgreSQL result; this is
// not an asynchronous or best-effort delivery path.
BootstrapReadyBatchMax int
BootstrapReadyBatchWait time.Duration
BootstrapReadyBatchQueue int
BootstrapReadyBatchTimeout time.Duration
// PresenceLastSeenBatch* bounds the coalescing asynchronous lifecycle
// presence writer. Explicit account.updateStatus is not batched.
PresenceLastSeenBatchMax int
PresenceLastSeenBatchWait time.Duration
PresenceLastSeenBatchQueue int
PresenceLastSeenBatchTimeout time.Duration
PresenceLastSeenDrainTimeout time.Duration
// ChannelRowCacheMaxEntries 是「共享频道行」进程内缓存容量(channelID→domain.Channel)。
// 由 channels 表 LISTEN/NOTIFY 触发器实时失效(强一致、零 TTL)。<=0 禁用缓存与监听。
ChannelRowCacheMaxEntries int
// ChannelTopMessageCacheMaxEntries 是 dialog 顶部频道消息共享缓存容量。
// viewer 的 read/reaction overlay 不入缓存;channel_base 事件按频道失效。
ChannelTopMessageCacheMaxEntries int
// ChannelMemberCacheMaxEntries 是频道成员/访问态 read-model 缓存容量((channelID,userID)→member)。
// 由 read_model_versions 统一通知实时失效。<=0 禁用缓存。
ChannelMemberCacheMaxEntries int
// ChannelDialogCacheMaxEntries 是频道 dialog 读投影缓存容量((viewerUserID,channelID)→dialog)。
// 由 channel_base/channel_member/dialog_light 统一通知实时失效。<=0 禁用缓存。
ChannelDialogCacheMaxEntries int
// ChannelDifferenceCache* 控制 viewer-independent difference 基础页工作集。
// entry/估算 bytes 双重有界;TTL 只是带外写安全兜底,正常正确性由
// channel_base/channel_difference_base 通知与稳定切面 key 保证。
ChannelDifferenceCacheMaxEntries int
ChannelDifferenceCacheMaxBytes int64
ChannelDifferenceCacheTTL time.Duration
// ChannelBoostCacheMaxEntries 是频道 boost read-model 缓存容量,覆盖当前用户
// SelfBoostsApplied 与频道总 active boost 数两类投影。写入 channel_boost_slots
// 时精确失效,TTL 兜底自然过期。<=0 禁用缓存。
@ -626,6 +737,20 @@ func Load() (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("TELESRV_PUBLIC_BASE_URL: %w", err)
}
updatePublicURL := strings.TrimSpace(envAllowEmptyOr("TELESRV_UPDATE_PUBLIC_URL", ""))
if updatePublicURL != "" {
updatePublicURL, err = links.ValidateBaseURL(updatePublicURL)
if err != nil {
return Config{}, fmt.Errorf("TELESRV_UPDATE_PUBLIC_URL: %w", err)
}
}
updateServiceURL := strings.TrimSpace(envOr("TELESRV_UPDATE_SERVICE_URL", updatePublicURL))
if updateServiceURL != "" {
updateServiceURL, err = links.ValidateBaseURL(updateServiceURL)
if err != nil {
return Config{}, fmt.Errorf("TELESRV_UPDATE_SERVICE_URL: %w", err)
}
}
publicAppScheme, err := links.ValidateAppScheme(envOr("TELESRV_PUBLIC_APP_SCHEME", links.DefaultAppScheme))
if err != nil {
return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_SCHEME: %w", err)
@ -663,7 +788,6 @@ func Load() (Config, error) {
if err != nil {
return Config{}, err
}
cfg := Config{
ListenAddr: envOr("TELESRV_LISTEN", "0.0.0.0:2398"),
WebSocketEnable: envBoolOr("TELESRV_WEBSOCKET_ENABLE", true),
@ -685,8 +809,10 @@ func Load() (Config, error) {
MTProtoRPCQueueSize: envIntOr("TELESRV_MTPROTO_RPC_QUEUE_SIZE", 64),
MTProtoRPCTimeout: envDurationOr("TELESRV_MTPROTO_RPC_TIMEOUT", 30*time.Second),
MTProtoRPCGlobalWorkers: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_WORKERS", 256),
MTProtoRPCGlobalMaxTasks: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", 8192),
MTProtoRPCGlobalMaxTasks: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", 32768),
MTProtoRPCGlobalMaxBytes: envInt64Or("TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES", 512<<20),
MTProtoRPCDeliveryHookWorkers: envIntOr("TELESRV_MTPROTO_RPC_DELIVERY_HOOK_WORKERS", 32),
MTProtoRPCDeliveryHookMaxPending: envIntOr("TELESRV_MTPROTO_RPC_DELIVERY_HOOK_MAX_PENDING", 16_384),
MTProtoRPCExecutionMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", 1<<18),
MTProtoRPCExecutionAuthMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES", 1<<15),
MTProtoRPCExecutionSessionMaxEntries: envIntOr(
@ -703,6 +829,9 @@ func Load() (Config, error) {
AdminAPIAddr: envAllowEmptyOr("TELESRV_ADMIN_API_ADDR", ""),
AdminAPIToken: envOr("TELESRV_ADMIN_API_TOKEN", ""),
PublicBaseURL: publicBaseURL,
UpdatePublicURL: updatePublicURL,
UpdateServiceURL: updateServiceURL,
UpdateRequestTimeout: envDurationOr("TELESRV_UPDATE_REQUEST_TIMEOUT", 2*time.Second),
PublicAppScheme: publicAppScheme,
PublicAppLinkBase: publicAppLinkBase,
PublicWebBaseURL: publicWebBaseURL,
@ -713,32 +842,32 @@ func Load() (Config, error) {
ReservedUsernames: envListOr("TELESRV_RESERVED_USERNAMES", []string{
"owpengram", "admin", "administrator", "support", "staff", "moderator", "official", "root", "owner",
}),
PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""),
TelegramLoginEnabled: envBoolOr("TELESRV_TELEGRAM_LOGIN_ENABLE", false),
TelegramLoginIssuer: strings.TrimSuffix(envOr("TELESRV_TELEGRAM_LOGIN_ISSUER", publicBaseURL), "/"),
TelegramLoginAllowHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", false),
TelegramLoginSigningKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "data/telegram-login/signing-keys.json"),
TelegramLoginCodeKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "data/telegram-login/code-keys.json"),
TelegramLoginSecretPepperFile: envOr("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "data/telegram-login/client-secret-pepper"),
TelegramLoginRequestTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_REQUEST_TTL", 5*time.Minute),
TelegramLoginCodeTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_CODE_TTL", 2*time.Minute),
TelegramLoginIDTokenTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL", time.Hour),
TelegramLoginTrustedProxyCIDRs: envListOr("TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS", nil),
TelegramLoginRetention: envDurationOr("TELESRV_TELEGRAM_LOGIN_RETENTION", 7*24*time.Hour),
TelegramLoginSweepInterval: envDurationOr("TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL", 5*time.Minute),
TelegramLoginSweepBatch: envIntOr("TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH", 500),
AdminUIPermissions: envListOr("TELESRV_ADMIN_UI_PERMISSIONS", []string{adminPermissionAll}),
AdminScopedTokens: adminScopedTokens,
AdminUIAddr: envOr("TELESRV_ADMIN_UI_ADDR", "127.0.0.1:2600"),
AdminUIPassword: envOr("TELESRV_ADMIN_UI_PASSWORD", ""),
AdminUIToken: envOr("TELESRV_ADMIN_UI_TOKEN", ""),
AdminSessionKey: envOr("TELESRV_ADMIN_SESSION_KEY", ""),
PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""),
TelegramLoginEnabled: envBoolOr("TELESRV_TELEGRAM_LOGIN_ENABLE", false),
TelegramLoginIssuer: strings.TrimSuffix(envOr("TELESRV_TELEGRAM_LOGIN_ISSUER", publicBaseURL), "/"),
TelegramLoginAllowHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", false),
TelegramLoginSigningKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "data/telegram-login/signing-keys.json"),
TelegramLoginCodeKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "data/telegram-login/code-keys.json"),
TelegramLoginSecretPepperFile: envOr("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "data/telegram-login/client-secret-pepper"),
TelegramLoginRequestTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_REQUEST_TTL", 5*time.Minute),
TelegramLoginCodeTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_CODE_TTL", 2*time.Minute),
TelegramLoginIDTokenTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL", time.Hour),
TelegramLoginTrustedProxyCIDRs: envListOr("TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS", nil),
TelegramLoginRetention: envDurationOr("TELESRV_TELEGRAM_LOGIN_RETENTION", 7*24*time.Hour),
TelegramLoginSweepInterval: envDurationOr("TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL", 5*time.Minute),
TelegramLoginSweepBatch: envIntOr("TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH", 500),
AdminUIPermissions: envListOr("TELESRV_ADMIN_UI_PERMISSIONS", []string{adminPermissionAll}),
AdminScopedTokens: adminScopedTokens,
AdminUIAddr: envOr("TELESRV_ADMIN_UI_ADDR", "127.0.0.1:2600"),
AdminUIPassword: envOr("TELESRV_ADMIN_UI_PASSWORD", ""),
AdminUIToken: envOr("TELESRV_ADMIN_UI_TOKEN", ""),
AdminSessionKey: envOr("TELESRV_ADMIN_SESSION_KEY", ""),
// 用 127.0.0.1 而非 localhost:localhost 在 Windows 上会先解析到 IPv6 ::1,而 Docker
// Desktop 的端口转发只在 IPv4 监听,IPv6 连接要等 ~1s 超时才回退 IPv4(实测 localhost
// 建连 1.0s vs 127.0.0.1 6ms)。冷连接洪峰下池扩容的新连接各等 1s → pre-handler 惊群卡顿。
// 生产由 TELESRV_POSTGRES_DSN 覆盖;该默认值仅作用于本地开发。
PostgresDSN: envOr("TELESRV_POSTGRES_DSN", "postgres://telesrv:telesrv@127.0.0.1:5432/telesrv?sslmode=disable"),
PostgresDSN: envOr("TELESRV_POSTGRES_DSN", "postgres://telesrv:telesrv@127.0.0.1:5432/telesrv_main?sslmode=disable"),
PostgresMaxConns: envIntOr("TELESRV_POSTGRES_MAX_CONNS", 50),
PostgresMinConns: envIntOr("TELESRV_POSTGRES_MIN_CONNS", 16),
RedisAddr: envOr("TELESRV_REDIS_ADDR", "127.0.0.1:6399"), // 同理避开 localhost→IPv6 回退延迟
@ -767,63 +896,118 @@ func Load() (Config, error) {
SMTPUsername: envOr("TELESRV_SMTP_USERNAME", ""),
SMTPPassword: envOr("TELESRV_SMTP_PASSWORD", ""),
SMTPFrom: envOr("TELESRV_SMTP_FROM", ""),
SMTPFromName: envOr("TELESRV_SMTP_FROM_NAME", "OwpenGram"),
SMTPFromName: envOr("TELESRV_SMTP_FROM_NAME", branding.ProductName),
SMTPTLSMode: strings.ToLower(strings.TrimSpace(envOr("TELESRV_SMTP_TLS", "starttls"))),
SMTPTimeout: envDurationOr("TELESRV_SMTP_TIMEOUT", 10*time.Second),
LangPackSeedDir: envOr("TELESRV_LANGPACK_SEED_DIR", "data/langpack"),
BlobDir: envOr("TELESRV_BLOB_DIR", "data/blobs"),
// s3 (MinIO by default, see deploy/docker-compose.yml's minio service) is
// the default blob backend; localfs remains fully supported as an
// explicit opt-in (TELESRV_BLOB_BACKEND=localfs).
BlobBackendKind: strings.ToLower(strings.TrimSpace(envOr("TELESRV_BLOB_BACKEND", "s3"))),
S3Endpoint: envOr("TELESRV_S3_ENDPOINT", "127.0.0.1:9000"), // 同理避开 localhost→IPv6 回退延迟
S3Region: envOr("TELESRV_S3_REGION", "us-east-1"),
S3Bucket: envOr("TELESRV_S3_BUCKET", "owpengram-media"),
S3AccessKeyID: envOr("TELESRV_S3_ACCESS_KEY_ID", "owpengram"),
S3SecretAccessKey: envOr("TELESRV_S3_SECRET_ACCESS_KEY", "owpengram123"),
S3UseSSL: envBoolOr("TELESRV_S3_USE_SSL", false),
S3PathStyle: envBoolOr("TELESRV_S3_PATH_STYLE", true),
StorageLowSpaceGuardEnable: envBoolOr("TELESRV_STORAGE_LOW_SPACE_GUARD_ENABLE", true),
StorageMinFreeBytes: envInt64Or("TELESRV_STORAGE_MIN_FREE_BYTES", 1<<30),
StorageMaxTotalBytes: envInt64Or("TELESRV_STORAGE_MAX_TOTAL_BYTES", 0),
StorageUsageRefreshInterval: envDurationOr("TELESRV_STORAGE_USAGE_REFRESH_INTERVAL", time.Minute),
StorageRetentionEnable: envBoolOr("TELESRV_STORAGE_RETENTION_ENABLE", false),
StorageRetentionMaxAge: envDurationOr("TELESRV_STORAGE_RETENTION_MAX_AGE", 30*24*time.Hour),
StickerSeedDir: envOr("TELESRV_STICKER_SEED_DIR", "data/sticker-seed"),
StickerSeedMaxSets: envIntOr("TELESRV_STICKER_SEED_MAX_SETS", 300),
PremiumPromoSeedDir: envOr("TELESRV_PREMIUM_PROMO_SEED_DIR", "data/premium-promo"),
GifSeedDir: envOr("TELESRV_GIF_SEED_DIR", "data/gifs"),
IdentityDir: envOr("TELESRV_IDENTITY_DIR", "data/identity"),
MapboxToken: envOr("TELESRV_MAPBOX_TOKEN", ""),
MapTileCacheDir: envOr("TELESRV_MAPTILE_CACHE_DIR", "data/maptiles"),
ExternalMediaEnable: envBoolOr("TELESRV_EXTERNAL_MEDIA_ENABLE", true),
ExternalMediaMaxBytes: int64(envIntOr("TELESRV_EXTERNAL_MEDIA_MAX_BYTES", 10<<20)),
ExternalMediaRatePerMin: envIntOr("TELESRV_EXTERNAL_MEDIA_RATE_PER_MIN", 60),
WebPagePreviewEnable: envBoolOr("TELESRV_WEBPAGE_PREVIEW_ENABLE", true),
WebPagePreviewMaxBytes: int64(envIntOr("TELESRV_WEBPAGE_PREVIEW_MAX_BYTES", 5<<20)),
WebPagePreviewRatePerMin: envIntOr("TELESRV_WEBPAGE_PREVIEW_RATE_PER_MIN", 300),
BusinessAIProvider: envOr("TELESRV_BUSINESS_AI_PROVIDER", "echo"),
AIEnabled: envBoolOr("TELESRV_AI_ENABLED", true),
AIProviders: loadAIProviders(fileEnv),
AITimeout: envDurationOr("TELESRV_AI_TIMEOUT", 15*time.Second),
AIRateLimit: envIntOr("TELESRV_AI_RATE_LIMIT", 20),
AIRateWindow: envDurationOr("TELESRV_AI_RATE_WINDOW", time.Minute),
AIPrivacyLogContent: envBoolOr("TELESRV_AI_LOG_CONTENT", false),
TranslationEnabled: envBoolOr("TELESRV_TRANSLATION_ENABLED", true),
TranslationProviders: envListOr("TELESRV_TRANSLATION_PROVIDERS", []string{}),
TranslationTimeout: envDurationOr("TELESRV_TRANSLATION_TIMEOUT", 15*time.Second),
TranslationRateLimit: envIntOr("TELESRV_TRANSLATION_RATE_LIMIT", 60),
TranslationRateWindow: envDurationOr("TELESRV_TRANSLATION_RATE_WINDOW", time.Minute),
TempKeyResolveCacheMaxEntries: envIntOr("TELESRV_TEMP_KEY_CACHE_MAX_ENTRIES", 262144),
TempKeyResolveCacheTTL: envDurationOr("TELESRV_TEMP_KEY_CACHE_TTL", 30*time.Minute),
ChannelRowCacheMaxEntries: envIntOr("TELESRV_CHANNEL_ROW_CACHE_MAX", 50000),
ChannelMemberCacheMaxEntries: envIntOr("TELESRV_CHANNEL_MEMBER_CACHE_MAX", 100000),
ChannelDialogCacheMaxEntries: envIntOr("TELESRV_CHANNEL_DIALOG_CACHE_MAX", 100000),
ChannelBoostCacheMaxEntries: envIntOr("TELESRV_CHANNEL_BOOST_CACHE_MAX", 100000),
ChannelBoostCacheTTL: envDurationOr("TELESRV_CHANNEL_BOOST_CACHE_TTL", 10*time.Second),
BlobBackendKind: strings.ToLower(strings.TrimSpace(envOr("TELESRV_BLOB_BACKEND", "s3"))),
BlobDir: envOr("TELESRV_BLOB_DIR", "data/blobs"),
BlobStagingDir: envOr("TELESRV_BLOB_STAGING_DIR", "data/blob-staging"),
S3Endpoint: envOr("TELESRV_S3_ENDPOINT", "127.0.0.1:9000"), // 同理避开 localhost→IPv6 回退延迟
S3Region: envOr("TELESRV_S3_REGION", "us-east-1"),
S3Bucket: envOr("TELESRV_S3_BUCKET", "owpengram-media"),
S3AccessKeyID: envOr("TELESRV_S3_ACCESS_KEY_ID", "owpengram"),
S3SecretAccessKey: envOr("TELESRV_S3_SECRET_ACCESS_KEY", "owpengram123"),
S3UseSSL: envBoolOr("TELESRV_S3_USE_SSL", false),
S3PathStyle: envBoolOr("TELESRV_S3_PATH_STYLE", true),
S3CreateBucket: envBoolOr("TELESRV_S3_CREATE_BUCKET", false),
StorageLowSpaceGuardEnable: envBoolOr("TELESRV_STORAGE_LOW_SPACE_GUARD_ENABLE", true),
StorageMinFreeBytes: envInt64Or("TELESRV_STORAGE_MIN_FREE_BYTES", 1<<30),
StorageMaxTotalBytes: envInt64Or("TELESRV_STORAGE_MAX_TOTAL_BYTES", 0),
StorageUsageRefreshInterval: envDurationOr("TELESRV_STORAGE_USAGE_REFRESH_INTERVAL", time.Minute),
StorageRetentionEnable: envBoolOr("TELESRV_STORAGE_RETENTION_ENABLE", false),
StorageRetentionMaxAge: envDurationOr("TELESRV_STORAGE_RETENTION_MAX_AGE", 30*24*time.Hour),
StickerSeedDir: envOr("TELESRV_STICKER_SEED_DIR", "data/sticker-seed"),
StickerSeedMaxSets: envIntOr("TELESRV_STICKER_SEED_MAX_SETS", 300),
PremiumPromoSeedDir: envOr("TELESRV_PREMIUM_PROMO_SEED_DIR", "data/premium-promo"),
GifSeedDir: envOr("TELESRV_GIF_SEED_DIR", "data/gifs"),
IdentityDir: envOr("TELESRV_IDENTITY_DIR", "data/identity"),
MapboxToken: envOr("TELESRV_MAPBOX_TOKEN", ""),
MapTileCacheDir: envOr("TELESRV_MAPTILE_CACHE_DIR", "data/maptiles"),
ExternalMediaEnable: envBoolOr("TELESRV_EXTERNAL_MEDIA_ENABLE", true),
ExternalMediaMaxBytes: int64(envIntOr("TELESRV_EXTERNAL_MEDIA_MAX_BYTES", 10<<20)),
ExternalMediaRatePerMin: envIntOr("TELESRV_EXTERNAL_MEDIA_RATE_PER_MIN", 60),
WebPagePreviewEnable: envBoolOr("TELESRV_WEBPAGE_PREVIEW_ENABLE", true),
WebPagePreviewMaxBytes: int64(envIntOr("TELESRV_WEBPAGE_PREVIEW_MAX_BYTES", 5<<20)),
WebPagePreviewRatePerMin: envIntOr("TELESRV_WEBPAGE_PREVIEW_RATE_PER_MIN", 300),
BusinessAIProvider: envOr("TELESRV_BUSINESS_AI_PROVIDER", "echo"),
AIEnabled: envBoolOr("TELESRV_AI_ENABLED", true),
AIProviders: loadAIProviders(fileEnv),
AITimeout: envDurationOr("TELESRV_AI_TIMEOUT", 15*time.Second),
AIRateLimit: envIntOr("TELESRV_AI_RATE_LIMIT", 20),
AIRateWindow: envDurationOr("TELESRV_AI_RATE_WINDOW", time.Minute),
AIPrivacyLogContent: envBoolOr("TELESRV_AI_LOG_CONTENT", false),
TranslationEnabled: envBoolOr("TELESRV_TRANSLATION_ENABLED", true),
TranslationProviders: envListOr("TELESRV_TRANSLATION_PROVIDERS", []string{}),
TranslationTimeout: envDurationOr("TELESRV_TRANSLATION_TIMEOUT", 15*time.Second),
TranslationRateLimit: envIntOr("TELESRV_TRANSLATION_RATE_LIMIT", 60),
TranslationRateWindow: envDurationOr("TELESRV_TRANSLATION_RATE_WINDOW", time.Minute),
TempKeyResolveCacheMaxEntries: envIntOr("TELESRV_TEMP_KEY_CACHE_MAX_ENTRIES", 262144),
TempKeyResolveCacheTTL: envDurationOr("TELESRV_TEMP_KEY_CACHE_TTL", 30*time.Minute),
ReadModelVersionCacheMaxEntries: envIntOr("TELESRV_READ_MODEL_VERSION_CACHE_MAX", 1000000),
ReadModelVersionBatchMaxKeys: envIntOr("TELESRV_READ_MODEL_VERSION_BATCH_MAX_KEYS", 4096),
ReadModelVersionBatchWait: envDurationOr("TELESRV_READ_MODEL_VERSION_BATCH_WAIT", 250*time.Microsecond),
ReadModelVersionBatchQueue: envIntOr("TELESRV_READ_MODEL_VERSION_BATCH_QUEUE", 16_384),
ReadModelVersionBatchTimeout: envDurationOr("TELESRV_READ_MODEL_VERSION_BATCH_TIMEOUT", 5*time.Second),
AuthKeyGetBatchMax: envIntOr("TELESRV_AUTH_KEY_GET_BATCH_MAX", 256),
AuthKeyGetBatchWait: envDurationOr("TELESRV_AUTH_KEY_GET_BATCH_WAIT", 250*time.Microsecond),
AuthKeyGetBatchQueue: envIntOr("TELESRV_AUTH_KEY_GET_BATCH_QUEUE", 16_384),
AuthKeyGetBatchTimeout: envDurationOr("TELESRV_AUTH_KEY_GET_BATCH_TIMEOUT", 5*time.Second),
ContactReverseBatchMaxPairs: envIntOr("TELESRV_CONTACT_REVERSE_BATCH_MAX_PAIRS", 4096),
ContactReverseBatchWait: envDurationOr("TELESRV_CONTACT_REVERSE_BATCH_WAIT", 2*time.Millisecond),
ContactReverseBatchQueue: envIntOr("TELESRV_CONTACT_REVERSE_BATCH_QUEUE", 16_384),
ContactReverseBatchTimeout: envDurationOr("TELESRV_CONTACT_REVERSE_BATCH_TIMEOUT", 5*time.Second),
ContactSnapshotCacheMaxViewers: envIntOr("TELESRV_CONTACT_SNAPSHOT_CACHE_MAX_VIEWERS", 16_384),
ProfilePhotoCacheMaxEntries: envIntOr("TELESRV_PROFILE_PHOTO_CACHE_MAX", 200_000),
ProfilePhotoCacheTTL: envDurationOr("TELESRV_PROFILE_PHOTO_CACHE_TTL", 24*time.Hour),
PeerIdentityCacheMaxEntries: envIntOr("TELESRV_PEER_IDENTITY_CACHE_MAX", 1000000),
DialogPrivatePeerCacheMaxEntries: envIntOr("TELESRV_DIALOG_PRIVATE_PEER_CACHE_MAX", 500000),
DialogPrivatePeerCacheMaxBytes: int64(envIntOr("TELESRV_DIALOG_PRIVATE_PEER_CACHE_BYTES_MAX", 256<<20)),
DialogDraftCacheMaxEntries: envIntOr("TELESRV_DIALOG_DRAFT_CACHE_MAX", 1000000),
DialogDraftCacheMaxBytes: int64(envIntOr("TELESRV_DIALOG_DRAFT_CACHE_BYTES_MAX", 256<<20)),
UserProjectionFactCacheMaxEntries: envIntOr("TELESRV_USER_PROJECTION_FACT_CACHE_MAX", 1000000),
StoryActivePeerCacheMaxEntries: envIntOr("TELESRV_STORY_ACTIVE_PEER_CACHE_MAX", 1000000),
StoryHiddenListCacheMaxEntries: envIntOr("TELESRV_STORY_HIDDEN_LIST_CACHE_MAX", 100000),
StoryHiddenListCacheMaxBytes: int64(envIntOr("TELESRV_STORY_HIDDEN_LIST_CACHE_BYTES_MAX", 64<<20)),
DialogListSnapshotCacheMaxEntries: envIntOr("TELESRV_DIALOG_LIST_SNAPSHOT_CACHE_MAX", 10000),
DialogListSnapshotCacheMaxHeaders: int64(envIntOr("TELESRV_DIALOG_LIST_SNAPSHOT_HEADERS_MAX", 1000000)),
DialogListSnapshotCacheTTL: envDurationOr("TELESRV_DIALOG_LIST_SNAPSHOT_CACHE_TTL", 5*time.Minute),
DialogListSnapshotRedisTTL: envDurationOr("TELESRV_DIALOG_LIST_SNAPSHOT_REDIS_TTL", time.Hour),
ActiveChannelIDsCacheMaxEntries: envIntOr("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_MAX", 32_768),
ActiveChannelIDsCacheTTL: envDurationOr("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_TTL", 24*time.Hour),
ActiveChannelIDsRedisTTL: envDurationOr("TELESRV_ACTIVE_CHANNEL_IDS_REDIS_TTL", 24*time.Hour),
ActiveChannelIDsBatchMax: envIntOr("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_MAX", 128),
ActiveChannelIDsBatchWait: envDurationOr("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_WAIT", 100*time.Millisecond),
ActiveChannelIDsBatchQueue: envIntOr("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_QUEUE", 16_384),
ActiveChannelIDsBatchTimeout: envDurationOr("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_TIMEOUT", 5*time.Second),
LayerAdvanceBatchMax: envIntOr("TELESRV_LAYER_ADVANCE_BATCH_MAX", 256),
LayerAdvanceBatchWait: envDurationOr("TELESRV_LAYER_ADVANCE_BATCH_WAIT", 250*time.Microsecond),
LayerAdvanceBatchQueue: envIntOr("TELESRV_LAYER_ADVANCE_BATCH_QUEUE", 8192),
LayerAdvanceBatchTimeout: envDurationOr("TELESRV_LAYER_ADVANCE_BATCH_TIMEOUT", 5*time.Second),
BootstrapReadyBatchMax: envIntOr("TELESRV_BOOTSTRAP_READY_BATCH_MAX", 32),
BootstrapReadyBatchWait: envDurationOr("TELESRV_BOOTSTRAP_READY_BATCH_WAIT", 100*time.Millisecond),
BootstrapReadyBatchQueue: envIntOr("TELESRV_BOOTSTRAP_READY_BATCH_QUEUE", 16_384),
BootstrapReadyBatchTimeout: envDurationOr("TELESRV_BOOTSTRAP_READY_BATCH_TIMEOUT", 5*time.Second),
PresenceLastSeenBatchMax: envIntOr("TELESRV_PRESENCE_LAST_SEEN_BATCH_MAX", 512),
PresenceLastSeenBatchWait: envDurationOr("TELESRV_PRESENCE_LAST_SEEN_BATCH_WAIT", time.Second),
PresenceLastSeenBatchQueue: envIntOr("TELESRV_PRESENCE_LAST_SEEN_BATCH_QUEUE", 65_536),
PresenceLastSeenBatchTimeout: envDurationOr("TELESRV_PRESENCE_LAST_SEEN_BATCH_TIMEOUT", 5*time.Second),
PresenceLastSeenDrainTimeout: envDurationOr("TELESRV_PRESENCE_LAST_SEEN_DRAIN_TIMEOUT", 10*time.Second),
ChannelRowCacheMaxEntries: envIntOr("TELESRV_CHANNEL_ROW_CACHE_MAX", 50000),
ChannelTopMessageCacheMaxEntries: envIntOr("TELESRV_CHANNEL_TOP_MESSAGE_CACHE_MAX", 100000),
ChannelMemberCacheMaxEntries: envIntOr("TELESRV_CHANNEL_MEMBER_CACHE_MAX", 1000000),
ChannelDialogCacheMaxEntries: envIntOr("TELESRV_CHANNEL_DIALOG_CACHE_MAX", 1000000),
ChannelDifferenceCacheMaxEntries: envIntOr("TELESRV_CHANNEL_DIFFERENCE_CACHE_MAX", 8192),
ChannelDifferenceCacheMaxBytes: int64(envIntOr("TELESRV_CHANNEL_DIFFERENCE_CACHE_BYTES_MAX", 256<<20)),
ChannelDifferenceCacheTTL: envDurationOr("TELESRV_CHANNEL_DIFFERENCE_CACHE_TTL", 5*time.Minute),
ChannelBoostCacheMaxEntries: envIntOr("TELESRV_CHANNEL_BOOST_CACHE_MAX", 100000),
ChannelBoostCacheTTL: envDurationOr("TELESRV_CHANNEL_BOOST_CACHE_TTL", 10*time.Second),
OutboxWorkers: envIntOr("TELESRV_OUTBOX_WORKERS", 4),
OutboxBatch: envIntOr("TELESRV_OUTBOX_BATCH", 100),
OutboxBatch: envIntOr("TELESRV_OUTBOX_BATCH", 10),
OutboxInterval: envDurationOr("TELESRV_OUTBOX_INTERVAL", 200*time.Millisecond),
OutboxLeaseTimeout: envDurationOr("TELESRV_OUTBOX_LEASE_TIMEOUT", 30*time.Second),
OutboxPoisonRetention: envDurationOr("TELESRV_OUTBOX_POISON_RETENTION", time.Minute),
@ -856,12 +1040,13 @@ func Load() (Config, error) {
CallSignalingRate: envIntOr("TELESRV_CALL_SIGNALING_RATE", 50),
CallExpiryInterval: envDurationOr("TELESRV_CALL_EXPIRY_INTERVAL", time.Second),
PremiumGrantMonths: envIntOr("TELESRV_PREMIUM_GRANT_MONTHS", 3),
DefaultStickerSetID: envInt64Or("TELESRV_DEFAULT_STICKER_SET_ID", 0),
PasskeyRPID: envOr("TELESRV_PASSKEY_RP_ID", "telesrv.net"),
PasskeyAllowedOrigins: envListOr("TELESRV_PASSKEY_ALLOWED_ORIGINS", nil),
PremiumSweepInterval: envDurationOr("TELESRV_PREMIUM_SWEEP_INTERVAL", time.Minute),
PremiumSweepBatch: envIntOr("TELESRV_PREMIUM_SWEEP_BATCH", 500),
PremiumGrantMonths: envIntOr("TELESRV_PREMIUM_GRANT_MONTHS", 3),
DefaultStickerSetID: envInt64Or("TELESRV_DEFAULT_STICKER_SET_ID", 0),
PasskeyRPID: envOr("TELESRV_PASSKEY_RP_ID", "telesrv.net"),
PasskeyAllowedOrigins: envListOr("TELESRV_PASSKEY_ALLOWED_ORIGINS", nil),
PremiumSweepInterval: envDurationOr("TELESRV_PREMIUM_SWEEP_INTERVAL", time.Minute),
PremiumSweepBatch: envIntOr("TELESRV_PREMIUM_SWEEP_BATCH", 500),
CollectibleUsernameURLTemplate: strings.TrimSpace(envAllowEmptyOr("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE", "")),
// Official verification defaults ship the feature on with the official bar
@ -920,6 +1105,15 @@ func Load() (Config, error) {
if err := validateRPCExecutionConfig(cfg); err != nil {
return Config{}, err
}
if cfg.ContactSnapshotCacheMaxViewers <= 0 {
return Config{}, fmt.Errorf("TELESRV_CONTACT_SNAPSHOT_CACHE_MAX_VIEWERS must be positive")
}
if cfg.ProfilePhotoCacheMaxEntries <= 0 {
return Config{}, fmt.Errorf("TELESRV_PROFILE_PHOTO_CACHE_MAX must be positive")
}
if cfg.ProfilePhotoCacheTTL <= 0 || cfg.ProfilePhotoCacheTTL > 7*24*time.Hour {
return Config{}, fmt.Errorf("TELESRV_PROFILE_PHOTO_CACHE_TTL must be greater than zero and at most 168h")
}
if err := validateCollectibleUsernameConfig(cfg); err != nil {
return Config{}, err
}
@ -935,9 +1129,159 @@ func Load() (Config, error) {
if err := validateStorageConfig(cfg); err != nil {
return Config{}, err
}
if err := validateBlobStorageConfig(cfg); err != nil {
return Config{}, err
}
if cfg.DC <= 0 || int64(cfg.DC) > int64(1<<31-1) {
return Config{}, fmt.Errorf("TELESRV_DC must be a positive TL int32")
}
if cfg.UpdateRequestTimeout <= 0 || cfg.UpdateRequestTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_UPDATE_REQUEST_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.DialogListSnapshotRedisTTL <= 0 {
return Config{}, fmt.Errorf("TELESRV_DIALOG_LIST_SNAPSHOT_REDIS_TTL must be greater than zero")
}
if cfg.ActiveChannelIDsCacheMaxEntries <= 0 || cfg.ActiveChannelIDsCacheMaxEntries > 1_000_000 {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_MAX must be in [1,1000000]")
}
if cfg.ActiveChannelIDsCacheTTL <= 0 || cfg.ActiveChannelIDsCacheTTL > 7*24*time.Hour {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_TTL must be greater than zero and at most 168h")
}
if cfg.ActiveChannelIDsRedisTTL <= 0 || cfg.ActiveChannelIDsRedisTTL > 30*24*time.Hour {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_REDIS_TTL must be greater than zero and at most 720h")
}
if cfg.ActiveChannelIDsBatchMax <= 0 || cfg.ActiveChannelIDsBatchMax > 4096 {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_MAX must be in [1,4096]")
}
if cfg.ActiveChannelIDsBatchWait <= 0 || cfg.ActiveChannelIDsBatchWait > time.Second {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_WAIT must be greater than zero and at most 1s")
}
if cfg.ActiveChannelIDsBatchQueue < cfg.ActiveChannelIDsBatchMax || cfg.ActiveChannelIDsBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_QUEUE must be in [TELESRV_ACTIVE_CHANNEL_IDS_BATCH_MAX,1048576]")
}
if cfg.ActiveChannelIDsBatchTimeout <= 0 || cfg.ActiveChannelIDsBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.ReadModelVersionBatchMaxKeys <= 0 || cfg.ReadModelVersionBatchMaxKeys > 1<<16 {
return Config{}, fmt.Errorf("TELESRV_READ_MODEL_VERSION_BATCH_MAX_KEYS must be in [1,65536]")
}
if cfg.ReadModelVersionBatchWait <= 0 || cfg.ReadModelVersionBatchWait > 10*time.Millisecond {
return Config{}, fmt.Errorf("TELESRV_READ_MODEL_VERSION_BATCH_WAIT must be greater than zero and at most 10ms")
}
if cfg.ReadModelVersionBatchQueue <= 0 || cfg.ReadModelVersionBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_READ_MODEL_VERSION_BATCH_QUEUE must be in [1,1048576]")
}
if cfg.ReadModelVersionBatchTimeout <= 0 || cfg.ReadModelVersionBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_READ_MODEL_VERSION_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.AuthKeyGetBatchMax <= 0 || cfg.AuthKeyGetBatchMax > 4096 {
return Config{}, fmt.Errorf("TELESRV_AUTH_KEY_GET_BATCH_MAX must be in [1,4096]")
}
if cfg.AuthKeyGetBatchWait <= 0 || cfg.AuthKeyGetBatchWait > 10*time.Millisecond {
return Config{}, fmt.Errorf("TELESRV_AUTH_KEY_GET_BATCH_WAIT must be greater than zero and at most 10ms")
}
if cfg.AuthKeyGetBatchQueue < cfg.AuthKeyGetBatchMax || cfg.AuthKeyGetBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_AUTH_KEY_GET_BATCH_QUEUE must be in [TELESRV_AUTH_KEY_GET_BATCH_MAX,1048576]")
}
if cfg.AuthKeyGetBatchTimeout <= 0 || cfg.AuthKeyGetBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_AUTH_KEY_GET_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.ContactReverseBatchMaxPairs <= 0 || cfg.ContactReverseBatchMaxPairs > 1<<16 {
return Config{}, fmt.Errorf("TELESRV_CONTACT_REVERSE_BATCH_MAX_PAIRS must be in [1,65536]")
}
if cfg.ContactReverseBatchWait <= 0 || cfg.ContactReverseBatchWait > 10*time.Millisecond {
return Config{}, fmt.Errorf("TELESRV_CONTACT_REVERSE_BATCH_WAIT must be greater than zero and at most 10ms")
}
if cfg.ContactReverseBatchQueue <= 0 || cfg.ContactReverseBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_CONTACT_REVERSE_BATCH_QUEUE must be in [1,1048576]")
}
if cfg.ContactReverseBatchTimeout <= 0 || cfg.ContactReverseBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_CONTACT_REVERSE_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.ChannelDifferenceCacheMaxEntries > 0 {
if cfg.ChannelDifferenceCacheMaxBytes <= 0 {
return Config{}, fmt.Errorf("TELESRV_CHANNEL_DIFFERENCE_CACHE_BYTES_MAX must be greater than zero when the cache is enabled")
}
if cfg.ChannelDifferenceCacheTTL <= 0 || cfg.ChannelDifferenceCacheTTL > 24*time.Hour {
return Config{}, fmt.Errorf("TELESRV_CHANNEL_DIFFERENCE_CACHE_TTL must be greater than zero and at most 24h when the cache is enabled")
}
}
if cfg.LayerAdvanceBatchMax <= 0 || cfg.LayerAdvanceBatchMax > 4096 {
return Config{}, fmt.Errorf("TELESRV_LAYER_ADVANCE_BATCH_MAX must be in [1,4096]")
}
if cfg.LayerAdvanceBatchWait <= 0 || cfg.LayerAdvanceBatchWait > 10*time.Millisecond {
return Config{}, fmt.Errorf("TELESRV_LAYER_ADVANCE_BATCH_WAIT must be greater than zero and at most 10ms")
}
if cfg.LayerAdvanceBatchQueue < cfg.LayerAdvanceBatchMax || cfg.LayerAdvanceBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_LAYER_ADVANCE_BATCH_QUEUE must be in [TELESRV_LAYER_ADVANCE_BATCH_MAX,1048576]")
}
if cfg.LayerAdvanceBatchTimeout <= 0 || cfg.LayerAdvanceBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_LAYER_ADVANCE_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.BootstrapReadyBatchMax <= 0 || cfg.BootstrapReadyBatchMax > 4096 {
return Config{}, fmt.Errorf("TELESRV_BOOTSTRAP_READY_BATCH_MAX must be in [1,4096]")
}
if cfg.BootstrapReadyBatchWait <= 0 || cfg.BootstrapReadyBatchWait > time.Second {
return Config{}, fmt.Errorf("TELESRV_BOOTSTRAP_READY_BATCH_WAIT must be greater than zero and at most 1s")
}
if cfg.BootstrapReadyBatchQueue < cfg.BootstrapReadyBatchMax || cfg.BootstrapReadyBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_BOOTSTRAP_READY_BATCH_QUEUE must be in [TELESRV_BOOTSTRAP_READY_BATCH_MAX,1048576]")
}
if cfg.BootstrapReadyBatchTimeout <= 0 || cfg.BootstrapReadyBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_BOOTSTRAP_READY_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.PresenceLastSeenBatchMax <= 0 || cfg.PresenceLastSeenBatchMax > 4096 {
return Config{}, fmt.Errorf("TELESRV_PRESENCE_LAST_SEEN_BATCH_MAX must be in [1,4096]")
}
if cfg.PresenceLastSeenBatchWait <= 0 || cfg.PresenceLastSeenBatchWait > 5*time.Second {
return Config{}, fmt.Errorf("TELESRV_PRESENCE_LAST_SEEN_BATCH_WAIT must be greater than zero and at most 5s")
}
if cfg.PresenceLastSeenBatchQueue < cfg.PresenceLastSeenBatchMax || cfg.PresenceLastSeenBatchQueue > 1<<20 {
return Config{}, fmt.Errorf("TELESRV_PRESENCE_LAST_SEEN_BATCH_QUEUE must be in [TELESRV_PRESENCE_LAST_SEEN_BATCH_MAX,1048576]")
}
if cfg.PresenceLastSeenBatchTimeout <= 0 || cfg.PresenceLastSeenBatchTimeout > 30*time.Second {
return Config{}, fmt.Errorf("TELESRV_PRESENCE_LAST_SEEN_BATCH_TIMEOUT must be greater than zero and at most 30s")
}
if cfg.PresenceLastSeenDrainTimeout <= 0 || cfg.PresenceLastSeenDrainTimeout > time.Minute {
return Config{}, fmt.Errorf("TELESRV_PRESENCE_LAST_SEEN_DRAIN_TIMEOUT must be greater than zero and at most 1m")
}
return cfg, nil
}
func validateBlobStorageConfig(cfg Config) error {
if cfg.StorageMinFreeBytes < 0 {
return fmt.Errorf("TELESRV_STORAGE_MIN_FREE_BYTES must be non-negative")
}
if cfg.StorageMaxTotalBytes < 0 {
return fmt.Errorf("TELESRV_STORAGE_MAX_TOTAL_BYTES must be non-negative")
}
if cfg.StorageLowSpaceGuardEnable && cfg.StorageUsageRefreshInterval <= 0 {
return fmt.Errorf("TELESRV_STORAGE_USAGE_REFRESH_INTERVAL must be positive when storage capacity guard is enabled")
}
switch cfg.BlobBackendKind {
case string(domain.MediaBackendLocalFS):
if strings.TrimSpace(cfg.BlobDir) == "" {
return fmt.Errorf("TELESRV_BLOB_DIR is required when TELESRV_BLOB_BACKEND=localfs")
}
case string(domain.MediaBackendS3):
if strings.TrimSpace(cfg.BlobStagingDir) == "" {
return fmt.Errorf("TELESRV_BLOB_STAGING_DIR is required when TELESRV_BLOB_BACKEND=s3")
}
if cfg.S3Endpoint == "" || strings.Contains(cfg.S3Endpoint, "://") {
return fmt.Errorf("TELESRV_S3_ENDPOINT must be host[:port] without a URL scheme when TELESRV_BLOB_BACKEND=s3")
}
if cfg.S3Bucket == "" {
return fmt.Errorf("TELESRV_S3_BUCKET is required when TELESRV_BLOB_BACKEND=s3")
}
if strings.TrimSpace(cfg.S3AccessKeyID) == "" || strings.TrimSpace(cfg.S3SecretAccessKey) == "" {
return fmt.Errorf("TELESRV_S3_ACCESS_KEY_ID and TELESRV_S3_SECRET_ACCESS_KEY are required when TELESRV_BLOB_BACKEND=s3")
}
default:
return fmt.Errorf("TELESRV_BLOB_BACKEND must be localfs or s3, got %q", cfg.BlobBackendKind)
}
return nil
}
func normalizeDefaultCountryCode(raw string) (string, error) {
code := strings.ToUpper(strings.TrimSpace(raw))
if len(code) != 2 || code[0] < 'A' || code[0] > 'Z' || code[1] < 'A' || code[1] > 'Z' {
@ -1040,6 +1384,9 @@ func validateVerificationConfig(cfg Config) error {
if cfg.VerificationNotifyBatch <= 0 || cfg.VerificationNotifyBatch > 500 {
return fmt.Errorf("TELESRV_VERIFICATION_NOTIFY_BATCH must be 1..500")
}
if cfg.BroadcastWorkerInterval <= 0 {
return fmt.Errorf("TELESRV_BROADCAST_WORKER_INTERVAL must be positive")
}
if cfg.VerificationMaxActivePerUser < 0 || cfg.VerificationMaxActivePerUser > 50 {
return fmt.Errorf("TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER must be 0..50")
}
@ -1237,6 +1584,11 @@ func validateCollectibleUsernameConfig(cfg Config) error {
}
func validateRPCExecutionConfig(cfg Config) error {
if cfg.MTProtoRPCDeliveryHookWorkers <= 0 ||
cfg.MTProtoRPCDeliveryHookMaxPending < cfg.MTProtoRPCDeliveryHookWorkers {
return fmt.Errorf("MTProto rpc delivery hook capacity must satisfy pending >= workers > 0: %d/%d",
cfg.MTProtoRPCDeliveryHookMaxPending, cfg.MTProtoRPCDeliveryHookWorkers)
}
if cfg.MTProtoRPCExecutionMaxEntries <= 0 || cfg.MTProtoRPCExecutionAuthMaxEntries <= 0 ||
cfg.MTProtoRPCExecutionSessionMaxEntries <= 0 {
return fmt.Errorf("MTProto rpc execution entry limits must be positive")
@ -1572,12 +1924,18 @@ func validateStrictMTProtoCapacityEnv(e envSource) error {
"TELESRV_MTPROTO_RPC_QUEUE_SIZE",
"TELESRV_MTPROTO_RPC_GLOBAL_WORKERS",
"TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS",
"TELESRV_MTPROTO_RPC_DELIVERY_HOOK_WORKERS",
"TELESRV_MTPROTO_RPC_DELIVERY_HOOK_MAX_PENDING",
"TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES",
"TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES",
"TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES",
"TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH",
"TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE",
"TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE",
"TELESRV_CONTACT_REVERSE_BATCH_MAX_PAIRS",
"TELESRV_CONTACT_REVERSE_BATCH_QUEUE",
"TELESRV_CONTACT_SNAPSHOT_CACHE_MAX_VIEWERS",
"TELESRV_PROFILE_PHOTO_CACHE_MAX",
} {
if raw := e.envOr(key, ""); raw != "" {
if _, err := strconv.Atoi(raw); err != nil {

View file

@ -33,8 +33,8 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
if cfg.PublicWebBaseURL != "https://weba.telesrv.net" {
t.Fatalf("PublicWebBaseURL = %q, want https://weba.telesrv.net", cfg.PublicWebBaseURL)
}
if cfg.PublicAppName != "telesrv" {
t.Fatalf("PublicAppName = %q, want telesrv", cfg.PublicAppName)
if cfg.PublicAppName != "Telesrv" {
t.Fatalf("PublicAppName = %q, want Telesrv", cfg.PublicAppName)
}
if cfg.CallRegistryMaxEntries != 10_000 {
t.Fatalf("CallRegistryMaxEntries = %d, want 10000", cfg.CallRegistryMaxEntries)
@ -42,6 +42,468 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
if cfg.PremiumPromoSeedDir != "data/premium-promo" {
t.Fatalf("PremiumPromoSeedDir = %q, want data/premium-promo", cfg.PremiumPromoSeedDir)
}
if cfg.BlobBackendKind != string(domain.MediaBackendLocalFS) {
t.Fatalf("BlobBackendKind = %q, want localfs", cfg.BlobBackendKind)
}
if cfg.BlobDir != "data/blobs" {
t.Fatalf("BlobDir = %q, want data/blobs", cfg.BlobDir)
}
if !cfg.StorageLowSpaceGuardEnable || cfg.StorageMinFreeBytes != 1<<30 || cfg.StorageMaxTotalBytes != 0 || cfg.StorageUsageRefreshInterval != time.Minute {
t.Fatalf("unexpected storage capacity defaults: enabled=%v min=%d max=%d interval=%v", cfg.StorageLowSpaceGuardEnable, cfg.StorageMinFreeBytes, cfg.StorageMaxTotalBytes, cfg.StorageUsageRefreshInterval)
}
if cfg.MTProtoRPCGlobalMaxTasks != 32768 {
t.Fatalf("MTProtoRPCGlobalMaxTasks = %d, want 32768", cfg.MTProtoRPCGlobalMaxTasks)
}
}
func TestLoadDialogListSnapshotRedisTTL(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_DIALOG_LIST_SNAPSHOT_REDIS_TTL", "37m")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.DialogListSnapshotRedisTTL != 37*time.Minute {
t.Fatalf("DialogListSnapshotRedisTTL = %v, want 37m", cfg.DialogListSnapshotRedisTTL)
}
}
func TestLoadRejectsInvalidDialogListSnapshotRedisTTL(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_DIALOG_LIST_SNAPSHOT_REDIS_TTL", "0s")
if _, err := Load(); err == nil {
t.Fatal("zero dialog list snapshot Redis TTL accepted")
}
}
func TestLoadActiveChannelIDsReadModel(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_MAX", "45678")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_CACHE_TTL", "9h")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_REDIS_TTL", "27h")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_MAX", "73")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_WAIT", "37ms")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_QUEUE", "901")
t.Setenv("TELESRV_ACTIVE_CHANNEL_IDS_BATCH_TIMEOUT", "3s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.ActiveChannelIDsCacheMaxEntries != 45678 || cfg.ActiveChannelIDsCacheTTL != 9*time.Hour ||
cfg.ActiveChannelIDsRedisTTL != 27*time.Hour || cfg.ActiveChannelIDsBatchMax != 73 ||
cfg.ActiveChannelIDsBatchWait != 37*time.Millisecond || cfg.ActiveChannelIDsBatchQueue != 901 ||
cfg.ActiveChannelIDsBatchTimeout != 3*time.Second {
t.Fatalf("active channel IDs config = max=%d l1=%v redis=%v batch=%d/%v/%d/%v",
cfg.ActiveChannelIDsCacheMaxEntries, cfg.ActiveChannelIDsCacheTTL, cfg.ActiveChannelIDsRedisTTL,
cfg.ActiveChannelIDsBatchMax, cfg.ActiveChannelIDsBatchWait, cfg.ActiveChannelIDsBatchQueue,
cfg.ActiveChannelIDsBatchTimeout)
}
}
func TestLoadRejectsInvalidActiveChannelIDsReadModel(t *testing.T) {
for _, test := range []struct{ key, value string }{
{key: "TELESRV_ACTIVE_CHANNEL_IDS_CACHE_MAX", value: "0"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_CACHE_TTL", value: "0s"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_REDIS_TTL", value: "0s"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_BATCH_MAX", value: "0"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_BATCH_WAIT", value: "0s"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_BATCH_QUEUE", value: "1"},
{key: "TELESRV_ACTIVE_CHANNEL_IDS_BATCH_TIMEOUT", value: "0s"},
} {
t.Run(test.key+"="+test.value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadChannelDifferenceCache(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_CHANNEL_DIFFERENCE_CACHE_MAX", "123")
t.Setenv("TELESRV_CHANNEL_DIFFERENCE_CACHE_BYTES_MAX", "456789")
t.Setenv("TELESRV_CHANNEL_DIFFERENCE_CACHE_TTL", "7m")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.ChannelDifferenceCacheMaxEntries != 123 || cfg.ChannelDifferenceCacheMaxBytes != 456789 || cfg.ChannelDifferenceCacheTTL != 7*time.Minute {
t.Fatalf("channel difference cache = %d/%d/%v",
cfg.ChannelDifferenceCacheMaxEntries, cfg.ChannelDifferenceCacheMaxBytes, cfg.ChannelDifferenceCacheTTL)
}
}
func TestLoadRejectsInvalidChannelDifferenceCache(t *testing.T) {
for _, test := range []struct{ key, value string }{
{key: "TELESRV_CHANNEL_DIFFERENCE_CACHE_BYTES_MAX", value: "0"},
{key: "TELESRV_CHANNEL_DIFFERENCE_CACHE_TTL", value: "0s"},
{key: "TELESRV_CHANNEL_DIFFERENCE_CACHE_TTL", value: "25h"},
} {
t.Run(test.key+"="+test.value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadLayerAdvanceBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_LAYER_ADVANCE_BATCH_MAX", "73")
t.Setenv("TELESRV_LAYER_ADVANCE_BATCH_WAIT", "400us")
t.Setenv("TELESRV_LAYER_ADVANCE_BATCH_QUEUE", "901")
t.Setenv("TELESRV_LAYER_ADVANCE_BATCH_TIMEOUT", "3s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.LayerAdvanceBatchMax != 73 || cfg.LayerAdvanceBatchWait != 400*time.Microsecond ||
cfg.LayerAdvanceBatchQueue != 901 || cfg.LayerAdvanceBatchTimeout != 3*time.Second {
t.Fatalf("layer advance batch = %d/%v/%d/%v",
cfg.LayerAdvanceBatchMax, cfg.LayerAdvanceBatchWait,
cfg.LayerAdvanceBatchQueue, cfg.LayerAdvanceBatchTimeout)
}
}
func TestLoadReadModelVersionBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_READ_MODEL_VERSION_BATCH_MAX_KEYS", "3072")
t.Setenv("TELESRV_READ_MODEL_VERSION_BATCH_WAIT", "350us")
t.Setenv("TELESRV_READ_MODEL_VERSION_BATCH_QUEUE", "777")
t.Setenv("TELESRV_READ_MODEL_VERSION_BATCH_TIMEOUT", "4s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.ReadModelVersionBatchMaxKeys != 3072 || cfg.ReadModelVersionBatchWait != 350*time.Microsecond ||
cfg.ReadModelVersionBatchQueue != 777 || cfg.ReadModelVersionBatchTimeout != 4*time.Second {
t.Fatalf("read-model version batch = %d/%v/%d/%v",
cfg.ReadModelVersionBatchMaxKeys, cfg.ReadModelVersionBatchWait,
cfg.ReadModelVersionBatchQueue, cfg.ReadModelVersionBatchTimeout)
}
}
func TestLoadRejectsInvalidReadModelVersionBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max keys", key: "TELESRV_READ_MODEL_VERSION_BATCH_MAX_KEYS", value: "0"},
{name: "wait too large", key: "TELESRV_READ_MODEL_VERSION_BATCH_WAIT", value: "11ms"},
{name: "zero queue", key: "TELESRV_READ_MODEL_VERSION_BATCH_QUEUE", value: "0"},
{name: "timeout too large", key: "TELESRV_READ_MODEL_VERSION_BATCH_TIMEOUT", value: "31s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadAuthKeyGetBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_AUTH_KEY_GET_BATCH_MAX", "97")
t.Setenv("TELESRV_AUTH_KEY_GET_BATCH_WAIT", "425us")
t.Setenv("TELESRV_AUTH_KEY_GET_BATCH_QUEUE", "997")
t.Setenv("TELESRV_AUTH_KEY_GET_BATCH_TIMEOUT", "4s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.AuthKeyGetBatchMax != 97 || cfg.AuthKeyGetBatchWait != 425*time.Microsecond ||
cfg.AuthKeyGetBatchQueue != 997 || cfg.AuthKeyGetBatchTimeout != 4*time.Second {
t.Fatalf("auth-key get batch = %d/%v/%d/%v",
cfg.AuthKeyGetBatchMax, cfg.AuthKeyGetBatchWait,
cfg.AuthKeyGetBatchQueue, cfg.AuthKeyGetBatchTimeout)
}
}
func TestLoadRejectsInvalidAuthKeyGetBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max", key: "TELESRV_AUTH_KEY_GET_BATCH_MAX", value: "0"},
{name: "wait too large", key: "TELESRV_AUTH_KEY_GET_BATCH_WAIT", value: "11ms"},
{name: "queue below max", key: "TELESRV_AUTH_KEY_GET_BATCH_QUEUE", value: "1"},
{name: "timeout too large", key: "TELESRV_AUTH_KEY_GET_BATCH_TIMEOUT", value: "31s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadContactReverseBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_CONTACT_REVERSE_BATCH_MAX_PAIRS", "3073")
t.Setenv("TELESRV_CONTACT_REVERSE_BATCH_WAIT", "375us")
t.Setenv("TELESRV_CONTACT_REVERSE_BATCH_QUEUE", "778")
t.Setenv("TELESRV_CONTACT_REVERSE_BATCH_TIMEOUT", "4s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.ContactReverseBatchMaxPairs != 3073 || cfg.ContactReverseBatchWait != 375*time.Microsecond ||
cfg.ContactReverseBatchQueue != 778 || cfg.ContactReverseBatchTimeout != 4*time.Second {
t.Fatalf("contact reverse batch = %d/%v/%d/%v",
cfg.ContactReverseBatchMaxPairs, cfg.ContactReverseBatchWait,
cfg.ContactReverseBatchQueue, cfg.ContactReverseBatchTimeout)
}
}
func TestLoadRejectsInvalidContactReverseBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max pairs", key: "TELESRV_CONTACT_REVERSE_BATCH_MAX_PAIRS", value: "0"},
{name: "wait too large", key: "TELESRV_CONTACT_REVERSE_BATCH_WAIT", value: "11ms"},
{name: "zero queue", key: "TELESRV_CONTACT_REVERSE_BATCH_QUEUE", value: "0"},
{name: "timeout too large", key: "TELESRV_CONTACT_REVERSE_BATCH_TIMEOUT", value: "31s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadRejectsInvalidLayerAdvanceBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max", key: "TELESRV_LAYER_ADVANCE_BATCH_MAX", value: "0"},
{name: "wait too large", key: "TELESRV_LAYER_ADVANCE_BATCH_WAIT", value: "11ms"},
{name: "queue below max", key: "TELESRV_LAYER_ADVANCE_BATCH_QUEUE", value: "1"},
{name: "timeout too large", key: "TELESRV_LAYER_ADVANCE_BATCH_TIMEOUT", value: "31s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadBootstrapReadyBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_BOOTSTRAP_READY_BATCH_MAX", "41")
t.Setenv("TELESRV_BOOTSTRAP_READY_BATCH_WAIT", "37ms")
t.Setenv("TELESRV_BOOTSTRAP_READY_BATCH_QUEUE", "917")
t.Setenv("TELESRV_BOOTSTRAP_READY_BATCH_TIMEOUT", "4s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.BootstrapReadyBatchMax != 41 || cfg.BootstrapReadyBatchWait != 37*time.Millisecond ||
cfg.BootstrapReadyBatchQueue != 917 || cfg.BootstrapReadyBatchTimeout != 4*time.Second {
t.Fatalf("bootstrap readiness batch = %d/%v/%d/%v",
cfg.BootstrapReadyBatchMax, cfg.BootstrapReadyBatchWait,
cfg.BootstrapReadyBatchQueue, cfg.BootstrapReadyBatchTimeout)
}
}
func TestLoadRejectsInvalidBootstrapReadyBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max", key: "TELESRV_BOOTSTRAP_READY_BATCH_MAX", value: "0"},
{name: "wait too large", key: "TELESRV_BOOTSTRAP_READY_BATCH_WAIT", value: "1001ms"},
{name: "queue below max", key: "TELESRV_BOOTSTRAP_READY_BATCH_QUEUE", value: "1"},
{name: "timeout too large", key: "TELESRV_BOOTSTRAP_READY_BATCH_TIMEOUT", value: "31s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadPresenceLastSeenBatch(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_PRESENCE_LAST_SEEN_BATCH_MAX", "73")
t.Setenv("TELESRV_PRESENCE_LAST_SEEN_BATCH_WAIT", "12ms")
t.Setenv("TELESRV_PRESENCE_LAST_SEEN_BATCH_QUEUE", "901")
t.Setenv("TELESRV_PRESENCE_LAST_SEEN_BATCH_TIMEOUT", "3s")
t.Setenv("TELESRV_PRESENCE_LAST_SEEN_DRAIN_TIMEOUT", "17s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.PresenceLastSeenBatchMax != 73 || cfg.PresenceLastSeenBatchWait != 12*time.Millisecond ||
cfg.PresenceLastSeenBatchQueue != 901 || cfg.PresenceLastSeenBatchTimeout != 3*time.Second ||
cfg.PresenceLastSeenDrainTimeout != 17*time.Second {
t.Fatalf("presence last-seen batch = %d/%v/%d/%v/%v",
cfg.PresenceLastSeenBatchMax, cfg.PresenceLastSeenBatchWait,
cfg.PresenceLastSeenBatchQueue, cfg.PresenceLastSeenBatchTimeout,
cfg.PresenceLastSeenDrainTimeout)
}
}
func TestLoadRejectsInvalidPresenceLastSeenBatch(t *testing.T) {
for _, test := range []struct {
name string
key string
value string
}{
{name: "zero max", key: "TELESRV_PRESENCE_LAST_SEEN_BATCH_MAX", value: "0"},
{name: "wait too large", key: "TELESRV_PRESENCE_LAST_SEEN_BATCH_WAIT", value: "6s"},
{name: "queue below max", key: "TELESRV_PRESENCE_LAST_SEEN_BATCH_QUEUE", value: "1"},
{name: "timeout too large", key: "TELESRV_PRESENCE_LAST_SEEN_BATCH_TIMEOUT", value: "31s"},
{name: "drain too large", key: "TELESRV_PRESENCE_LAST_SEEN_DRAIN_TIMEOUT", value: "61s"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("invalid %s=%s accepted", test.key, test.value)
}
})
}
}
func TestLoadS3BlobStorageConfig(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_BLOB_BACKEND", "s3")
t.Setenv("TELESRV_BLOB_STAGING_DIR", `D:\staging\telesrv`)
t.Setenv("TELESRV_S3_ENDPOINT", "minio.example.test:9000")
t.Setenv("TELESRV_S3_BUCKET", "telesrv-media")
t.Setenv("TELESRV_S3_ACCESS_KEY_ID", "access")
t.Setenv("TELESRV_S3_SECRET_ACCESS_KEY", "secret")
t.Setenv("TELESRV_S3_USE_SSL", "false")
t.Setenv("TELESRV_S3_PATH_STYLE", "true")
t.Setenv("TELESRV_S3_CREATE_BUCKET", "true")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.BlobBackendKind != "s3" || cfg.S3Endpoint != "minio.example.test:9000" || cfg.S3Bucket != "telesrv-media" {
t.Fatalf("unexpected s3 config: backend=%q endpoint=%q bucket=%q", cfg.BlobBackendKind, cfg.S3Endpoint, cfg.S3Bucket)
}
if cfg.S3UseSSL || !cfg.S3PathStyle || !cfg.S3CreateBucket {
t.Fatalf("unexpected s3 flags: ssl=%v path_style=%v create=%v", cfg.S3UseSSL, cfg.S3PathStyle, cfg.S3CreateBucket)
}
}
func TestLoadRejectsInvalidBlobStorageConfig(t *testing.T) {
tests := []struct {
name string
backend string
endpoint string
bucket string
access string
secret string
}{
{name: "unknown backend", backend: "mirror"},
{name: "missing s3 endpoint", backend: "s3", bucket: "media", access: "access", secret: "secret"},
{name: "endpoint has scheme", backend: "s3", endpoint: "http://minio:9000", bucket: "media", access: "access", secret: "secret"},
{name: "missing s3 bucket", backend: "s3", endpoint: "minio:9000", access: "access", secret: "secret"},
{name: "missing s3 credentials", backend: "s3", endpoint: "minio:9000", bucket: "media"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_BLOB_BACKEND", tt.backend)
t.Setenv("TELESRV_S3_ENDPOINT", tt.endpoint)
t.Setenv("TELESRV_S3_BUCKET", tt.bucket)
t.Setenv("TELESRV_S3_ACCESS_KEY_ID", tt.access)
t.Setenv("TELESRV_S3_SECRET_ACCESS_KEY", tt.secret)
if _, err := Load(); err == nil {
t.Fatal("invalid blob storage config accepted")
}
})
}
}
func TestLoadRejectsInvalidStorageCapacityConfig(t *testing.T) {
for _, item := range []struct{ key, value string }{
{"TELESRV_STORAGE_MIN_FREE_BYTES", "-1"},
{"TELESRV_STORAGE_MAX_TOTAL_BYTES", "-1"},
{"TELESRV_STORAGE_USAGE_REFRESH_INTERVAL", "0s"},
{"TELESRV_STORAGE_USAGE_REFRESH_INTERVAL", "-1s"},
} {
t.Run(item.key+"="+item.value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(item.key, item.value)
if _, err := Load(); err == nil {
t.Fatalf("Load accepted invalid %s=%s", item.key, item.value)
}
})
}
}
func TestLoadUpdateServiceConfig(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_UPDATE_PUBLIC_URL", "https://updates.example.test/root/")
t.Setenv("TELESRV_UPDATE_SERVICE_URL", "http://127.0.0.1:2402/")
t.Setenv("TELESRV_UPDATE_REQUEST_TIMEOUT", "3s")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.UpdatePublicURL != "https://updates.example.test/root" || cfg.UpdateServiceURL != "http://127.0.0.1:2402" {
t.Fatalf("update URLs = %q / %q", cfg.UpdatePublicURL, cfg.UpdateServiceURL)
}
if cfg.UpdateRequestTimeout != 3*time.Second {
t.Fatalf("UpdateRequestTimeout = %v", cfg.UpdateRequestTimeout)
}
}
func TestLoadUpdateServiceDefaultsInternalURLToPublic(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_UPDATE_PUBLIC_URL", "https://updates.example.test")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.UpdateServiceURL != cfg.UpdatePublicURL {
t.Fatalf("UpdateServiceURL = %q, want %q", cfg.UpdateServiceURL, cfg.UpdatePublicURL)
}
}
func TestLoadRejectsInvalidUpdateServiceConfig(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_UPDATE_PUBLIC_URL", "file:///updates")
if _, err := Load(); err == nil {
t.Fatal("invalid update public URL accepted")
}
}
func TestLoadPremiumPromoSeedDirOverride(t *testing.T) {
@ -165,6 +627,18 @@ func TestLoadStrictDCCheck(t *testing.T) {
})
}
func TestLoadRejectsNonPositiveCanonicalDC(t *testing.T) {
for _, value := range []string{"0", "-2", "2147483648"} {
t.Run(value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_DC", value)
if _, err := Load(); err == nil {
t.Fatalf("Load accepted TELESRV_DC=%s", value)
}
})
}
}
func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_MTPROTO_MAX_CONNECTIONS", "12345")
@ -176,6 +650,8 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_WORKERS", "33")
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", "444")
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES", "555555")
t.Setenv("TELESRV_MTPROTO_RPC_DELIVERY_HOOK_WORKERS", "17")
t.Setenv("TELESRV_MTPROTO_RPC_DELIVERY_HOOK_MAX_PENDING", "777")
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", "555")
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES", "444")
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES", "333")
@ -188,6 +664,9 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
t.Setenv("TELESRV_TEMP_KEY_CACHE_MAX_ENTRIES", "666")
t.Setenv("TELESRV_TEMP_KEY_CACHE_TTL", "17m")
t.Setenv("TELESRV_ORPHAN_AUTH_KEY_RETENTION", "36h")
t.Setenv("TELESRV_CONTACT_SNAPSHOT_CACHE_MAX_VIEWERS", "1234")
t.Setenv("TELESRV_PROFILE_PHOTO_CACHE_MAX", "2345")
t.Setenv("TELESRV_PROFILE_PHOTO_CACHE_TTL", "36h")
cfg, err := Load()
if err != nil {
@ -200,6 +679,9 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
cfg.MTProtoRPCGlobalWorkers != 33 || cfg.MTProtoRPCGlobalMaxTasks != 444 || cfg.MTProtoRPCGlobalMaxBytes != 555555 {
t.Fatalf("rpc budget config = %d/%d/%v/%d/%d/%d", cfg.MTProtoRPCMaxInflight, cfg.MTProtoRPCQueueSize, cfg.MTProtoRPCTimeout, cfg.MTProtoRPCGlobalWorkers, cfg.MTProtoRPCGlobalMaxTasks, cfg.MTProtoRPCGlobalMaxBytes)
}
if cfg.MTProtoRPCDeliveryHookWorkers != 17 || cfg.MTProtoRPCDeliveryHookMaxPending != 777 {
t.Fatalf("rpc delivery hook config = %d/%d", cfg.MTProtoRPCDeliveryHookWorkers, cfg.MTProtoRPCDeliveryHookMaxPending)
}
if cfg.MTProtoRPCExecutionMaxEntries != 555 ||
cfg.MTProtoRPCExecutionAuthMaxEntries != 444 ||
cfg.MTProtoRPCExecutionSessionMaxEntries != 333 ||
@ -219,6 +701,12 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
if cfg.TempKeyResolveCacheMaxEntries != 666 || cfg.TempKeyResolveCacheTTL != 17*time.Minute || cfg.OrphanAuthKeyRetention != 36*time.Hour {
t.Fatalf("auth key resource config = %d/%v/%v", cfg.TempKeyResolveCacheMaxEntries, cfg.TempKeyResolveCacheTTL, cfg.OrphanAuthKeyRetention)
}
if cfg.ContactSnapshotCacheMaxViewers != 1234 {
t.Fatalf("contact snapshot cache viewers = %d", cfg.ContactSnapshotCacheMaxViewers)
}
if cfg.ProfilePhotoCacheMaxEntries != 2345 || cfg.ProfilePhotoCacheTTL != 36*time.Hour {
t.Fatalf("profile photo cache = %d/%v", cfg.ProfilePhotoCacheMaxEntries, cfg.ProfilePhotoCacheTTL)
}
}
func TestLoadRPCExecutionFairBudgetDefaults(t *testing.T) {
@ -237,6 +725,15 @@ func TestLoadRPCExecutionFairBudgetDefaults(t *testing.T) {
cfg.MTProtoRPCExecutionSessionMaxEntries,
cfg.MTProtoRPCExecutionPendingPerAuth)
}
if cfg.MTProtoRPCDeliveryHookWorkers != 32 || cfg.MTProtoRPCDeliveryHookMaxPending != 16_384 {
t.Fatalf("rpc delivery hook defaults = %d/%d", cfg.MTProtoRPCDeliveryHookWorkers, cfg.MTProtoRPCDeliveryHookMaxPending)
}
if cfg.ContactSnapshotCacheMaxViewers != 16_384 {
t.Fatalf("contact snapshot cache default = %d", cfg.ContactSnapshotCacheMaxViewers)
}
if cfg.ProfilePhotoCacheMaxEntries != 200_000 || cfg.ProfilePhotoCacheTTL != 24*time.Hour {
t.Fatalf("profile photo cache defaults = %d/%v", cfg.ProfilePhotoCacheMaxEntries, cfg.ProfilePhotoCacheTTL)
}
}
func TestLoadRejectsInvalidRPCExecutionFairBudgets(t *testing.T) {
@ -246,7 +743,9 @@ func TestLoadRejectsInvalidRPCExecutionFairBudgets(t *testing.T) {
value string
}{
{name: "entry hierarchy", key: "TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", value: "1024"},
{name: "pending hierarchy", key: "TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH", value: "9000"},
{name: "pending hierarchy", key: "TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH", value: "33000"},
{name: "delivery workers", key: "TELESRV_MTPROTO_RPC_DELIVERY_HOOK_WORKERS", value: "0"},
{name: "delivery pending hierarchy", key: "TELESRV_MTPROTO_RPC_DELIVERY_HOOK_MAX_PENDING", value: "16"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@ -259,6 +758,38 @@ func TestLoadRejectsInvalidRPCExecutionFairBudgets(t *testing.T) {
}
}
func TestLoadRejectsNonPositiveContactSnapshotCache(t *testing.T) {
for _, value := range []string{"0", "-1"} {
t.Run(value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv("TELESRV_CONTACT_SNAPSHOT_CACHE_MAX_VIEWERS", value)
if _, err := Load(); err == nil {
t.Fatalf("Load accepted contact snapshot cache capacity %s", value)
}
})
}
}
func TestLoadRejectsInvalidProfilePhotoCache(t *testing.T) {
for _, test := range []struct {
key string
value string
}{
{key: "TELESRV_PROFILE_PHOTO_CACHE_MAX", value: "0"},
{key: "TELESRV_PROFILE_PHOTO_CACHE_MAX", value: "-1"},
{key: "TELESRV_PROFILE_PHOTO_CACHE_TTL", value: "0s"},
{key: "TELESRV_PROFILE_PHOTO_CACHE_TTL", value: "169h"},
} {
t.Run(test.key+"="+test.value, func(t *testing.T) {
disableDefaultConfigFile(t)
t.Setenv(test.key, test.value)
if _, err := Load(); err == nil {
t.Fatalf("Load accepted invalid %s=%s", test.key, test.value)
}
})
}
}
func TestLoadRejectsMalformedMTProtoCapacity(t *testing.T) {
for _, test := range []struct {
name string
@ -269,6 +800,7 @@ func TestLoadRejectsMalformedMTProtoCapacity(t *testing.T) {
{name: "receipt entries overflow", key: "TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", value: "999999999999999999999999"},
{name: "tracked bytes overflow", key: "TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES", value: "999999999999999999999999"},
{name: "outbound queue malformed", key: "TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE", value: "many"},
{name: "profile photo entries malformed", key: "TELESRV_PROFILE_PHOTO_CACHE_MAX", value: "many"},
} {
t.Run(test.name, func(t *testing.T) {
disableDefaultConfigFile(t)
@ -539,6 +1071,21 @@ func TestLoadTranslationConfig(t *testing.T) {
}
}
func TestLoadStorySparseProjectionCacheConfig(t *testing.T) {
t.Setenv("TELESRV_CONFIG", "")
t.Setenv("TELESRV_STORY_ACTIVE_PEER_CACHE_MAX", "1234")
t.Setenv("TELESRV_STORY_HIDDEN_LIST_CACHE_MAX", "2345")
t.Setenv("TELESRV_STORY_HIDDEN_LIST_CACHE_BYTES_MAX", "3456")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.StoryActivePeerCacheMaxEntries != 1234 || cfg.StoryHiddenListCacheMaxEntries != 2345 || cfg.StoryHiddenListCacheMaxBytes != 3456 {
t.Fatalf("story sparse cache config = %d/%d/%d, want 1234/2345/3456",
cfg.StoryActivePeerCacheMaxEntries, cfg.StoryHiddenListCacheMaxEntries, cfg.StoryHiddenListCacheMaxBytes)
}
}
func TestLoadReadsEnvStyleConfigFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "telesrv.env")
writeConfigFile(t, path, `