package main import ( "context" "os" "os/signal" "strings" "syscall" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/vincentc-afk/gitea-notification-hub/internal/event" "github.com/vincentc-afk/gitea-notification-hub/internal/identity" "github.com/vincentc-afk/gitea-notification-hub/internal/identity/cache" identitygitea "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea" identityslack "github.com/vincentc-afk/gitea-notification-hub/internal/identity/slack" "github.com/vincentc-afk/gitea-notification-hub/internal/slack" "github.com/vincentc-afk/gitea-notification-hub/internal/storage" "github.com/vincentc-afk/gitea-notification-hub/internal/storage/sqlite" "github.com/vincentc-afk/gitea-notification-hub/internal/webhook" ) func main() { app := &cli.App{ Name: "gitea-notification-hub", Usage: "Gitea webhook event processor that routes notifications to Slack", Flags: []cli.Flag{ &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Path to config file", Value: "config.yaml", }, &cli.BoolFlag{ Name: "debug", Aliases: []string{"d"}, Usage: "Enable debug logging", Value: false, }, &cli.IntFlag{ Name: "port", Usage: "HTTP server port", Value: 8080, }, }, Action: run, } if err := app.Run(os.Args); err != nil { // Use default logger since setup might have failed fallback := zerolog.New(os.Stderr).With().Timestamp().Logger() fallback.Fatal().Err(err).Msg("application failed to start") } } func run(ctx *cli.Context) error { // --- Logger setup --- logLevel := ctx.String("log-level") if logLevel == "" { // Check LOG_LEVEL env var (overrides --debug flag) logLevel = os.Getenv("LOG_LEVEL") } if logLevel == "" && ctx.Bool("debug") { logLevel = "debug" } if logLevel == "" { logLevel = "info" } level, err := zerolog.ParseLevel(strings.ToLower(logLevel)) if err != nil { level = zerolog.InfoLevel } logger := zerolog.New(os.Stdout). With(). Timestamp(). Logger(). Level(level) logger.Info().Str("level", level.String()).Msg("logger initialized") // --- Create storage --- repo, err := sqlite.NewSQLiteRepository("data/notifications.db", logger) if err != nil { logger.Fatal().Err(err).Msg("failed to initialize storage") } defer repo.Close() // --- Create identity resolver --- giteaEmailLookup := identitygitea.NewEmailLookup(logger) slackLookup := identityslack.NewLookup(logger) // Parse manual user mappings manualMappingsRaw := os.Getenv("MANUAL_USER_MAPPINGS") manualMappings := cache.ParseManualMappings(manualMappingsRaw) if len(manualMappings) > 0 { logger.Info().Int("count", len(manualMappings)).Msg("loaded manual user mappings") } for giteaUser, slackID := range manualMappings { logger.Debug(). Str("gitea_user", giteaUser). Str("slack_id", slackID). Msg("manual mapping entry") } resolver := cache.NewCachedResolver(repo, giteaEmailLookup, slackLookup, manualMappings, logger) // --- Create Slack client --- slackClient, err := slack.NewClient(logger) if err != nil { logger.Fatal().Err(err).Msg("failed to create Slack client") } // --- Create event processor --- processor := event.NewProcessor(resolver, slackClient, logger) // --- Create webhook handler --- handler := webhook.NewHandler(processor, logger) // --- Setup HTTP routes --- mux := setupRouter(handler, logger) // --- Start HTTP server --- port := ctx.Int("port") srv := startServer(mux, port, logger) // --- Wait for shutdown signal --- sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh logger.Info().Str("signal", sig.String()).Msg("received shutdown signal") // --- Graceful shutdown --- shutdownCtx, cancel := context.WithTimeout(context.Background(), 10_000) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error().Err(err).Msg("server shutdown error") } logger.Info().Msg("server stopped") return nil } // setupRouter configures the HTTP router with all routes func setupRouter(handler *webhook.Handler, logger zerolog.Logger) *webhook.Router { router := webhook.NewRouter(logger) // Health check router.Get("/health", handler.HealthCheck) // Webhook endpoint router.Post("/webhook", handler.HandleWebhook) return router } // startServer starts the HTTP server on the given port func startServer(router *webhook.Router, port int, logger zerolog.Logger) *webhook.Server { srv := webhook.NewServer(router, port, logger) go func() { logger.Info().Int("port", port).Msg("starting HTTP server") if err := srv.Start(); err != nil { logger.Fatal().Err(err).Msg("server error") } }() return srv }