216 lines
8.6 KiB
Go
216 lines
8.6 KiB
Go
// Command telesrv 是基于 gotd/td 的 Telegram-like server(第一兼容目标:Telegram Desktop)。
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net"
|
||
"os"
|
||
"os/signal"
|
||
"strconv"
|
||
"syscall"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/gotd/td/clock"
|
||
"github.com/gotd/td/exchange"
|
||
"github.com/gotd/td/tg"
|
||
|
||
"telesrv/internal/app/account"
|
||
"telesrv/internal/app/auth"
|
||
channelapp "telesrv/internal/app/channels"
|
||
"telesrv/internal/app/contacts"
|
||
"telesrv/internal/app/dialogs"
|
||
filesapp "telesrv/internal/app/files"
|
||
"telesrv/internal/app/help"
|
||
"telesrv/internal/app/langpack"
|
||
"telesrv/internal/app/maintenance"
|
||
messageapp "telesrv/internal/app/messages"
|
||
privacyapp "telesrv/internal/app/privacy"
|
||
"telesrv/internal/app/updates"
|
||
"telesrv/internal/app/users"
|
||
"telesrv/internal/config"
|
||
"telesrv/internal/mtprotoedge"
|
||
"telesrv/internal/rpc"
|
||
"telesrv/internal/store/postgres"
|
||
"telesrv/internal/store/redisstore"
|
||
)
|
||
|
||
func main() {
|
||
logger, err := zap.NewDevelopment()
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "init logger:", err)
|
||
os.Exit(1)
|
||
}
|
||
defer func() { _ = logger.Sync() }()
|
||
|
||
if err := run(logger); err != nil {
|
||
logger.Error("telesrv 退出", zap.Error(err))
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func run(logger *zap.Logger) error {
|
||
cfg, err := config.Load()
|
||
if err != nil {
|
||
return fmt.Errorf("load config: %w", err)
|
||
}
|
||
|
||
rsaKey, err := mtprotoedge.LoadOrGenerateRSAKey(cfg.RSAKeyPath)
|
||
if err != nil {
|
||
return fmt.Errorf("server rsa key: %w", err)
|
||
}
|
||
fingerprint := exchange.PrivateKey{RSA: rsaKey}.Fingerprint()
|
||
|
||
_, portStr, err := net.SplitHostPort(cfg.ListenAddr)
|
||
if err != nil {
|
||
return fmt.Errorf("parse listen addr %q: %w", cfg.ListenAddr, err)
|
||
}
|
||
port, err := strconv.Atoi(portStr)
|
||
if err != nil {
|
||
return fmt.Errorf("parse listen port %q: %w", portStr, err)
|
||
}
|
||
|
||
// tg.Layer 来自 gotd/td v0.144.0(应为 225),需与目标 TDesktop 基线对齐。
|
||
logger.Info("telesrv 启动",
|
||
zap.String("listen", cfg.ListenAddr),
|
||
zap.Int("dc", cfg.DC),
|
||
zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)),
|
||
zap.Int("tl_layer", tg.Layer),
|
||
zap.String("rsa_key", cfg.RSAKeyPath),
|
||
zap.Int64("rsa_fingerprint", fingerprint),
|
||
)
|
||
|
||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
defer stop()
|
||
|
||
// 持久化依赖:先迁移 schema,再建立连接。auth_key 落 PostgreSQL、session 落 Redis。
|
||
// 依赖由 deploy/docker-compose.yml 启动;连不上则启动失败(开发期须先 docker compose up)。
|
||
if err := postgres.Migrate(cfg.PostgresDSN); err != nil {
|
||
return fmt.Errorf("postgres migrate: %w", err)
|
||
}
|
||
pool, err := postgres.Open(ctx, cfg.PostgresDSN,
|
||
postgres.WithMaxConns(cfg.PostgresMaxConns),
|
||
postgres.WithMinConns(cfg.PostgresMinConns),
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("connect postgres: %w", err)
|
||
}
|
||
defer pool.Close()
|
||
|
||
rdb, err := redisstore.Open(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)
|
||
if err != nil {
|
||
return fmt.Errorf("connect redis: %w", err)
|
||
}
|
||
defer func() { _ = rdb.Close() }()
|
||
logger.Info("持久化依赖就绪", zap.String("redis", cfg.RedisAddr))
|
||
|
||
ln, err := net.Listen("tcp", cfg.ListenAddr)
|
||
if err != nil {
|
||
return fmt.Errorf("listen %q: %w", cfg.ListenAddr, err)
|
||
}
|
||
|
||
authKeyStore := postgres.NewAuthKeyStore(pool)
|
||
userStore := postgres.NewUserStore(pool)
|
||
authzStore := postgres.NewAuthorizationStore(pool)
|
||
updateStateStore := postgres.NewUpdateStateStore(pool)
|
||
updateEventStore := postgres.NewUpdateEventStore(pool)
|
||
dispatchOutboxStore := postgres.NewDispatchOutboxStore(pool, postgres.WithLeaseTimeout(cfg.OutboxLeaseTimeout))
|
||
ptsAllocator := redisstore.NewPtsAllocator(rdb, updateEventStore)
|
||
boxIDAllocator := redisstore.NewBoxIDAllocator(rdb, postgres.NewMessageBoxCounterSource(pool))
|
||
channelIDAllocator := redisstore.NewChannelIDAllocator(rdb, postgres.NewChannelIDCounterSource(pool))
|
||
channelPtsAllocator := redisstore.NewChannelPtsAllocator(rdb, postgres.NewChannelPtsCounterSource(pool))
|
||
channelMessageIDAllocator := redisstore.NewChannelMessageIDAllocator(rdb, postgres.NewChannelMessageIDCounterSource(pool))
|
||
contactStore := postgres.NewContactStore(pool)
|
||
dialogStore := postgres.NewDialogStore(pool)
|
||
messageStore := postgres.NewMessageStore(pool, postgres.WithMessageAllocators(boxIDAllocator, ptsAllocator))
|
||
channelStore := postgres.NewChannelStore(pool, postgres.WithChannelAllocators(channelIDAllocator, channelPtsAllocator, channelMessageIDAllocator))
|
||
mediaStore := postgres.NewMediaStore(pool)
|
||
privacyStore := postgres.NewPrivacyStore(pool)
|
||
blobBackend, err := filesapp.NewLocalFS(cfg.BlobDir)
|
||
if err != nil {
|
||
return fmt.Errorf("init blob backend: %w", err)
|
||
}
|
||
filesService := filesapp.NewService(mediaStore, blobBackend, cfg.DC)
|
||
if stats, err := filesService.SeedMedia(ctx, cfg.StickerSeedDir, cfg.StickerSeedMaxSets); err != nil {
|
||
return fmt.Errorf("seed media: %w", err)
|
||
} else if !stats.Skipped {
|
||
logger.Info("媒体种子导入完成",
|
||
zap.String("dir", cfg.StickerSeedDir),
|
||
zap.Int("reactions", stats.Reactions),
|
||
zap.Int("sticker_sets", stats.StickerSets),
|
||
zap.Int("documents", stats.Documents),
|
||
zap.Int("blobs", stats.Blobs),
|
||
)
|
||
}
|
||
if stats, err := filesService.WarmCaches(ctx); err != nil {
|
||
logger.Warn("媒体资源缓存预热失败", zap.Error(err))
|
||
} else if stats.StickerSets > 0 || stats.Documents > 0 || stats.Blobs > 0 {
|
||
logger.Info("媒体资源缓存预热完成",
|
||
zap.Int("sticker_sets", stats.StickerSets),
|
||
zap.Int("documents", stats.Documents),
|
||
zap.Int("blobs", stats.Blobs),
|
||
)
|
||
}
|
||
langPackStore := postgres.NewLangPackStore(pool)
|
||
passwordStore := postgres.NewPasswordStore(pool)
|
||
helpStore := postgres.NewHelpStore(pool)
|
||
tempAuthKeyStore := postgres.NewTempAuthKeyBindingStore(pool)
|
||
sessionStore := redisstore.NewSessionStore(rdb, redisstore.DefaultSessionTTL)
|
||
codeStore := redisstore.NewCodeStore(rdb)
|
||
rateLimiter := redisstore.NewRateLimiter(rdb)
|
||
activeSessions := mtprotoedge.NewSessionManager(logger.Named("mtprotoedge").Named("sessions"))
|
||
go maintenance.NewRetentionWorker(dispatchOutboxStore, logger.Named("maintenance").Named("retention"),
|
||
cfg.UpdateEventRetention,
|
||
cfg.RetentionInterval,
|
||
cfg.RetentionBatch,
|
||
).Run(ctx)
|
||
go rpc.NewOutboxDispatcher(updateEventStore, dispatchOutboxStore, activeSessions, logger.Named("rpc").Named("outbox"),
|
||
rpc.WithOutboxWorkers(cfg.OutboxWorkers),
|
||
rpc.WithOutboxBatch(cfg.OutboxBatch),
|
||
rpc.WithOutboxInterval(cfg.OutboxInterval),
|
||
rpc.WithOutboxPushTimeout(cfg.OutboundPushTimeout),
|
||
).Run(ctx)
|
||
langPackService := langpack.NewService(langPackStore)
|
||
privacyService := privacyapp.NewService(privacyStore, contactStore)
|
||
if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil {
|
||
return fmt.Errorf("seed langpack: %w", err)
|
||
} else if seeded > 0 {
|
||
logger.Info("语言包种子导入完成", zap.String("dir", cfg.LangPackSeedDir), zap.Int("strings", seeded))
|
||
}
|
||
|
||
router := rpc.New(rpc.Config{
|
||
DC: cfg.DC,
|
||
IP: cfg.AdvertiseIP,
|
||
Port: port,
|
||
OutboundPushTimeout: cfg.OutboundPushTimeout,
|
||
}, rpc.Deps{
|
||
Auth: auth.NewService(userStore, authzStore, codeStore, authKeyStore, tempAuthKeyStore, cfg.DevAuthCode, auth.WithLoginMessages(messageStore, dialogStore), auth.WithPasswords(passwordStore)),
|
||
Account: account.NewService(passwordStore, account.WithReactionSettings(passwordStore)),
|
||
Privacy: privacyService,
|
||
Help: help.NewService(helpStore, helpStore),
|
||
Users: users.NewService(userStore, users.WithContactStore(contactStore), users.WithPhotoProvider(mediaStore), users.WithPrivacyEvaluator(privacyService)),
|
||
Updates: updates.NewService(updateStateStore, updateEventStore, updates.WithPtsAllocator(ptsAllocator)),
|
||
Contacts: contacts.NewService(contactStore, userStore).Configure(contacts.WithPhotoProvider(mediaStore), contacts.WithPrivacyEvaluator(privacyService)),
|
||
Dialogs: dialogs.NewService(dialogStore, channelStore).Configure(dialogs.WithContactStore(contactStore), dialogs.WithPhotoProvider(mediaStore), dialogs.WithPrivacyEvaluator(privacyService)),
|
||
Messages: messageapp.NewService(messageStore, dialogStore, messageapp.WithContactStore(contactStore), messageapp.WithPhotoProvider(mediaStore), messageapp.WithPrivacyEvaluator(privacyService)),
|
||
Channels: channelapp.NewService(channelStore),
|
||
Files: filesService,
|
||
LangPack: langPackService,
|
||
Sessions: activeSessions,
|
||
Limiter: rateLimiter,
|
||
}, logger.Named("rpc"), clock.System)
|
||
activeSessions.SetLifecycleObserver(router)
|
||
|
||
srv := mtprotoedge.New(mtprotoedge.Options{
|
||
Logger: logger.Named("mtprotoedge"),
|
||
DC: cfg.DC,
|
||
RSAKey: rsaKey,
|
||
RPC: router,
|
||
AuthKeys: authKeyStore,
|
||
Sessions: sessionStore,
|
||
ActiveSessions: activeSessions,
|
||
ObfuscatedTCP: true,
|
||
})
|
||
return srv.Serve(ctx, ln)
|
||
}
|