diff --git a/cmd/server/main.go b/cmd/server/main.go index 4a6a23a..0539aba 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,145 +1 @@ -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 with optional manual mappings - manualMappings := cache.ParseManualMappings(os.Getenv("MANUAL_USER_MAPPINGS")) - if len(manualMappings) > 0 { - logger.Info().Int("count", len(manualMappings)).Msg("manual user mappings configured") - for username := range manualMappings { - logger.Debug().Str("username", username).Msg("manual mapping registered") - } - } - resolver := cache.NewCachedResolver(repo, emailLookup, slackClient, manualMappings, 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 -} +cGFja2FnZSBtYWluCgppbXBvcnQgKAoJImNvbnRleHQiCgkiZmxhZyIKCSJuZXQvaHR0cCIKCSJvcyIKCSJvcy9zaWduYWwiCgkic3RyaW5ncyIKCSJzeXNjYWxsIgoJInRpbWUiCgoJImdpdGh1Yi5jb20vcnMvemVyb2xvZyIKCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9jb25maWciCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9ldmVudCIKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL2lkZW50aXR5IgoJImdpdGh1Yi5jb20vdmluY2VudGMtYWZrL2dpdGVhLW5vdGlmaWNhdGlvbi1odWIvaW50ZXJuYWwvaWRlbnRpdHkvY2FjaGUiCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9pZGVudGl0eS9naXRlYSIKCXNsYWNrbm90aWZpZXIgImdpdGh1Yi5jb20vdmluY2VudGMtYWZrL2dpdGVhLW5vdGlmaWNhdGlvbi1odWIvaW50ZXJuYWwvbm90aWZpZXIvc2xhY2siCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9zZXJ2ZXIiCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9zdG9yYWdlL3NxbGl0ZSIKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL3dlYmhvb2siCikKCmZ1bmMgbWFpbigpIHsKCS8vIFBhcnNlIGZsYWdzCgljb25maWdQYXRoIDo9IGZsYWcuU3RyaW5nKCJjb25maWciLCAiY29uZmlnL2NvbmZpZy55YW1sIiwgInBhdGggdG8gY29uZmlnIGZpbGUiKQoJZGVidWcgOj0gZmxhZy5Cb29sKCJkZWJ1ZyIsIGZhbHNlLCAiZW5hYmxlIGRlYnVnIGxvZ2dpbmciKQoJZmxhZy5QYXJzZSgpCgoJLy8gU2V0dXAgbG9nZ2VyCglsb2dnZXIgOj0gc2V0dXBMb2dnZXIoKmRlYnVnKQoKCS8vIExvYWQgY29uZmlndXJhdGlvbgoJY2ZnLCBlcnIgOj0gY29uZmlnLkxvYWQoKmNvbmZpZ1BhdGgpCglpZiBlcnIgIT0gbmlsIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coImZhaWxlZCB0byBsb2FkIGNvbmZpZ3VyYXRpb24iKQoJfQoJbG9nZ2VyLkluZm8oKS5TdHIoInBhdGgiLCAqY29uZmlnUGF0aCkuTXNnKCJjb25maWd1cmF0aW9uIGxvYWRlZCIpCgoJY3R4IDo9IGNvbnRleHQuQmFja2dyb3VuZCgpCgoJLy8gSW5pdGlhbGl6ZSBzdG9yYWdlCglyZXBvLCBlcnIgOj0gc3FsaXRlLk5ldyhjZmcuRGF0YWJhc2UuRFNOKQoJaWYgZXJyICE9IG5pbCB7CgkJbG9nZ2VyLkZhdGFsKCkuRXJyKGVycikuTXNnKCJmYWlsZWQgdG8gaW5pdGlhbGl6ZSBkYXRhYmFzZSIpCgl9CglkZWZlciByZXBvLkNsb3NlKCkKCgkvLyBSdW4gbWlncmF0aW9ucwoJaWYgZXJyIDo9IHJlcG8uTWlncmF0ZShjdHgpOyBlcnIgIT0gbmlsIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coImZhaWxlZCB0byBydW4gbWlncmF0aW9ucyIpCgl9Cglsb2dnZXIuSW5mbygpLk1zZygiZGF0YWJhc2UgaW5pdGlhbGl6ZWQiKQoKCS8vIEluaXRpYWxpemUgR2l0ZWEgQVBJIHByb3ZpZGVyIGZvciBlbWFpbCBsb29rdXAKCXZhciBlbWFpbExvb2t1cCBpZGVudGl0eS5FbWFpbExvb2t1cAoJaWYgY2ZnLklkZW50aXR5LkdpdGVhLlVSTCAhPSAiIiB7CgkJZ2l0ZWFQcm92aWRlciA6PSBnaXRlYS5OZXcoJmNmZy5JZGVudGl0eS5HaXRlYSwgbG9nZ2VyKQoJCWVtYWlsTG9va3VwID0gZ2l0ZWFQcm92aWRlcgoJCWxvZ2dlci5JbmZvKCkuU3RyKCJ1cmwiLCBjZmcuSWRlbnRpdHkuR2l0ZWEuVVJMKS5Nc2coIkdpdGVhIGlkZW50aXR5IHByb3ZpZGVyIGluaXRpYWxpemVkIikKCX0gZWxzZSB7CgkJbG9nZ2VyLldhcm4oKS5Nc2coIkdpdGVhIFVSTCBub3QgY29uZmlndXJlZCwgZW1haWwgbG9va3VwIGRpc2FibGVkIikKCX0KCgkvLyBJbml0aWFsaXplIFNsYWNrIG5vdGlmaWVyCglzbGFja0NsaWVudCA6PSBzbGFja25vdGlmaWVyLk5ldygmY2ZnLk5vdGlmaWNhdGlvbi5TbGFjaywgbG9nZ2VyKQoJbG9nZ2VyLkluZm8oKS5Nc2coIlNsYWNrIG5vdGlmaWVyIGluaXRpYWxpemVkIikKCgkvLyBJbml0aWFsaXplIGNhY2hlZCBpZGVudGl0eSByZXNvbHZlciB3aXRoIG9wdGlvbmFsIG1hbnVhbCBtYXBwaW5ncwoJbWFudWFsTWFwcGluZ3MgOj0gY2FjaGUuUGFyc2VNYW51YWxNYXBwaW5ncyhvcy5HZXRlbnYoIk1BTlVBTF9VU0VSX01BUFBJTkdTIikpCglpZiBsZW4obWFudWFsTWFwcGluZ3MpID4gMCB7CgkJbG9nZ2VyLkluZm8oKS5JbnQoImNvdW50IiwgbGVuKG1hbnVhbE1hcHBpbmdzKSkuTXNnKCJtYW51YWwgdXNlciBtYXBwaW5ncyBjb25maWd1cmVkIikKCQlmb3IgdXNlcm5hbWUgOj0gcmFuZ2UgbWFudWFsTWFwcGluZ3MgewoJCQlsb2dnZXIuRGVidWcoKS5TdHIoInVzZXJuYW1lIiwgdXNlcm5hbWUpLk1zZygibWFudWFsIG1hcHBpbmcgcmVnaXN0ZXJlZCIpCgkJfQoJfQoJcmVzb2x2ZXIgOj0gY2FjaGUuTmV3Q2FjaGVkUmVzb2x2ZXIocmVwbywgZW1haWxMb29rdXAsIHNsYWNrQ2xpZW50LCBtYW51YWxNYXBwaW5ncywgbG9nZ2VyKQoJbG9nZ2VyLkluZm8oKS5Nc2coImlkZW50aXR5IHJlc29sdmVyIGluaXRpYWxpemVkIikKCgkvLyBDcmVhdGUgcHJvY2Vzc29yIGFkYXB0ZXIgdGhhdCBpbXBsZW1lbnRzIHdlYmhvb2suRXZlbnRIYW5kbGVyCglwcm9jZXNzb3IgOj0gZXZlbnQuTmV3UHJvY2Vzc29yKGNmZywgJnJlc29sdmVyQWRhcHRlcntyZXNvbHZlcn0sIHNsYWNrQ2xpZW50LCBsb2dnZXIpCglsb2dnZXIuSW5mbygpLk1zZygiZXZlbnQgcHJvY2Vzc29yIGluaXRpYWxpemVkIikKCgkvLyBDcmVhdGUgSFRUUCBzZXJ2ZXIKCXNydiA6PSBzZXJ2ZXIuTmV3KGNmZywgbG9nZ2VyKQoKCS8vIFJlZ2lzdGVyIHdlYmhvb2sgaGFuZGxlcgoJd2ViaG9va0hhbmRsZXIgOj0gd2ViaG9vay5OZXdIYW5kbGVyKGNmZy5TZXJ2ZXIuV2ViaG9va1NlY3JldCwgcHJvY2Vzc29yLCBsb2dnZXIpCglzcnYuUm91dGVyKCkuUG9zdCgiL3dlYmhvb2siLCB3ZWJob29rSGFuZGxlci5TZXJ2ZUhUVFApCglsb2dnZXIuSW5mbygpLk1zZygid2ViaG9vayBoYW5kbGVyIHJlZ2lzdGVyZWQgYXQgUE9TVCAvd2ViaG9vayIpCgoJLy8gR3JhY2VmdWwgc2h1dGRvd24KCWdvIGZ1bmMoKSB7CgkJc2lnQ2ggOj0gbWFrZShjaGFuIG9zLlNpZ25hbCwgMSkKCQlzaWduYWwuTm90aWZ5KHNpZ0NoLCBzeXNjYWxsLlNJR0lOVCwgc3lzY2FsbC5TSUdURVJNKQoJCTwtc2lnQ2gKCgkJbG9nZ2VyLkluZm8oKS5Nc2coInJlY2VpdmVkIHNodXRkb3duIHNpZ25hbCIpCgoJCXNodXRkb3duQ3R4LCBjYW5jZWwgOj0gY29udGV4dC5XaXRoVGltZW91dChjb250ZXh0LkJhY2tncm91bmQoKSwgMzAqdGltZS5TZWNvbmQpCgkJZGVmZXIgY2FuY2VsKCkKCgkJaWYgZXJyIDo9IHNydi5TaHV0ZG93bihzaHV0ZG93bkN0eCk7IGVyciAhPSBuaWwgewoJCQlsb2dnZXIuRXJyb3IoKS5FcnIoZXJyKS5Nc2coInNodXRkb3duIGVycm9yIikKCQl9Cgl9KCkKCgkvLyBTdGFydCBzZXJ2ZXIKCWxvZ2dlci5JbmZvKCkuSW50KCJwb3J0IiwgY2ZnLlNlcnZlci5Qb3J0KS5Nc2coInN0YXJ0aW5nIHNlcnZlciIpCglpZiBlcnIgOj0gc3J2LlN0YXJ0KCk7IGVyciAhPSBuaWwgJiYgZXJyICE9IGh0dHAuRXJyU2VydmVyQ2xvc2VkIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coInNlcnZlciBlcnJvciIpCgl9Cn0KCmZ1bmMgc2V0dXBMb2dnZXIoZGVidWcgYm9vbCkgemVyb2xvZy5Mb2dnZXIgewoJemVyb2xvZy5UaW1lRmllbGRGb3JtYXQgPSB0aW1lLlJGQzMzMzkKCglsZXZlbCA6PSB6ZXJvbG9nLkluZm9MZXZlbAoJaWYgZGVidWcgewoJCWxldmVsID0gemVyb2xvZy5EZWJ1Z0xldmVsCgl9CgoJLy8gQWxsb3cgTE9HX0xFVkVMIGVudiB2YXIgdG8gb3ZlcnJpZGUKCWlmIGxvZ0xldmVsIDo9IG9zLkdldGVudigiTE9HX0xFVkVMIik7IGxvZ0xldmVsICE9ICIiIHsKCQlzd2l0Y2ggc3RyaW5ncy5Ub0xvd2VyKGxvZ0xldmVsKSB7CgkJY2FzZSAiZGVidWciOgoJCQlsZXZlbCA9IHplcm9sb2cuRGVidWdMZXZlbAoJCWNhc2UgImluZm8iOgoJCQlsZXZlbCA9IHplcm9sb2cuSW5mb0xldmVsCgkJY2FzZSAid2FybiI6CgkJCWxldmVsID0gemVyb2xvZy5XYXJuTGV2ZWwKCQljYXNlICJlcnJvciI6CgkJCWxldmVsID0gemVyb2xvZy5FcnJvckxldmVsCgkJfQoJfQoKCXJldHVybiB6ZXJvbG9nLk5ldyh6ZXJvbG9nLkNvbnNvbGVXcml0ZXJ7CgkJT3V0OiAgICAgICAgb3MuU3Rkb3V0LAoJCVRpbWVGb3JtYXQ6ICIxNTowNDowNSIsCgl9KS5MZXZlbChsZXZlbCkuV2l0aCgpLlRpbWVzdGFtcCgpLkNhbGxlcigpLkxvZ2dlcigpCn0KCi8vIHJlc29sdmVyQWRhcHRlciBhZGFwdHMgY2FjaGUuQ2FjaGVkUmVzb2x2ZXIgdG8gZXZlbnQuSWRlbnRpdHlSZXNvbHZlcgp0eXBlIHJlc29sdmVyQWRhcHRlciBzdHJ1Y3QgewoJcmVzb2x2ZXIgKmNhY2hlLkNhY2hlZFJlc29sdmVyCn0KCmZ1bmMgKGEgKnJlc29sdmVyQWRhcHRlcikgUmVzb2x2ZShjdHggY29udGV4dC5Db250ZXh0LCB1c2VyIGV2ZW50LlVzZXIpICgqZXZlbnQuUmVzb2x2ZWRJZGVudGl0eSwgZXJyb3IpIHsKCWlkZW50aXR5LCBlcnIgOj0gYS5yZXNvbHZlci5SZXNvbHZlKGN0eCwgdXNlcikKCWlmIGVyciAhPSBuaWwgewoJCXJldHVybiBuaWwsIGVycgoJfQoJcmV0dXJuICZldmVudC5SZXNvbHZlZElkZW50aXR5ewoJCUVtYWlsOiAgICAgaWRlbnRpdHkuRW1haWwsCgkJU2xhY2tJRDogICBpZGVudGl0eS5TbGFja0lELAoJCVNsYWNrTmFtZTogaWRlbnRpdHkuU2xhY2tOYW1lLAoJfSwgbmlsCn0K \ No newline at end of file