Last active
March 1, 2025 00:57
-
-
Save jippi/32eb0384fd1a8caccda75d1901ae562b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package server | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"errors" | |
"fmt" | |
"log/slog" | |
"net/http" | |
"os" | |
"os/signal" | |
"sync" | |
"syscall" | |
"time" | |
goversion "github.com/caarlos0/go-version" | |
"github.com/gofiber/fiber/v2" | |
"github.com/gofiber/fiber/v2/middleware/compress" | |
"github.com/gofiber/fiber/v2/middleware/cors" | |
"github.com/gofiber/fiber/v2/middleware/etag" | |
"github.com/gofiber/fiber/v2/middleware/filesystem" | |
"github.com/gofiber/fiber/v2/middleware/monitor" | |
"github.com/gofiber/fiber/v2/middleware/recover" | |
"github.com/gofiber/fiber/v2/middleware/requestid" | |
"github.com/jackc/pgx/v5/pgconn" | |
"github.com/jippi/mastodon-mod-tools/pkg/db" | |
"github.com/jippi/mastodon-mod-tools/pkg/db/schema" | |
"github.com/jippi/mastodon-mod-tools/pkg/tasks" | |
slogfiber "github.com/samber/slog-fiber" | |
"github.com/spf13/cobra" | |
slogctx "github.com/veqryn/slog-context" | |
) | |
var scheduler *tasks.Runner | |
func New() *cobra.Command { | |
cmd := &cobra.Command{ | |
Use: "server", | |
RunE: RunE, | |
Version: buildVersion().String(), | |
} | |
return cmd | |
} | |
func RunE(cmd *cobra.Command, _ []string) error { | |
baseURL := os.Getenv("SERVER_BASE_URL") | |
if len(baseURL) == 0 { | |
return errors.New("Missing SERVER_BASE_URL") | |
} | |
ctx, stop := context.WithCancel(cmd.Context()) | |
logger := slogctx.FromCtx(ctx) | |
logger.Info("Starting server") | |
// | |
// Shutdown handling | |
// | |
var serverShutdown sync.WaitGroup | |
stopChan := make(chan os.Signal, 1) | |
signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) | |
go func() { | |
<-stopChan | |
fmt.Fprintln(cmd.ErrOrStderr()) | |
logger.Warn("Rescieved interrupt, shutting down") | |
stop() | |
}() | |
// | |
// Database | |
// | |
dbConn, err := db.Connect(ctx) | |
if err != nil { | |
return err | |
} | |
if err := db.Migrate(ctx, dbConn); err != nil { | |
return fmt.Errorf("DB migrations failed: %w", err) | |
} | |
schema.SetDefault(dbConn) | |
// | |
// Scheduler | |
// | |
scheduler, err = tasks.NewRunner(ctx, dbConn) | |
if err != nil { | |
return err | |
} | |
// Shutdown Scheduler gracefully | |
serverShutdown.Add(1) | |
go func() { | |
<-ctx.Done() | |
defer serverShutdown.Done() | |
logger.Warn("Stopping Scheduler") | |
if err := scheduler.Shutdown(); err != nil { | |
logger.Error("Failed to stop Scheduler", "err", err) | |
return | |
} | |
logger.Info("Scheduler stopped") | |
}() | |
// | |
// HTTP Server (Go Fiber) | |
// | |
server := fiber.New(fiber.Config{ | |
ReadTimeout: 5 * time.Minute, | |
WriteTimeout: 5 * time.Minute, | |
IdleTimeout: 5 * time.Minute, | |
JSONEncoder: jsonEncoder, | |
ErrorHandler: errorHandler, | |
}) | |
// | |
// HTTP Middleware | |
// | |
server.Use(recover.New()) | |
server.Use(etag.New()) | |
server.Use(compress.New()) | |
server.Use(cors.New(cors.Config{ExposeHeaders: "*"})) | |
server.Use(requestid.New()) | |
server.Use(slogfiber.NewWithConfig( | |
logger.WithGroup("http"), | |
slogfiber.Config{ | |
DefaultLevel: slog.LevelInfo, | |
ClientErrorLevel: slog.LevelWarn, | |
ServerErrorLevel: slog.LevelError, | |
WithUserAgent: false, | |
WithRequestID: false, | |
WithRequestBody: false, | |
WithRequestHeader: false, | |
WithResponseBody: false, | |
WithResponseHeader: false, | |
WithSpanID: false, | |
WithTraceID: false, | |
Filters: []slogfiber.Filter{ | |
slogfiber.IgnoreStatus( | |
http.StatusOK, | |
http.StatusNoContent, | |
http.StatusNotModified, | |
), | |
}, | |
}, | |
)) | |
// | |
// HTTP Endpoints | |
// | |
server.Get("/metrics", monitor.New()) | |
// Background tasks | |
server.Group("/api/background_task_runs"). | |
Get("/trigger/:name", func(c *fiber.Ctx) error { | |
res, err := scheduler.Run(ctx, c.Params("name", "")) | |
if err != nil { | |
return err | |
} | |
return c.JSON(res) | |
}). | |
Get("/view/:id", BackgroundTaskRunsView). | |
Get("/", BackgroundTaskRuns) | |
// Users | |
server.Group("/api/users"). | |
Get("/profiles", UserProfiles). | |
Get("/pending_approval", UsersPendingApproval). | |
Get("/view/:id", UserView). | |
Get("/", Users) | |
// Trending | |
server.Group("/api/trending/auto"). | |
Get("/", AutoTrendingIndex). | |
Post("/", AutoTrendingNew). | |
Post("/delete", AutoTrendingDelete). | |
Post("/edit", AutoTrendingEdit) | |
// IP Rules | |
server.Group("/api/ip-rules"). | |
Get("/", IPRuleIndex). | |
Get("/:id", IPRuleView). | |
Post("/", IPRuleNew). | |
Post("/delete/:id", IPRuleDelete). | |
Post("/edit", IPRuleEdit) | |
// IP info | |
server.Group("/api/ip-info"). | |
Get("/suspended-by/", IPInfoSuspendedBreakdown) | |
// Google sitemap | |
server.Group("/sitemap.xml", GoogleSitemap) | |
// | |
// HTTP static files | |
// | |
server.Use(filesystem.New(filesystem.Config{ | |
Root: http.Dir("./build"), | |
Browse: true, | |
Index: "index.html", | |
NotFoundFile: "index.html", | |
MaxAge: 0, | |
})) | |
// HTTP server shutdown | |
serverShutdown.Add(1) | |
go func() { | |
<-ctx.Done() | |
defer serverShutdown.Done() | |
logger.Warn("Stopping HTTP server") | |
if err := server.Shutdown(); err != nil { | |
logger.Error("Failed to stop HTTP server", "err", err) | |
return | |
} | |
logger.Info("HTTP server stopped") | |
}() | |
if err := server.Listen("0.0.0.0:8080"); err != nil { | |
return err | |
} | |
serverShutdown.Wait() | |
return nil | |
} | |
func jsonEncoder(v interface{}) ([]byte, error) { | |
var buff bytes.Buffer | |
encoder := json.NewEncoder(&buff) | |
encoder.SetIndent("", " ") | |
encoder.SetEscapeHTML(false) | |
err := encoder.Encode(v) | |
return buff.Bytes(), err | |
} | |
func errorHandler(ctx *fiber.Ctx, err error) error { | |
// Status code defaults to 500 | |
code := fiber.StatusInternalServerError | |
// Retrieve the custom status code if it's a *fiber.Error | |
var e *fiber.Error | |
if errors.As(err, &e) { | |
code = e.Code | |
} | |
return ctx.Status(code).JSON(getErrorBody(code, err)) | |
} | |
func getErrorBody(code int, input any) fiber.Map { | |
err, ok := input.(error) | |
if !ok { | |
return fiber.Map{ | |
"status": code, | |
"body": fiber.Map{ | |
"message": fmt.Sprintf("%v", input), | |
}, | |
} | |
} | |
switch err.(type) { | |
case *pgconn.PgError: | |
return fiber.Map{ | |
"status": code, | |
"body": fiber.Map{ | |
"message": err.Error(), | |
"details": err, | |
}, | |
} | |
default: | |
return fiber.Map{ | |
"status": code, | |
"body": fiber.Map{ | |
"message": err.Error(), | |
}, | |
} | |
} | |
} | |
func buildVersion() goversion.Info { | |
return goversion.GetVersionInfo( | |
// goversion.WithAppDetails("dottie", "Making .env file management easy", "https://github.com/jippi/dottie"), | |
func(versionInfo *goversion.Info) { | |
}, | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# syntax=docker/dockerfile:1 | |
ARG CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX="" | |
# | |
# svelte-builder | |
# | |
FROM ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}ubuntu:22.04 as app-builder | |
RUN set -ex ; apt-get update | |
RUN set -ex ; apt-get install -y curl | |
RUN set -ex ; curl https://get.volta.sh | bash | |
WORKDIR /app | |
COPY ./frontend /app | |
RUN --mount=type=cache,target=/root/.npm \ | |
--mount=type=cache,target=/app/node_modules \ | |
--mount=type=cache,target=/app/.svelte-kit \ | |
--mount=type=cache,target=/app/.yarn \ | |
set -ex \ | |
&& export PATH="$HOME/.volta/bin:$PATH" \ | |
&& volta run yarn \ | |
&& yarn setup | |
ENV NODE_ENV=production | |
ARG CI_COMMIT_BRANCH='' | |
ARG CI_COMMIT_SHA='' | |
ARG CI_COMMIT_SHORT_SHA='' | |
ARG CI_COMMIT_TIMESTAMP='' | |
ENV NODE_ENV=$NODE_ENV | |
ENV CI_COMMIT_BRANCH=$CI_COMMIT_BRANCH | |
ENV CI_COMMIT_SHA=$CI_COMMIT_SHA | |
ENV CI_COMMIT_SHORT_SHA=$CI_COMMIT_SHORT_SHA | |
ENV CI_COMMIT_TIMESTAMP=$CI_COMMIT_TIMESTAMP | |
RUN --mount=type=cache,target=/root/.npm \ | |
--mount=type=cache,target=/app/node_modules \ | |
--mount=type=cache,target=/app/.svelte-kit \ | |
--mount=type=cache,target=/app/.yarn \ | |
set -ex \ | |
&& export PATH="$HOME/.volta/bin:$PATH" \ | |
&& yarn build | |
# | |
# server-builder | |
# | |
FROM ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}golang:alpine as server-builder | |
WORKDIR /app | |
ENV CGO_ENABLED=0 | |
RUN apk add --no-cache build-base git | |
# Download Go modules | |
RUN --mount=type=cache,target=/root/.cache/go-build \ | |
--mount=type=cache,target=/go/pkg/mod \ | |
--mount=type=bind,source=backend/go.mod,target=go.mod \ | |
--mount=type=bind,source=backend/go.sum,target=go.sum \ | |
go mod download | |
COPY ./backend /app | |
RUN --mount=type=cache,target=/root/.cache/go-build \ | |
--mount=type=cache,target=/go/pkg/mod \ | |
go build -buildvcs=false -mod=readonly -v ./cmd/mastodon-mod-tools/ | |
# | |
# deploy | |
# | |
FROM ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}alpine as deployment | |
WORKDIR /app | |
COPY --from=app-builder /app/build /app/build | |
COPY --from=server-builder /app/mastodon-mod-tools /app | |
EXPOSE 8080 | |
CMD ./mastodon-mod-tools server |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment