Skip to content

Instantly share code, notes, and snippets.

@jippi
Last active March 1, 2025 00:57
Show Gist options
  • Save jippi/32eb0384fd1a8caccda75d1901ae562b to your computer and use it in GitHub Desktop.
Save jippi/32eb0384fd1a8caccda75d1901ae562b to your computer and use it in GitHub Desktop.
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) {
},
)
}
# 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