// Command validator is the home-side Telegram validator. It serves the validation // gRPC API (pkg/proto/telegram/v1 ValidateInitData/ValidateLoginWidget) the gateway // calls during Telegram auth. It holds the bot token only as the HMAC secret and // never reaches the Telegram Bot API, so it runs on the main host with no VPN and // no Telegram egress, and game login stays independent of the remote bot. See // platform/telegram/README.md. package main import ( "context" "errors" "log" "net" "os/signal" "syscall" "time" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.uber.org/zap" "google.golang.org/grpc" telegramv1 "scrabble/pkg/proto/telegram/v1" pkgtel "scrabble/pkg/telemetry" "scrabble/platform/telegram/internal/config" "scrabble/platform/telegram/internal/initdata" "scrabble/platform/telegram/internal/loginwidget" "scrabble/platform/telegram/internal/validator" ) // telemetryShutdownTimeout bounds the OpenTelemetry flush during process exit. const telemetryShutdownTimeout = 5 * time.Second func main() { cfg, err := config.LoadValidator() if err != nil { log.Fatalf("validator: load config: %v", err) } logger, err := newLogger(cfg.LogLevel) if err != nil { log.Fatalf("validator: build logger: %v", err) } defer func() { _ = logger.Sync() }() ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := run(ctx, cfg, logger); err != nil { logger.Fatal("validator: terminated", zap.Error(err)) } } // run wires the validator gRPC server and serves it until the context is cancelled. func run(ctx context.Context, cfg config.ValidatorConfig, logger *zap.Logger) error { tel, err := pkgtel.New(ctx, cfg.Telemetry) if err != nil { return err } defer func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), telemetryShutdownTimeout) defer cancel() if err := tel.Shutdown(shutdownCtx); err != nil { logger.Warn("telemetry shutdown", zap.Error(err)) } }() if err := tel.StartRuntimeMetrics(); err != nil { logger.Warn("telemetry: start runtime metrics", zap.Error(err)) } srv := validator.NewServer(initdata.NewHMACValidator(cfg.Token), loginwidget.NewHMACValidator(cfg.Token)) grpcServer := grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler())) telegramv1.RegisterTelegramServer(grpcServer, srv) lis, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { return err } go func() { <-ctx.Done() grpcServer.GracefulStop() }() logger.Info("telegram validator starting", zap.String("grpc_addr", cfg.GRPCAddr)) if err := grpcServer.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) { return err } return nil } // newLogger builds a production JSON logger at the given level. func newLogger(level string) (*zap.Logger, error) { var lvl zap.AtomicLevel if err := lvl.UnmarshalText([]byte(level)); err != nil { return nil, err } cfg := zap.NewProductionConfig() cfg.Level = lvl return cfg.Build() }