package apiserver import ( "context" "errors" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "git.unkin.net/unkin/forgebot/internal/apiserver/handlers" "git.unkin.net/unkin/forgebot/internal/database" "git.unkin.net/unkin/forgebot/internal/provider/gitea" ) type Server struct { cfg *Config router chi.Router db *database.DB provider *gitea.Client } func New(cfg *Config) (*Server, error) { db, err := database.New(cfg.DatabaseDSN()) if err != nil { return nil, err } provider := gitea.NewClient(cfg.GiteaURL, cfg.GiteaToken) s := &Server{ cfg: cfg, db: db, provider: provider, } s.router = s.routes() return s, nil } func (s *Server) routes() chi.Router { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(60 * time.Second)) healthH := handlers.NewHealthHandler(s.db) webhookH := handlers.NewWebhookHandler(s.db, s.provider, s.cfg.WebhookSecret) tasksH := handlers.NewTasksHandler(s.db, s.provider) r.Get("/health", healthH.Health) r.Route("/api/v1", func(r chi.Router) { r.Post("/webhook/gitea", webhookH.HandleGitea) r.Get("/tasks", tasksH.List) r.Post("/tasks", tasksH.Create) r.Get("/tasks/{id}", tasksH.Get) r.Patch("/tasks/{id}", tasksH.UpdateStatus) r.Post("/tasks/{id}/complete", tasksH.Complete) r.Post("/tasks/{id}/comment", tasksH.PostComment) }) return r } func (s *Server) Run(ctx context.Context) error { httpServer := &http.Server{ Addr: s.cfg.ListenAddr, Handler: s.router, ReadTimeout: 30 * time.Second, WriteTimeout: 300 * time.Second, IdleTimeout: 120 * time.Second, } go func() { <-ctx.Done() slog.Info("shutting down server") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = httpServer.Shutdown(shutdownCtx) }() slog.Info("starting server", "addr", s.cfg.ListenAddr) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil }