From d33b1abd7ee25effd7e7b9521bb4fd6f27756188 Mon Sep 17 00:00:00 2001 From: Dimitris Karvounis <1+jimkarvo@jksoftware.gr> Date: Mon, 29 Jun 2026 20:16:40 +0000 Subject: [PATCH] fix: rewrite main.go with raw Go source (was base64-corrupted) --- cmd/server/main.go | 172 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 0539aba..33c40f3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1 +1,171 @@ -cGFja2FnZSBtYWluCgppbXBvcnQgKAoJImNvbnRleHQiCgkiZmxhZyIKCSJuZXQvaHR0cCIKCSJvcyIKCSJvcy9zaWduYWwiCgkic3RyaW5ncyIKCSJzeXNjYWxsIgoJInRpbWUiCgoJImdpdGh1Yi5jb20vcnMvemVyb2xvZyIKCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9jb25maWciCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9ldmVudCIKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL2lkZW50aXR5IgoJImdpdGh1Yi5jb20vdmluY2VudGMtYWZrL2dpdGVhLW5vdGlmaWNhdGlvbi1odWIvaW50ZXJuYWwvaWRlbnRpdHkvY2FjaGUiCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9pZGVudGl0eS9naXRlYSIKCXNsYWNrbm90aWZpZXIgImdpdGh1Yi5jb20vdmluY2VudGMtYWZrL2dpdGVhLW5vdGlmaWNhdGlvbi1odWIvaW50ZXJuYWwvbm90aWZpZXIvc2xhY2siCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9zZXJ2ZXIiCgkiZ2l0aHViLmNvbS92aW5jZW50Yy1hZmsvZ2l0ZWEtbm90aWZpY2F0aW9uLWh1Yi9pbnRlcm5hbC9zdG9yYWdlL3NxbGl0ZSIKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL3dlYmhvb2siCikKCmZ1bmMgbWFpbigpIHsKCS8vIFBhcnNlIGZsYWdzCgljb25maWdQYXRoIDo9IGZsYWcuU3RyaW5nKCJjb25maWciLCAiY29uZmlnL2NvbmZpZy55YW1sIiwgInBhdGggdG8gY29uZmlnIGZpbGUiKQoJZGVidWcgOj0gZmxhZy5Cb29sKCJkZWJ1ZyIsIGZhbHNlLCAiZW5hYmxlIGRlYnVnIGxvZ2dpbmciKQoJZmxhZy5QYXJzZSgpCgoJLy8gU2V0dXAgbG9nZ2VyCglsb2dnZXIgOj0gc2V0dXBMb2dnZXIoKmRlYnVnKQoKCS8vIExvYWQgY29uZmlndXJhdGlvbgoJY2ZnLCBlcnIgOj0gY29uZmlnLkxvYWQoKmNvbmZpZ1BhdGgpCglpZiBlcnIgIT0gbmlsIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coImZhaWxlZCB0byBsb2FkIGNvbmZpZ3VyYXRpb24iKQoJfQoJbG9nZ2VyLkluZm8oKS5TdHIoInBhdGgiLCAqY29uZmlnUGF0aCkuTXNnKCJjb25maWd1cmF0aW9uIGxvYWRlZCIpCgoJY3R4IDo9IGNvbnRleHQuQmFja2dyb3VuZCgpCgoJLy8gSW5pdGlhbGl6ZSBzdG9yYWdlCglyZXBvLCBlcnIgOj0gc3FsaXRlLk5ldyhjZmcuRGF0YWJhc2UuRFNOKQoJaWYgZXJyICE9IG5pbCB7CgkJbG9nZ2VyLkZhdGFsKCkuRXJyKGVycikuTXNnKCJmYWlsZWQgdG8gaW5pdGlhbGl6ZSBkYXRhYmFzZSIpCgl9CglkZWZlciByZXBvLkNsb3NlKCkKCgkvLyBSdW4gbWlncmF0aW9ucwoJaWYgZXJyIDo9IHJlcG8uTWlncmF0ZShjdHgpOyBlcnIgIT0gbmlsIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coImZhaWxlZCB0byBydW4gbWlncmF0aW9ucyIpCgl9Cglsb2dnZXIuSW5mbygpLk1zZygiZGF0YWJhc2UgaW5pdGlhbGl6ZWQiKQoKCS8vIEluaXRpYWxpemUgR2l0ZWEgQVBJIHByb3ZpZGVyIGZvciBlbWFpbCBsb29rdXAKCXZhciBlbWFpbExvb2t1cCBpZGVudGl0eS5FbWFpbExvb2t1cAoJaWYgY2ZnLklkZW50aXR5LkdpdGVhLlVSTCAhPSAiIiB7CgkJZ2l0ZWFQcm92aWRlciA6PSBnaXRlYS5OZXcoJmNmZy5JZGVudGl0eS5HaXRlYSwgbG9nZ2VyKQoJCWVtYWlsTG9va3VwID0gZ2l0ZWFQcm92aWRlcgoJCWxvZ2dlci5JbmZvKCkuU3RyKCJ1cmwiLCBjZmcuSWRlbnRpdHkuR2l0ZWEuVVJMKS5Nc2coIkdpdGVhIGlkZW50aXR5IHByb3ZpZGVyIGluaXRpYWxpemVkIikKCX0gZWxzZSB7CgkJbG9nZ2VyLldhcm4oKS5Nc2coIkdpdGVhIFVSTCBub3QgY29uZmlndXJlZCwgZW1haWwgbG9va3VwIGRpc2FibGVkIikKCX0KCgkvLyBJbml0aWFsaXplIFNsYWNrIG5vdGlmaWVyCglzbGFja0NsaWVudCA6PSBzbGFja25vdGlmaWVyLk5ldygmY2ZnLk5vdGlmaWNhdGlvbi5TbGFjaywgbG9nZ2VyKQoJbG9nZ2VyLkluZm8oKS5Nc2coIlNsYWNrIG5vdGlmaWVyIGluaXRpYWxpemVkIikKCgkvLyBJbml0aWFsaXplIGNhY2hlZCBpZGVudGl0eSByZXNvbHZlciB3aXRoIG9wdGlvbmFsIG1hbnVhbCBtYXBwaW5ncwoJbWFudWFsTWFwcGluZ3MgOj0gY2FjaGUuUGFyc2VNYW51YWxNYXBwaW5ncyhvcy5HZXRlbnYoIk1BTlVBTF9VU0VSX01BUFBJTkdTIikpCglpZiBsZW4obWFudWFsTWFwcGluZ3MpID4gMCB7CgkJbG9nZ2VyLkluZm8oKS5JbnQoImNvdW50IiwgbGVuKG1hbnVhbE1hcHBpbmdzKSkuTXNnKCJtYW51YWwgdXNlciBtYXBwaW5ncyBjb25maWd1cmVkIikKCQlmb3IgdXNlcm5hbWUgOj0gcmFuZ2UgbWFudWFsTWFwcGluZ3MgewoJCQlsb2dnZXIuRGVidWcoKS5TdHIoInVzZXJuYW1lIiwgdXNlcm5hbWUpLk1zZygibWFudWFsIG1hcHBpbmcgcmVnaXN0ZXJlZCIpCgkJfQoJfQoJcmVzb2x2ZXIgOj0gY2FjaGUuTmV3Q2FjaGVkUmVzb2x2ZXIocmVwbywgZW1haWxMb29rdXAsIHNsYWNrQ2xpZW50LCBtYW51YWxNYXBwaW5ncywgbG9nZ2VyKQoJbG9nZ2VyLkluZm8oKS5Nc2coImlkZW50aXR5IHJlc29sdmVyIGluaXRpYWxpemVkIikKCgkvLyBDcmVhdGUgcHJvY2Vzc29yIGFkYXB0ZXIgdGhhdCBpbXBsZW1lbnRzIHdlYmhvb2suRXZlbnRIYW5kbGVyCglwcm9jZXNzb3IgOj0gZXZlbnQuTmV3UHJvY2Vzc29yKGNmZywgJnJlc29sdmVyQWRhcHRlcntyZXNvbHZlcn0sIHNsYWNrQ2xpZW50LCBsb2dnZXIpCglsb2dnZXIuSW5mbygpLk1zZygiZXZlbnQgcHJvY2Vzc29yIGluaXRpYWxpemVkIikKCgkvLyBDcmVhdGUgSFRUUCBzZXJ2ZXIKCXNydiA6PSBzZXJ2ZXIuTmV3KGNmZywgbG9nZ2VyKQoKCS8vIFJlZ2lzdGVyIHdlYmhvb2sgaGFuZGxlcgoJd2ViaG9va0hhbmRsZXIgOj0gd2ViaG9vay5OZXdIYW5kbGVyKGNmZy5TZXJ2ZXIuV2ViaG9va1NlY3JldCwgcHJvY2Vzc29yLCBsb2dnZXIpCglzcnYuUm91dGVyKCkuUG9zdCgiL3dlYmhvb2siLCB3ZWJob29rSGFuZGxlci5TZXJ2ZUhUVFApCglsb2dnZXIuSW5mbygpLk1zZygid2ViaG9vayBoYW5kbGVyIHJlZ2lzdGVyZWQgYXQgUE9TVCAvd2ViaG9vayIpCgoJLy8gR3JhY2VmdWwgc2h1dGRvd24KCWdvIGZ1bmMoKSB7CgkJc2lnQ2ggOj0gbWFrZShjaGFuIG9zLlNpZ25hbCwgMSkKCQlzaWduYWwuTm90aWZ5KHNpZ0NoLCBzeXNjYWxsLlNJR0lOVCwgc3lzY2FsbC5TSUdURVJNKQoJCTwtc2lnQ2gKCgkJbG9nZ2VyLkluZm8oKS5Nc2coInJlY2VpdmVkIHNodXRkb3duIHNpZ25hbCIpCgoJCXNodXRkb3duQ3R4LCBjYW5jZWwgOj0gY29udGV4dC5XaXRoVGltZW91dChjb250ZXh0LkJhY2tncm91bmQoKSwgMzAqdGltZS5TZWNvbmQpCgkJZGVmZXIgY2FuY2VsKCkKCgkJaWYgZXJyIDo9IHNydi5TaHV0ZG93bihzaHV0ZG93bkN0eCk7IGVyciAhPSBuaWwgewoJCQlsb2dnZXIuRXJyb3IoKS5FcnIoZXJyKS5Nc2coInNodXRkb3duIGVycm9yIikKCQl9Cgl9KCkKCgkvLyBTdGFydCBzZXJ2ZXIKCWxvZ2dlci5JbmZvKCkuSW50KCJwb3J0IiwgY2ZnLlNlcnZlci5Qb3J0KS5Nc2coInN0YXJ0aW5nIHNlcnZlciIpCglpZiBlcnIgOj0gc3J2LlN0YXJ0KCk7IGVyciAhPSBuaWwgJiYgZXJyICE9IGh0dHAuRXJyU2VydmVyQ2xvc2VkIHsKCQlsb2dnZXIuRmF0YWwoKS5FcnIoZXJyKS5Nc2coInNlcnZlciBlcnJvciIpCgl9Cn0KCmZ1bmMgc2V0dXBMb2dnZXIoZGVidWcgYm9vbCkgemVyb2xvZy5Mb2dnZXIgewoJemVyb2xvZy5UaW1lRmllbGRGb3JtYXQgPSB0aW1lLlJGQzMzMzkKCglsZXZlbCA6PSB6ZXJvbG9nLkluZm9MZXZlbAoJaWYgZGVidWcgewoJCWxldmVsID0gemVyb2xvZy5EZWJ1Z0xldmVsCgl9CgoJLy8gQWxsb3cgTE9HX0xFVkVMIGVudiB2YXIgdG8gb3ZlcnJpZGUKCWlmIGxvZ0xldmVsIDo9IG9zLkdldGVudigiTE9HX0xFVkVMIik7IGxvZ0xldmVsICE9ICIiIHsKCQlzd2l0Y2ggc3RyaW5ncy5Ub0xvd2VyKGxvZ0xldmVsKSB7CgkJY2FzZSAiZGVidWciOgoJCQlsZXZlbCA9IHplcm9sb2cuRGVidWdMZXZlbAoJCWNhc2UgImluZm8iOgoJCQlsZXZlbCA9IHplcm9sb2cuSW5mb0xldmVsCgkJY2FzZSAid2FybiI6CgkJCWxldmVsID0gemVyb2xvZy5XYXJuTGV2ZWwKCQljYXNlICJlcnJvciI6CgkJCWxldmVsID0gemVyb2xvZy5FcnJvckxldmVsCgkJfQoJfQoKCXJldHVybiB6ZXJvbG9nLk5ldyh6ZXJvbG9nLkNvbnNvbGVXcml0ZXJ7CgkJT3V0OiAgICAgICAgb3MuU3Rkb3V0LAoJCVRpbWVGb3JtYXQ6ICIxNTowNDowNSIsCgl9KS5MZXZlbChsZXZlbCkuV2l0aCgpLlRpbWVzdGFtcCgpLkNhbGxlcigpLkxvZ2dlcigpCn0KCi8vIHJlc29sdmVyQWRhcHRlciBhZGFwdHMgY2FjaGUuQ2FjaGVkUmVzb2x2ZXIgdG8gZXZlbnQuSWRlbnRpdHlSZXNvbHZlcgp0eXBlIHJlc29sdmVyQWRhcHRlciBzdHJ1Y3QgewoJcmVzb2x2ZXIgKmNhY2hlLkNhY2hlZFJlc29sdmVyCn0KCmZ1bmMgKGEgKnJlc29sdmVyQWRhcHRlcikgUmVzb2x2ZShjdHggY29udGV4dC5Db250ZXh0LCB1c2VyIGV2ZW50LlVzZXIpICgqZXZlbnQuUmVzb2x2ZWRJZGVudGl0eSwgZXJyb3IpIHsKCWlkZW50aXR5LCBlcnIgOj0gYS5yZXNvbHZlci5SZXNvbHZlKGN0eCwgdXNlcikKCWlmIGVyciAhPSBuaWwgewoJCXJldHVybiBuaWwsIGVycgoJfQoJcmV0dXJuICZldmVudC5SZXNvbHZlZElkZW50aXR5ewoJCUVtYWlsOiAgICAgaWRlbnRpdHkuRW1haWwsCgkJU2xhY2tJRDogICBpZGVudGl0eS5TbGFja0lELAoJCVNsYWNrTmFtZTogaWRlbnRpdHkuU2xhY2tOYW1lLAoJfSwgbmlsCn0K \ No newline at end of file +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 +}