package main import ( "context" "flag" "net/http" "os" "os/signal" "syscall" "time" "github.com/rs/zerolog" "github.com/vincentc-afk/gitea-notification-hub/internal/config" "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" "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea" slacknotifier "github.com/vincentc-afk/gitea-notification-hub/internal/notifier/slack" "github.com/vincentc-afk/gitea-notification-hub/internal/server" "github.com/vincentc-afk/gitea-notification-hub/internal/storage/sqlite" "github.com/vincentc-afk/gitea-notification-hub/internal/webhook" ) func main() { // Parse flags configPath := flag.String("config", "config/config.yaml", "path to config file") debug := flag.Bool("debug", false, "enable debug logging") flag.Parse() // Setup logger logger := setupLogger(*debug) // Load configuration cfg, err := config.Load(*configPath) if err != nil { logger.Fatal().Err(err).Msg("failed to load configuration") } logger.Info().Str("path", *configPath).Msg("configuration loaded") ctx := context.Background() // Initialize storage repo, err := sqlite.New(cfg.Database.DSN) if err != nil { logger.Fatal().Err(err).Msg("failed to initialize database") } defer repo.Close() // Run migrations if err := repo.Migrate(ctx); err != nil { logger.Fatal().Err(err).Msg("failed to run migrations") } logger.Info().Msg("database initialized") // Initialize Gitea API provider for email lookup var emailLookup identity.EmailLookup if cfg.Identity.Gitea.URL != "" { giteaProvider := gitea.New(&cfg.Identity.Gitea, logger) emailLookup = giteaProvider logger.Info().Str("url", cfg.Identity.Gitea.URL).Msg("Gitea identity provider initialized") } else { logger.Warn().Msg("Gitea URL not configured, email lookup disabled") } // Initialize Slack notifier slackClient := slacknotifier.New(&cfg.Notification.Slack, logger) logger.Info().Msg("Slack notifier initialized") // Initialize cached identity resolver resolver := cache.NewCachedResolver(repo, emailLookup, slackClient, logger) logger.Info().Msg("identity resolver initialized") // Create processor adapter that implements webhook.EventHandler processor := event.NewProcessor(cfg, &resolverAdapter{resolver}, slackClient, logger) logger.Info().Msg("event processor initialized") // Create HTTP server srv := server.New(cfg, logger) // Register webhook handler webhookHandler := webhook.NewHandler(cfg.Server.WebhookSecret, processor, logger) srv.Router().Post("/webhook", webhookHandler.ServeHTTP) logger.Info().Msg("webhook handler registered at POST /webhook") // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh logger.Info().Msg("received shutdown signal") shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error().Err(err).Msg("shutdown error") } }() // Start server logger.Info().Int("port", cfg.Server.Port).Msg("starting server") if err := srv.Start(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("server error") } } func setupLogger(debug bool) zerolog.Logger { zerolog.TimeFieldFormat = time.RFC3339 level := zerolog.InfoLevel if debug { level = zerolog.DebugLevel } return zerolog.New(zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: "15:04:05", }).Level(level).With().Timestamp().Caller().Logger() } // resolverAdapter adapts cache.CachedResolver to event.IdentityResolver type resolverAdapter struct { resolver *cache.CachedResolver } func (a *resolverAdapter) Resolve(ctx context.Context, user event.User) (*event.ResolvedIdentity, error) { identity, err := a.resolver.Resolve(ctx, user) if err != nil { return nil, err } return &event.ResolvedIdentity{ Email: identity.Email, SlackID: identity.SlackID, SlackName: identity.SlackName, }, nil }