Merge remote-tracking branch 'upstream/main' into merge-gramsrv-9106877

This commit is contained in:
onysd 2026-08-03 23:29:20 +03:00
commit ac6a50c5ff
697 changed files with 100880 additions and 8052 deletions

View file

@ -27,9 +27,12 @@ import (
"telesrv/internal/app/account"
aiapp "telesrv/internal/app/ai"
"telesrv/internal/app/auth"
authdiagnosticsapp "telesrv/internal/app/authdiagnostics"
botsapp "telesrv/internal/app/bots"
botverificationapp "telesrv/internal/app/botverification"
channelapp "telesrv/internal/app/channels"
chatlistsapp "telesrv/internal/app/chatlists"
clienttelemetryapp "telesrv/internal/app/clienttelemetry"
communitiesapp "telesrv/internal/app/communities"
"telesrv/internal/app/contacts"
"telesrv/internal/app/dialogs"
@ -41,10 +44,12 @@ import (
"telesrv/internal/app/livestream"
"telesrv/internal/app/maintenance"
messageapp "telesrv/internal/app/messages"
moderationapp "telesrv/internal/app/moderation"
passkeyapp "telesrv/internal/app/passkey"
phoneapp "telesrv/internal/app/phone"
pollsapp "telesrv/internal/app/polls"
privacyapp "telesrv/internal/app/privacy"
ratingapp "telesrv/internal/app/rating"
secretchatapp "telesrv/internal/app/secretchat"
"telesrv/internal/app/stargifts"
"telesrv/internal/app/stars"
@ -53,12 +58,15 @@ import (
themesapp "telesrv/internal/app/themes"
translationapp "telesrv/internal/app/translation"
"telesrv/internal/app/updates"
usernamesapp "telesrv/internal/app/usernames"
"telesrv/internal/app/userprojection"
"telesrv/internal/app/users"
verificationapp "telesrv/internal/app/verification"
"telesrv/internal/botapi"
"telesrv/internal/config"
"telesrv/internal/domain"
"telesrv/internal/mtprotoedge"
obsmetrics "telesrv/internal/observability/metrics"
"telesrv/internal/officialgifts"
"telesrv/internal/otpdelivery"
otpsmtp "telesrv/internal/otpdelivery/smtp"
@ -230,7 +238,7 @@ func newTranslationOptions(cfg config.Config, limiter translationapp.RateLimiter
// - /debug/pprof/allocs 累计分配(带宽/序列化热点常与之相关)
//
// mutex/block 采样在低流量测试环境开销可忽略;高流量生产如担心扰动,置空 DebugAddr 关闭整端点。
func startDebugServer(ctx context.Context, addr string, logger *zap.Logger) {
func startDebugServer(ctx context.Context, addr string, metricsHandler http.Handler, logger *zap.Logger) {
if addr == "" {
return
}
@ -243,6 +251,9 @@ func startDebugServer(ctx context.Context, addr string, logger *zap.Logger) {
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
if metricsHandler != nil {
mux.Handle("/metrics", metricsHandler)
}
srv := &http.Server{Addr: addr, Handler: mux}
go func() {
@ -260,6 +271,55 @@ func startDebugServer(ctx context.Context, addr string, logger *zap.Logger) {
}()
}
func goRuntimeGaugeSamples() []obsmetrics.GaugeSample {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
return []obsmetrics.GaugeSample{
{Name: "telesrv_go_goroutines", Value: float64(runtime.NumGoroutine())},
{Name: "telesrv_go_heap_alloc_bytes", Value: float64(mem.HeapAlloc)},
{Name: "telesrv_go_heap_inuse_bytes", Value: float64(mem.HeapInuse)},
{Name: "telesrv_go_heap_objects", Value: float64(mem.HeapObjects)},
{Name: "telesrv_go_stack_inuse_bytes", Value: float64(mem.StackInuse)},
{Name: "telesrv_go_sys_bytes", Value: float64(mem.Sys)},
{Name: "telesrv_go_gc_cycles", Value: float64(mem.NumGC)},
{Name: "telesrv_go_gc_pause_seconds", Value: time.Duration(mem.PauseTotalNs).Seconds()},
}
}
func mtprotoRuntimeGaugeSamples(snapshot mtprotoedge.RuntimeSnapshot) []obsmetrics.GaugeSample {
return []obsmetrics.GaugeSample{
{Name: "telesrv_mtproto_raw_connections", Value: float64(snapshot.RawConnections)},
{Name: "telesrv_mtproto_raw_connection_limit", Value: float64(snapshot.RawConnectionLimit)},
{Name: "telesrv_mtproto_handshakes_active", Value: float64(snapshot.Handshakes)},
{Name: "telesrv_mtproto_handshake_limit", Value: float64(snapshot.HandshakeLimit)},
{Name: "telesrv_mtproto_sessions", Labels: []obsmetrics.Label{{Name: "state", Value: "active"}}, Value: float64(snapshot.ActiveSessions)},
{Name: "telesrv_mtproto_sessions", Labels: []obsmetrics.Label{{Name: "state", Value: "provisional"}}, Value: float64(snapshot.ProvisionalSessions)},
{Name: "telesrv_mtproto_logical_sessions", Labels: []obsmetrics.Label{{Name: "state", Value: "retained"}}, Value: float64(snapshot.LogicalSessions)},
{Name: "telesrv_mtproto_logical_sessions", Labels: []obsmetrics.Label{{Name: "state", Value: "offline"}}, Value: float64(snapshot.OfflineLogicalSessions)},
{Name: "telesrv_mtproto_logical_outbox_frames", Value: float64(snapshot.LogicalOutboxFrames)},
{Name: "telesrv_mtproto_logical_outbox_bytes", Value: float64(snapshot.LogicalOutboxBytes)},
{Name: "telesrv_mtproto_pending_push_bytes", Value: float64(snapshot.PendingPushBytes)},
{Name: "telesrv_mtproto_inbound_rpc_tasks", Value: float64(snapshot.InboundRPCTasks)},
{Name: "telesrv_mtproto_inbound_rpc_bytes", Value: float64(snapshot.InboundRPCBytes)},
{Name: "telesrv_mtproto_inbound_rpc_ready_connections", Value: float64(snapshot.InboundRPCReadyConnections)},
{Name: "telesrv_mtproto_inbound_rpc_task_limit", Value: float64(snapshot.InboundRPCMaxTasks)},
{Name: "telesrv_mtproto_inbound_rpc_byte_limit", Value: float64(snapshot.InboundRPCMaxBytes)},
{Name: "telesrv_mtproto_inbound_frame_bytes", Value: float64(snapshot.InboundFrameBytes)},
{Name: "telesrv_mtproto_inbound_frame_byte_limit", Value: float64(snapshot.InboundFrameMaxBytes)},
{Name: "telesrv_mtproto_outbound_tracked_bytes", Labels: []obsmetrics.Label{{Name: "kind", Value: "body"}}, Value: float64(snapshot.OutboundTrackedBytes)},
{Name: "telesrv_mtproto_outbound_tracked_bytes", Labels: []obsmetrics.Label{{Name: "kind", Value: "control"}}, Value: float64(snapshot.OutboundControlBytes)},
{Name: "telesrv_mtproto_outbound_tracked_byte_limit", Labels: []obsmetrics.Label{{Name: "kind", Value: "body"}}, Value: float64(snapshot.OutboundTrackedMaxBytes)},
{Name: "telesrv_mtproto_outbound_tracked_byte_limit", Labels: []obsmetrics.Label{{Name: "kind", Value: "control"}}, Value: float64(snapshot.OutboundControlMaxBytes)},
{Name: "telesrv_mtproto_outbound_write_bytes", Value: float64(snapshot.OutboundWriteBytes)},
{Name: "telesrv_mtproto_outbound_write_byte_limit", Value: float64(snapshot.OutboundWriteMaxBytes)},
{Name: "telesrv_mtproto_rpc_execution_owners", Value: float64(snapshot.RPCExecutionOwners)},
{Name: "telesrv_mtproto_rpc_execution_reserved_entries", Value: float64(snapshot.RPCExecutionReservedEntries)},
{Name: "telesrv_mtproto_rpc_execution_receipts", Value: float64(snapshot.RPCExecutionReceipts)},
{Name: "telesrv_mtproto_rpc_execution_receipt_budget_bytes", Value: float64(snapshot.RPCExecutionReceiptBudgetBytes)},
{Name: "telesrv_mtproto_rpc_execution_subscribers", Value: float64(snapshot.RPCExecutionSubscribers)},
}
}
// externalMediaOption 按配置启用外链媒体抓取;禁用时返回 nilNewService 跳过 nil option
// liveStreamDep 把可能为 nil 的 *livestream.Service 转成 rpc.LiveStreamsService
// 避免 typed-nil interfacenil 具体指针装进接口后 != nil 的坑)。
@ -270,6 +330,172 @@ func liveStreamDep(s *livestream.Service) rpc.LiveStreamsService {
return s
}
// verificationPeerVerifier writes the platform verification flag onto the peer
// record for app/verification.
//
// It is called from *inside* the store transaction that decides the application,
// which is the whole point of the port: "approved" and "target carries the badge"
// must commit together. That is why the transaction is taken from the context
// (postgres.VerificationTxFromContext) and written through — a write on a separate
// pool connection would survive a rollback of the decision and leave a peer
// wearing a badge no approved application backs.
//
// The app-service path is only the fallback for a context that carries no
// transaction (a non-postgres store, or a direct call): there is nothing to join
// then, and going through the services keeps their cache refresh behaviour.
type verificationPeerVerifier struct {
users interface {
SetVerified(ctx context.Context, userID int64, verified bool) (domain.User, error)
}
channels interface {
SetVerified(ctx context.Context, channelID int64, verified bool) (domain.Channel, error)
}
// channelRowCache is handed to the transaction-scoped channel store so the
// cached channel row is dropped on the flag write, exactly as the pooled store
// does it.
channelRowCache *postgres.ChannelRowCache
}
func (v verificationPeerVerifier) SetUserVerified(ctx context.Context, userID int64, verified bool) error {
if tx, ok := postgres.VerificationTxFromContext(ctx); ok {
_, err := postgres.NewUserStore(tx).SetVerified(ctx, userID, verified)
return err
}
if v.users == nil {
return fmt.Errorf("verification peer verifier: user service is not wired")
}
_, err := v.users.SetVerified(ctx, userID, verified)
return err
}
func (v verificationPeerVerifier) SetChannelVerified(ctx context.Context, channelID int64, verified bool) error {
if tx, ok := postgres.VerificationTxFromContext(ctx); ok {
opts := []postgres.ChannelStoreOption(nil)
if v.channelRowCache != nil {
opts = append(opts, postgres.WithChannelRowCache(v.channelRowCache))
}
_, err := postgres.NewChannelStore(tx, opts...).SetChannelVerified(ctx, channelID, verified)
return err
}
if v.channels == nil {
return fmt.Errorf("verification peer verifier: channel service is not wired")
}
_, err := v.channels.SetVerified(ctx, channelID, verified)
return err
}
var _ verificationapp.PeerVerifier = verificationPeerVerifier{}
// botVerificationMarkApplier writes a third-party mark on the decision's own
// transaction when there is one.
//
// postgres.DecideCustomVerificationRequest hands its callback a context carrying
// the transaction, and the pooled store would open a second, independently
// committing one -- so an approval whose mark write failed would leave the request
// approved with no mark. This adapter is what makes "approved implies mark exists"
// survive a rollback, exactly as verificationPeerVerifier does for the official flag.
type botVerificationMarkApplier struct {
store storepkg.BotVerificationStore
}
func (a botVerificationMarkApplier) GrantCustomVerification(ctx context.Context, mark domain.CustomVerification) (domain.CustomVerification, bool, error) {
if tx, ok := postgres.VerificationTxFromContext(ctx); ok {
return postgres.NewBotVerificationStore(tx).GrantCustomVerification(ctx, mark)
}
return a.store.GrantCustomVerification(ctx, mark)
}
func (a botVerificationMarkApplier) RevokeCustomVerification(ctx context.Context, verifierBotID int64, peer domain.Peer) (bool, error) {
if tx, ok := postgres.VerificationTxFromContext(ctx); ok {
return postgres.NewBotVerificationStore(tx).RevokeCustomVerification(ctx, verifierBotID, peer)
}
return a.store.RevokeCustomVerification(ctx, verifierBotID, peer)
}
var _ botverificationapp.MarkApplier = botVerificationMarkApplier{}
// compositeBotVerificationNotifier drops the cached peer projections before the
// edge rebuilds and pushes the peer, so a mark change cannot be pushed with a
// stale badge.
type compositeBotVerificationNotifier struct {
cache rpcProjectionVerificationNotifier
edge botverificationapp.PeerNotifier
}
func (n compositeBotVerificationNotifier) NotifyPeerBotVerification(ctx context.Context, peer domain.Peer) error {
if err := n.cache.NotifyPeerVerified(ctx, peer); err != nil && n.cache.log != nil {
n.cache.log.Warn("invalidate peer caches after third-party verification change",
zap.String("peer_type", string(peer.Type)), zap.Int64("peer_id", peer.ID), zap.Error(err))
}
if n.edge == nil {
return nil
}
return n.edge.NotifyPeerBotVerification(ctx, peer)
}
var _ botverificationapp.PeerNotifier = compositeBotVerificationNotifier{}
// rpcProjectionVerificationNotifier is the fallback badge-change hook, the same
// shape and for the same reason as rpcProjectionUsernameNotifier: the RPC edge
// owns both the cached peer projections and the tg.* push, and until it exposes
// NotifyPeerVerified only the invalidation half can be wired here. Invalidation is
// the half that must not be skipped — a decided application whose peer projection
// still says "not verified" would keep showing the old badge state to every client
// that reads from cache.
type rpcProjectionVerificationNotifier struct {
invalidator interface {
InvalidateRPCProjectionReadModelForUser(userID int64)
InvalidateRPCProjectionReadModelForChannel(channelID int64)
}
users storepkg.UserCache
log *zap.Logger
}
func (n rpcProjectionVerificationNotifier) NotifyPeerVerified(ctx context.Context, peer domain.Peer) error {
if n.invalidator == nil {
return nil
}
switch peer.Type {
case domain.PeerTypeUser:
n.invalidator.InvalidateRPCProjectionReadModelForUser(peer.ID)
// The shared user:base cache is the source the projection rebuilds from, so
// dropping only the projection would let it rebuild from a stale row.
if n.users != nil {
if err := n.users.Delete(ctx, []int64{peer.ID}); err != nil && n.log != nil {
n.log.Warn("invalidate base user cache after verification change",
zap.Int64("user_id", peer.ID), zap.Error(err))
}
}
case domain.PeerTypeChannel:
n.invalidator.InvalidateRPCProjectionReadModelForChannel(peer.ID)
}
return nil
}
// compositeVerificationNotifier drops the cached peer projections first and only
// then lets the protocol edge push the change, so the pushed peer is rebuilt from
// the committed row rather than from a cache entry written before the decision.
// A cache failure must not swallow the push: the push is what online clients see.
type compositeVerificationNotifier struct {
cache rpcProjectionVerificationNotifier
edge verificationapp.PeerNotifier
}
func (n compositeVerificationNotifier) NotifyPeerVerified(ctx context.Context, peer domain.Peer) error {
if err := n.cache.NotifyPeerVerified(ctx, peer); err != nil && n.cache.log != nil {
n.cache.log.Warn("invalidate peer caches after verification change",
zap.String("peer_type", string(peer.Type)), zap.Int64("peer_id", peer.ID), zap.Error(err))
}
if n.edge == nil {
return nil
}
return n.edge.NotifyPeerVerified(ctx, peer)
}
var _ verificationapp.PeerNotifier = compositeVerificationNotifier{}
var _ verificationapp.PeerNotifier = rpcProjectionVerificationNotifier{}
func externalMediaOption(cfg config.Config) filesapp.Option {
if !cfg.ExternalMediaEnable {
return nil
@ -312,6 +538,7 @@ func run(logger *zap.Logger) error {
logger.Info("telesrv starting",
zap.String("listen", cfg.ListenAddr),
zap.Int("dc", cfg.DC),
zap.String("default_country_code", cfg.DefaultCountryCode),
zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)),
zap.Int("tl_layer", tg.Layer),
zap.String("git_commit", buildMeta.Commit),
@ -325,10 +552,12 @@ func run(logger *zap.Logger) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
metricRegistry := obsmetrics.New()
metricRegistry.AddGaugeProvider(goRuntimeGaugeSamples)
// pprof 调试端点telesrv 是宿主进程(不在 docker 内docker stats 看不到它CPU/内存/
// goroutine/锁竞争的定位全靠此端点。早于重负载初始化启动,连 seed/预热阶段也可剖析。
startDebugServer(ctx, cfg.DebugAddr, logger)
startDebugServer(ctx, cfg.DebugAddr, metricRegistry, logger)
// 持久化依赖:先迁移 schema再建立连接。auth key 与业务事实落 PostgreSQL
// Redis 只承载可重建的短 TTL 状态、缓存、计数器和限流。
@ -350,6 +579,20 @@ func run(logger *zap.Logger) error {
return fmt.Errorf("connect postgres: %w", err)
}
defer pool.Close()
metricRegistry.AddGaugeProvider(func() []obsmetrics.GaugeSample {
stat := pool.Stat()
return []obsmetrics.GaugeSample{
{Name: "telesrv_postgres_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "total"}}, Value: float64(stat.TotalConns())},
{Name: "telesrv_postgres_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "acquired"}}, Value: float64(stat.AcquiredConns())},
{Name: "telesrv_postgres_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "idle"}}, Value: float64(stat.IdleConns())},
{Name: "telesrv_postgres_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "constructing"}}, Value: float64(stat.ConstructingConns())},
{Name: "telesrv_postgres_pool_max_connections", Value: float64(stat.MaxConns())},
{Name: "telesrv_postgres_pool_acquire_count", Value: float64(stat.AcquireCount())},
{Name: "telesrv_postgres_pool_acquire_wait_seconds", Value: stat.AcquireDuration().Seconds()},
{Name: "telesrv_postgres_pool_empty_acquire_count", Value: float64(stat.EmptyAcquireCount())},
{Name: "telesrv_postgres_pool_canceled_acquire_count", Value: float64(stat.CanceledAcquireCount())},
}
})
var telegramLoginService *telegramloginapp.Service
var telegramLoginIDTokens *telegramloginapp.IDTokenIssuer
@ -390,6 +633,19 @@ func run(logger *zap.Logger) error {
return fmt.Errorf("connect redis: %w", err)
}
defer func() { _ = rdb.Close() }()
metricRegistry.AddGaugeProvider(func() []obsmetrics.GaugeSample {
stat := rdb.PoolStats()
return []obsmetrics.GaugeSample{
{Name: "telesrv_redis_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "total"}}, Value: float64(stat.TotalConns)},
{Name: "telesrv_redis_pool_connections", Labels: []obsmetrics.Label{{Name: "state", Value: "idle"}}, Value: float64(stat.IdleConns)},
{Name: "telesrv_redis_pool_pending_requests", Value: float64(stat.PendingRequests)},
{Name: "telesrv_redis_pool_hits", Value: float64(stat.Hits)},
{Name: "telesrv_redis_pool_misses", Value: float64(stat.Misses)},
{Name: "telesrv_redis_pool_timeouts", Value: float64(stat.Timeouts)},
{Name: "telesrv_redis_pool_wait_count", Value: float64(stat.WaitCount)},
{Name: "telesrv_redis_pool_wait_seconds", Value: time.Duration(stat.WaitDurationNs).Seconds()},
}
})
logger.Info("persistence dependencies ready", zap.String("redis", cfg.RedisAddr))
if cfg.TelegramLoginEnabled {
telegramLoginHTTPHandler, err = telegramloginhttp.NewHandler(telegramloginhttp.Config{
@ -420,6 +676,9 @@ func run(logger *zap.Logger) error {
botCallbackStore := redisstore.NewBotCallbackRegistryStore(rdb)
ephemeralStore := redisstore.NewEphemeralMessageStore(rdb)
ephemeralReportStore := postgres.NewEphemeralReportStore(pool)
moderationReportStore := postgres.NewModerationReportStore(pool)
authDeliveryReportStore := postgres.NewAuthDeliveryReportStore(pool)
clientTelemetryStore := postgres.NewClientTelemetryStore(pool)
boxIDAllocator := redisstore.NewBoxIDAllocator(rdb, postgres.NewMessageBoxCounterSource(pool))
channelIDAllocator := redisstore.NewChannelIDAllocator(rdb, postgres.NewChannelIDCounterSource(pool))
channelMessageIDAllocator := redisstore.NewChannelMessageIDAllocator(rdb, postgres.NewChannelMessageIDCounterSource(pool))
@ -491,6 +750,15 @@ func run(logger *zap.Logger) error {
zap.Int("blobs", stats.Blobs),
)
}
if stats, err := filesService.SeedPremiumPromo(ctx, cfg.PremiumPromoSeedDir); err != nil {
return fmt.Errorf("seed premium promo: %w", err)
} else if !stats.Skipped {
logger.Info("Premium promo 视频种子导入完成",
zap.String("dir", cfg.PremiumPromoSeedDir),
zap.Int("videos", stats.Videos),
zap.Int("blobs", stats.Blobs),
)
}
if stats, err := filesService.SeedAppearance(ctx); err != nil {
return fmt.Errorf("seed appearance: %w", err)
} else if !stats.Skipped {
@ -545,6 +813,8 @@ func run(logger *zap.Logger) error {
tempAuthKeyStore := postgres.NewTempAuthKeyBindingStore(pool)
inlineRegistryStore := redisstore.NewInlineRegistryStore(rdb)
codeStore := redisstore.NewCodeStore(rdb)
authDeliveryReportService := authdiagnosticsapp.NewService(codeStore, authDeliveryReportStore)
clientTelemetryService := clienttelemetryapp.NewService(clientTelemetryStore)
rateLimiter := redisstore.NewRateLimiter(rdb)
activeSessions := mtprotoedge.NewSessionManager(logger.Named("mtprotoedge").Named("sessions"))
adminService := adminapp.NewService(adminapp.Dependencies{
@ -560,6 +830,9 @@ func run(logger *zap.Logger) error {
WithBotAPIUpdateRetention(botAPIUpdateStore, cfg.BotAPIUpdateRetention).
WithAuthKeySessionLayerRetention(authKeyStore).
WithLoginCodeDeliveryRetention(messageStore).
WithClientTelemetryRetention(clientTelemetryStore, 30*24*time.Hour).
WithAuthDeliveryReportRetention(authDeliveryReportStore, 30*24*time.Hour).
WithModerationRetention(moderationReportStore).
WithUserUpdateRetention(updateEventStore).
WithChannelUpdateRetention(channelStore).
WithOrphanAuthKeyRetention(authKeyStore, activeSessions, cfg.OrphanAuthKeyRetention).
@ -600,6 +873,7 @@ func run(logger *zap.Logger) error {
// userCache 与 users 服务共享同一实例bot 元数据写入version bump后必须
// 失效缓存,否则 TTL 内 getUsers 回旧 first_name/旧 bot_info_version。
userCache := redisstore.NewUserCache(rdb, redisstore.DefaultUserCacheTTL)
accountLifecycleStore := postgres.NewAccountLifecycleStore(pool)
accountOptions := []account.ServiceOption{
account.WithReactionSettings(passwordStore),
account.WithAccountSettings(passwordStore),
@ -610,7 +884,7 @@ func run(logger *zap.Logger) error {
account.WithBusinessAutomation(passwordStore),
account.WithUsers(userStore),
account.WithPhoneChange(phoneChangeStore, authzStore, codeStore, userCache, cfg.DevAuthCode, cfg.AuthCodeTTL, cfg.AuthCodeMaxAttempts),
account.WithAccountLifecycle(postgres.NewAccountLifecycleStore(pool)),
account.WithAccountLifecycle(accountLifecycleStore),
account.WithPublicBaseURL(cfg.PublicBaseURL),
account.WithEmailSignup(cfg.EmailSignupEnable),
account.WithEmailSignupPhonePrefixes(cfg.EmailSignupPhonePrefixes),
@ -672,6 +946,7 @@ func run(logger *zap.Logger) error {
botsapp.WithStickerSetCreator(filesService),
botsapp.WithUserStickerSets(accountService),
botsapp.WithTelegramLogin(telegramLoginService),
botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow),
botsapp.WithPublicBaseURL(cfg.PublicBaseURL))
groupCallStore := postgres.NewGroupCallStore(pool)
groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL))
@ -751,6 +1026,7 @@ func run(logger *zap.Logger) error {
RingTimeout: cfg.CallRingTimeout,
TombstoneTTL: cfg.CallTombstoneTTL,
MaxActivePerUser: cfg.CallMaxActivePerUser,
MaxRegistryEntries: cfg.CallRegistryMaxEntries,
SignalingRatePerSecond: cfg.CallSignalingRate,
})
// 私聊端对端加密Secret Chat握手状态机 + qts 投递队列(盲中继)。
@ -758,7 +1034,10 @@ func run(logger *zap.Logger) error {
encryptedQueueStore := postgres.NewEncryptedQueueStore(pool)
secretChatService := secretchatapp.NewService(secretChatStore, encryptedQueueStore, secretChatIDAllocator)
starsStore := postgres.NewStarsStore(pool)
starsService := stars.NewService(starsStore, stars.WithStartingGrant(cfg.StarsStartingGrant))
starsPurchaseStore := postgres.NewStarsPurchaseStore(pool, messageStore, channelStore)
starsService := stars.NewService(starsStore,
stars.WithStartingGrant(cfg.StarsStartingGrant),
stars.WithPurchaseStore(starsPurchaseStore))
starGiftStore := postgres.NewStarGiftStore(pool)
starGiftUpgradeStore := postgres.NewStarGiftUpgradeStore(pool, messageStore, postgres.WithStarGiftLifecyclePolicy(domain.StarGiftLifecyclePolicy{
TransferStars: cfg.StarGiftTransferStars, DropOriginalDetailsStars: cfg.StarGiftDropOriginalDetailsStars,
@ -789,6 +1068,7 @@ func run(logger *zap.Logger) error {
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(adminService))
privacyService.ConfigureReadModels(usersService, channelStore)
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
botsService.SetAIChatGenerator(aiComposeService)
dialogsService := dialogs.NewService(dialogStore, channelStore).Configure(
@ -809,6 +1089,7 @@ func run(logger *zap.Logger) error {
)
communitiesService := communitiesapp.NewService(communityStore)
ephemeralService := ephemeralapp.NewService(ephemeralStore, channelsService, usersService, botsService)
storiesService := storiesapp.NewService(storyStore, storiesapp.WithChannelStoryAccess(channelsService))
chatlistsService := chatlistsapp.NewService(
chatlistStore,
dialogStore,
@ -826,6 +1107,21 @@ func run(logger *zap.Logger) error {
messageapp.WithSendPermissionChecker(adminService),
messageapp.WithBusinessAutomation(passwordStore, businessAutomationOptions...),
)
moderationService := moderationapp.NewService(
moderationReportStore,
moderationapp.WithMessageReaders(messagesService, channelsService),
moderationapp.WithStoryReader(storiesService),
moderationapp.WithPeerReaders(usersService, channelsService),
moderationapp.WithProfilePhotoReader(filesService),
)
legacyReportsMigrated, err := moderationService.MigrateLegacyEphemeralReports(ctx, ephemeralReportStore, 500)
if err != nil {
return fmt.Errorf("migrate legacy ephemeral reports: %w", err)
}
if legacyReportsMigrated > 0 {
logger.Info("旧 ephemeral 举报已迁移到统一审核管线",
zap.Int("reports", legacyReportsMigrated))
}
translationService := translationapp.NewService(
messagesService,
channelsService,
@ -858,10 +1154,82 @@ func run(logger *zap.Logger) error {
}),
auth.WithEmailSignup(cfg.EmailSignupEnable),
auth.WithEmailSignupPhonePrefixes(cfg.EmailSignupPhonePrefixes))
// Collectible (NFT) usernames and the gramsrv composite account rating are
// optional read models projected at the protocol edge. The rating worker
// computes and persists scores; profile reads never recompute them.
collectibleUsernameStore := postgres.NewCollectibleUsernameStore(pool)
accountRatingStore := postgres.NewAccountRatingStore(pool)
usernamesService := usernamesapp.NewService(
usernamesapp.WithRegistryStore(collectibleUsernameStore),
usernamesapp.WithCollectibleStore(collectibleUsernameStore),
usernamesapp.WithURLTemplate(cfg.CollectibleUsernameURLTemplate),
usernamesapp.WithPublicBaseURL(cfg.PublicBaseURL),
usernamesapp.WithLogger(logger.Named("app").Named("usernames")),
)
ratingService := ratingapp.NewService(
ratingapp.WithStore(accountRatingStore),
ratingapp.WithEnabled(cfg.RatingEnabled),
ratingapp.WithWeights(cfg.AccountRatingWeights()),
ratingapp.WithPendingDelay(cfg.RatingPendingDelay),
ratingapp.WithStaleAfter(cfg.RatingStaleAfter),
ratingapp.WithLogger(logger.Named("app").Named("rating")),
)
// Official platform verification: applications are filed through the built-in
// @verifybot and decided in the admin panel. Every eligibility rule lives in
// this service; the bot and the panel are only its two surfaces.
verificationStore := postgres.NewVerificationStore(pool)
verificationLogger := logger.Named("app").Named("verification")
verificationService := verificationapp.NewService(
verificationapp.WithStore(verificationStore),
verificationapp.WithUserDirectory(usersService),
verificationapp.WithBotDirectory(botsService),
verificationapp.WithChannelDirectory(channelsService),
verificationapp.WithAccountFreezeProvider(adminService),
verificationapp.WithPeerVerifier(verificationPeerVerifier{
users: usersService,
channels: channelsService,
channelRowCache: channelRowCache,
}),
verificationapp.WithRateLimiter(rateLimiter, cfg.VerificationApplyRateLimit, cfg.VerificationApplyRateWindow),
verificationapp.WithEnabled(cfg.VerificationEnabled),
verificationapp.WithAllowUserTargets(cfg.VerificationAllowUserTargets),
verificationapp.WithRejectCooldown(cfg.VerificationRejectCooldown),
verificationapp.WithMaxActivePerUser(cfg.VerificationMaxActivePerUser),
verificationapp.WithLogger(verificationLogger),
)
// @verifybot is the applicant surface, and the notifier that carries decisions
// back to the applicant as ordinary messages. Both directions are deferred
// injections because the bots service is built before the peer directories the
// verification service needs.
botsService.SetVerification(verificationService)
verificationService.SetApplicantNotifier(botsService)
// Third-party verification is a SEPARATE mechanism: a verifier bot marks peers
// with its own custom-emoji icon and description, which clients render before the
// name. It shares no state with the official badge above -- different tables,
// different rights, different TL fields (bot_verification_icon / bot_verification
// versus verified).
botVerificationStore := postgres.NewBotVerificationStore(pool)
botVerificationService := botverificationapp.NewService(
botverificationapp.WithStore(botVerificationStore),
botverificationapp.WithUserDirectory(usersService),
botverificationapp.WithBotDirectory(botsService),
botverificationapp.WithChannelDirectory(channelsService),
// The icon must be a real custom emoji document: an id no client can fetch
// renders as nothing, so the badge would be silently invisible.
botverificationapp.WithIconResolver(filesService),
botverificationapp.WithMarkApplier(botVerificationMarkApplier{store: botVerificationStore}),
botverificationapp.WithRateLimiter(rateLimiter, cfg.BotVerificationRequestRateLimit, cfg.BotVerificationRequestRateWindow),
botverificationapp.WithEnabled(cfg.BotVerificationEnabled),
botverificationapp.WithMaxPerVerifier(cfg.BotVerificationMaxPerVerifier),
botverificationapp.WithLogger(logger.Named("app").Named("botverification")),
)
// @verifierbot files applications with the operator and reports decisions back.
botsService.SetCustomVerification(botVerificationService)
botVerificationService.SetApplicantNotifier(botsService)
updatesService := updates.NewService(updateStateStore, updateEventStore, updates.WithLogger(logger.Named("app").Named("updates")))
rpc.SetModerationWarnings(cfg.ScamWarning, cfg.FakeWarning)
router := rpc.New(rpc.Config{
DC: cfg.DC,
DefaultCountryCode: cfg.DefaultCountryCode,
IP: cfg.AdvertiseIP,
Port: port,
OutboundPushTimeout: cfg.OutboundPushTimeout,
@ -886,6 +1254,8 @@ func run(logger *zap.Logger) error {
TempKeyResolveCacheMaxEntries: cfg.TempKeyResolveCacheMaxEntries,
}, rpc.Deps{
Auth: authService,
AuthDeliveryReports: authDeliveryReportService,
ClientTelemetry: clientTelemetryService,
AuthKeySessionLayers: authKeyStore,
Account: accountService,
Privacy: privacyService,
@ -895,42 +1265,48 @@ func run(logger *zap.Logger) error {
help.WithEmailSignupPhonePrefixes(cfg.EmailSignupPhonePrefixes),
help.WithAccountFreezeProvider(adminService),
),
AccountFreeze: adminService,
AICompose: aiComposeService,
Ephemeral: ephemeralService,
EphemeralPush: ephemeralStore,
EphemeralReports: ephemeralReportStore,
Users: usersService,
TelegramLogin: telegramLoginRPCDependency(telegramLoginService),
Updates: updatesService,
BootstrapUpdates: bootstrapUpdateStore,
BotAPIUpdates: botAPIUpdateStore,
BotCallbacks: botCallbackStore,
Contacts: contactsService,
Dialogs: dialogsService,
Chatlists: chatlistsService,
Messages: messagesService,
Translation: translationService,
Channels: channelsService,
Communities: communitiesService,
Files: filesService,
Bots: botsService,
Polls: pollsapp.NewService(pollStore),
Stories: storiesapp.NewService(storyStore, storiesapp.WithChannelStoryAccess(channelsService)),
Phone: phoneService,
SecretChats: secretChatService,
Stars: starsService,
Gifts: giftsService,
Passkey: passkeyService,
Themes: themeService,
GroupCalls: groupCallsService,
LiveStreams: liveStreamDep(liveStreamService),
SFU: sfuService,
TURN: turnService,
LangPack: langPackService,
Sessions: activeSessions,
Inline: inlineRegistryStore,
Limiter: rateLimiter,
AccountFreeze: adminService,
AICompose: aiComposeService,
Ephemeral: ephemeralService,
EphemeralPush: ephemeralStore,
Moderation: moderationService,
Users: usersService,
Usernames: usernamesService,
AccountRatings: ratingService,
BotVerifications: botVerificationService,
TelegramLogin: telegramLoginRPCDependency(telegramLoginService),
Updates: updatesService,
BootstrapUpdates: bootstrapUpdateStore,
BotAPIUpdates: botAPIUpdateStore,
BotCallbacks: botCallbackStore,
Contacts: contactsService,
Dialogs: dialogsService,
Chatlists: chatlistsService,
Messages: messagesService,
Translation: translationService,
Channels: channelsService,
Communities: communitiesService,
Files: filesService,
PremiumPromo: filesService,
Bots: botsService,
ServiceBotCallbacks: botsService,
Polls: pollsapp.NewService(pollStore),
Stories: storiesService,
Phone: phoneService,
SecretChats: secretChatService,
Stars: starsService,
Gifts: giftsService,
Passkey: passkeyService,
Themes: themeService,
GroupCalls: groupCallsService,
LiveStreams: liveStreamDep(liveStreamService),
SFU: sfuService,
TURN: turnService,
LangPack: langPackService,
Sessions: activeSessions,
Metrics: metricRegistry,
Inline: inlineRegistryStore,
Limiter: rateLimiter,
}, logger.Named("rpc"), clock.System)
readModelListener := postgres.NewReadModelChangeListener(cfg.PostgresDSN, postgres.ReadModelCacheSet{
ReadModelVersions: readModelVersionStore,
@ -940,7 +1316,7 @@ func run(logger *zap.Logger) error {
ChannelBoosts: channelBoostCache,
Contacts: postgres.ContactReadModelCaches{contactStore, contactsService},
Dialogs: dialogsService,
Privacy: privacyStore,
Privacy: privacyService,
ProfilePhotos: cachedPhotos,
Stories: router,
ChannelFullBots: router,
@ -951,27 +1327,108 @@ func run(logger *zap.Logger) error {
BaseUsers: userCache,
BotProfiles: botsService,
StarGifts: giftsService,
AccountSettings: router,
}, logger.Named("store").Named("read-model-listener"))
go readModelListener.Run(ctx)
activeSessions.SetLifecycleObserver(router)
adminService.Configure(adminapp.Dependencies{
Auth: authService,
Revoker: router,
Users: usersService,
Stars: starsService,
StarsNotifier: router,
UserNotifier: router,
FreezeNotifier: router,
Channels: channelsService,
ChannelNotifier: router,
Messages: messagesService,
Gifts: giftsService,
Photos: filesService,
StickerSets: filesService,
GiftGranter: router,
Bots: botsService,
Emoji: filesService,
Auth: authService,
Revoker: router,
Users: usersService,
Stars: starsService,
StarsNotifier: router,
UserNotifier: router,
UserModerationNotifier: router,
FreezeNotifier: router,
Channels: channelsService,
ChannelNotifier: router,
Messages: messagesService,
Gifts: giftsService,
Photos: filesService,
StickerSets: filesService,
GiftGranter: router,
Bots: botsService,
Emoji: filesService,
Moderation: moderationService,
Usernames: usernamesService,
Rating: ratingService,
Verification: verificationService,
BotVerification: botVerificationService,
})
// The RPC edge owns the tg.* projection cache and the standard non-PTS
// updateUser/updateChannel refresh, so committed registry mutations are
// visible to online viewers immediately.
usernamesService.SetPeerUsernameNotifier(router)
// The badge change is a peer fact the protocol edge caches and pushes, so the
// verification service gets the same hook the username registry uses. The
// assertion is deliberately dynamic: NotifyPeerVerified lands with the edge
// agent, and until then only projection invalidation is wired — a decision can
// then never be masked by a stale projection, and clients converge on their next
// authoritative peer read.
if notifier, ok := any(router).(verificationapp.PeerNotifier); ok {
// Compose rather than choose: the decision writes users.verified inside the
// verification transaction (through postgres.VerificationTxFromContext), so it
// bypasses users.Service and its cache refresh. Dropping the shared user:base
// entry before the edge builds the pushed tg.User is what keeps the badge in
// that push from being one beat stale; the cross-instance read-model listener
// would otherwise only catch up asynchronously.
verificationService.SetPeerNotifier(compositeVerificationNotifier{
cache: rpcProjectionVerificationNotifier{
invalidator: router,
users: userCache,
log: verificationLogger,
},
edge: notifier,
})
} else {
verificationService.SetPeerNotifier(rpcProjectionVerificationNotifier{
invalidator: router,
users: userCache,
log: verificationLogger,
})
logger.Warn("verification badge update push is not implemented by the RPC edge; only projection invalidation is wired",
zap.String("expected_hook", "rpc.Router.NotifyPeerVerified"))
}
// The third-party mark lives on the same peer projections as the official flag,
// so it needs the same edge hook. Composed with the cache drop for the same reason:
// the mark can be written on the decision's own transaction, bypassing the app
// services that would otherwise refresh the shared user:base entry.
if notifier, ok := any(router).(botverificationapp.PeerNotifier); ok {
botVerificationService.SetPeerNotifier(compositeBotVerificationNotifier{
cache: rpcProjectionVerificationNotifier{
invalidator: router,
users: userCache,
log: verificationLogger,
},
edge: notifier,
})
} else {
logger.Warn("third-party verification push is not implemented by the RPC edge",
zap.String("expected_hook", "rpc.Router.NotifyPeerBotVerification"))
}
go ratingapp.NewRecomputeWorker(ratingService, logger.Named("rating").Named("recompute"),
cfg.RatingRecomputeInterval, cfg.RatingRecomputeBatch).Run(ctx)
// Applicant notifications are delivered from a durable outbox, never inside the
// decision transaction: @verifybot may be blocked and the panel must not wait on
// a message send.
go verificationapp.NewNotificationWorker(verificationService, logger.Named("verification").Named("notify"),
cfg.VerificationNotifyInterval, cfg.VerificationNotifyBatch).Run(ctx)
moderationActionOptions := []moderationapp.ActionExecutorOption{}
if cfg.PublicLinkWebAddr != "" {
moderationActionOptions = append(
moderationActionOptions,
moderationapp.WithAppealLinks(moderationService, cfg.PublicBaseURL),
)
}
moderationActionExecutor := moderationapp.NewActionExecutor(
adminService, channelsService, router, accountLifecycleStore,
moderationActionOptions...,
)
go moderationapp.NewActionWorker(
moderationReportStore,
moderationActionExecutor,
logger.Named("moderation").Named("actions"),
).Run(ctx)
// bot session 撤销、在线通知与 @ChatBot 流式草稿推送经 router 实现(需 tg.* 边界),
// router 创建后注入。
botsService.SetRouterHooks(router)
@ -981,6 +1438,7 @@ func run(logger *zap.Logger) error {
rpc.WithOutboxBatch(cfg.OutboxBatch),
rpc.WithOutboxInterval(cfg.OutboxInterval),
rpc.WithOutboxPushTimeout(cfg.OutboundPushTimeout),
rpc.WithOutboxMetrics(metricRegistry),
rpc.WithOutboxUpdateBuilder(router.BuildOutboxUpdates),
).Run(ctx)
go rpc.NewBootstrapUpdateDispatcher(router, logger.Named("rpc").Named("bootstrap")).Run(ctx)
@ -1031,61 +1489,74 @@ func run(logger *zap.Logger) error {
if _, err := botapi.Start(ctx, cfg.BotAPIAddr, botsService, usersService, router, router, logger.Named("botapi")); err != nil {
return fmt.Errorf("start bot api: %w", err)
}
if _, err := adminapi.Start(ctx, adminapi.Config{Addr: cfg.AdminAPIAddr, Token: cfg.AdminAPIToken}, adminService, logger.Named("adminapi")); err != nil {
// Scoped tokens carry a bounded permission set; the master token stays
// unrestricted, so a deployment that configures none behaves exactly as before.
adminScopedTokens := make([]adminapi.ScopedToken, 0, len(cfg.AdminScopedTokens))
for _, item := range cfg.AdminScopedTokens {
adminScopedTokens = append(adminScopedTokens, adminapi.ScopedToken{
Name: item.Name,
Token: item.Token,
Permissions: item.Permissions,
})
}
if _, err := adminapi.Start(ctx, adminapi.Config{
Addr: cfg.AdminAPIAddr,
Token: cfg.AdminAPIToken,
ScopedTokens: adminScopedTokens,
}, adminService, logger.Named("adminapi")); err != nil {
return fmt.Errorf("start admin api: %w", err)
}
if _, err := web.Start(ctx, web.Config{
Addr: cfg.PublicLinkWebAddr,
PublicBaseURL: cfg.PublicBaseURL,
AppScheme: cfg.PublicAppScheme,
AppLinkBase: cfg.PublicAppLinkBase,
WebBaseURL: cfg.PublicWebBaseURL,
AppName: cfg.PublicAppName,
DownloadURL: cfg.PublicDownloadURL,
StickerSets: filesService,
Users: userStore,
Channels: channelStore,
Privacy: privacyService,
Photos: filesService,
UniqueGifts: giftsService,
GiftWithdrawals: giftsService,
TelegramLogin: telegramLoginHTTPHandler,
Addr: cfg.PublicLinkWebAddr,
PublicBaseURL: cfg.PublicBaseURL,
AppScheme: cfg.PublicAppScheme,
AppLinkBase: cfg.PublicAppLinkBase,
WebBaseURL: cfg.PublicWebBaseURL,
AppName: cfg.PublicAppName,
DownloadURL: cfg.PublicDownloadURL,
StickerSets: filesService,
Users: userStore,
Channels: channelStore,
Privacy: privacyService,
Photos: filesService,
UniqueGifts: giftsService,
GiftWithdrawals: giftsService,
ModerationAppeals: moderationService,
TelegramLogin: telegramLoginHTTPHandler,
}, logger.Named("public-web")); err != nil {
return fmt.Errorf("start public Web: %w", err)
}
srv := mtprotoedge.New(mtprotoedge.Options{
Logger: logger.Named("mtprotoedge"),
DC: cfg.DC,
StrictDC: cfg.StrictDCCheck,
RSAKey: rsaKey,
LayerRPC: router,
AuthKeys: authKeyStore,
ActiveSessions: activeSessions,
ObfuscatedTCP: true,
WebSocket: cfg.WebSocketEnable,
WebSocketAllowedOrigins: cfg.WebSocketAllowedOrigins,
MaxConnections: cfg.MTProtoMaxConnections,
MaxConnectionsPerIP: cfg.MTProtoMaxConnectionsPerIP,
MaxConcurrentHandshakes: cfg.MTProtoMaxConcurrentHandshakes,
RPCMaxInflight: cfg.MTProtoRPCMaxInflight,
RPCQueueSize: cfg.MTProtoRPCQueueSize,
RPCTimeout: cfg.MTProtoRPCTimeout,
RPCGlobalWorkers: cfg.MTProtoRPCGlobalWorkers,
RPCGlobalMaxTasks: cfg.MTProtoRPCGlobalMaxTasks,
RPCGlobalMaxBytes: cfg.MTProtoRPCGlobalMaxBytes,
RPCResultCacheMaxEntries: cfg.MTProtoRPCResultCacheMaxEntries,
RPCResultCacheMaxBytes: cfg.MTProtoRPCResultCacheMaxBytes,
RPCResultCacheAuthMaxEntries: cfg.MTProtoRPCResultCacheAuthMaxEntries,
RPCResultCacheAuthMaxBytes: cfg.MTProtoRPCResultCacheAuthMaxBytes,
RPCResultCacheSessionMaxEntries: cfg.MTProtoRPCResultCacheSessionMaxEntries,
RPCResultCacheSessionMaxBytes: cfg.MTProtoRPCResultCacheSessionMaxBytes,
RPCResultPendingPerAuth: cfg.MTProtoRPCResultPendingPerAuth,
InboundFrameGlobalMaxBytes: cfg.MTProtoInboundFrameGlobalMaxBytes,
OutboundQueueSize: cfg.MTProtoOutboundQueueSize,
OutboundControlQueueSize: cfg.MTProtoOutboundControlQueueSize,
OutboundTrackedGlobalMaxBytes: cfg.MTProtoOutboundTrackedGlobalMaxBytes,
OutboundWriteGlobalMaxBytes: cfg.MTProtoOutboundWriteGlobalMaxBytes,
Logger: logger.Named("mtprotoedge"),
DC: cfg.DC,
StrictDC: cfg.StrictDCCheck,
RSAKey: rsaKey,
LayerRPC: router,
AuthKeys: authKeyStore,
ActiveSessions: activeSessions,
Metrics: metricRegistry,
ObfuscatedTCP: true,
WebSocket: cfg.WebSocketEnable,
WebSocketAllowedOrigins: cfg.WebSocketAllowedOrigins,
MaxConnections: cfg.MTProtoMaxConnections,
MaxConnectionsPerIP: cfg.MTProtoMaxConnectionsPerIP,
MaxConcurrentHandshakes: cfg.MTProtoMaxConcurrentHandshakes,
RPCMaxInflight: cfg.MTProtoRPCMaxInflight,
RPCQueueSize: cfg.MTProtoRPCQueueSize,
RPCTimeout: cfg.MTProtoRPCTimeout,
RPCGlobalWorkers: cfg.MTProtoRPCGlobalWorkers,
RPCGlobalMaxTasks: cfg.MTProtoRPCGlobalMaxTasks,
RPCGlobalMaxBytes: cfg.MTProtoRPCGlobalMaxBytes,
RPCExecutionMaxEntries: cfg.MTProtoRPCExecutionMaxEntries,
RPCExecutionAuthMaxEntries: cfg.MTProtoRPCExecutionAuthMaxEntries,
RPCExecutionSessionMaxEntries: cfg.MTProtoRPCExecutionSessionMaxEntries,
RPCExecutionPendingPerAuth: cfg.MTProtoRPCExecutionPendingPerAuth,
InboundFrameGlobalMaxBytes: cfg.MTProtoInboundFrameGlobalMaxBytes,
OutboundQueueSize: cfg.MTProtoOutboundQueueSize,
OutboundControlQueueSize: cfg.MTProtoOutboundControlQueueSize,
OutboundTrackedGlobalMaxBytes: cfg.MTProtoOutboundTrackedGlobalMaxBytes,
OutboundWriteGlobalMaxBytes: cfg.MTProtoOutboundWriteGlobalMaxBytes,
OnServing: func(_ net.Addr) {
logger.Info("telesrv service ready",
zap.String("listen", cfg.ListenAddr),
@ -1097,6 +1568,9 @@ func run(logger *zap.Logger) error {
)
},
})
metricRegistry.AddGaugeProvider(func() []obsmetrics.GaugeSample {
return mtprotoRuntimeGaugeSamples(srv.RuntimeSnapshot())
})
// This is intentionally the final startup operation. ListenAndServe owns the
// public listener so no seed/prewarm work can run after port 2398 is exposed.
return srv.ListenAndServe(ctx, cfg.ListenAddr)