From 41062151800061324267a2261e64200c6e8aad29 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Tue, 31 Mar 2026 18:43:17 -0400 Subject: [PATCH] zenno, cmd/horsebot: host website in horsebot --- cmd/horsebot/main.go | 87 +++++++++++++++++++++++++++---------- zenno/package-lock.json | 11 +++++ zenno/package.json | 1 + zenno/src/routes/+layout.ts | 1 + zenno/svelte.config.js | 5 +-- 5 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 zenno/src/routes/+layout.ts diff --git a/cmd/horsebot/main.go b/cmd/horsebot/main.go index a8c617a..5f32bc8 100644 --- a/cmd/horsebot/main.go +++ b/cmd/horsebot/main.go @@ -3,11 +3,14 @@ package main import ( "bytes" "context" + "encoding/hex" "encoding/json" "errors" "flag" "fmt" "log/slog" + "net" + "net/http" "os" "os/signal" "path/filepath" @@ -26,20 +29,23 @@ import ( func main() { var ( - dataDir string + // public site + addr string + dataDir string + public string + // discord tokenFile string - // http api options - addr string - route string - pubkey string - // logging options + apiRoute string + pubkey string + // logging level slog.Level textfmt string ) + flag.StringVar(&addr, "http", ":80", "`address` to bind HTTP server") flag.StringVar(&dataDir, "data", "", "`dir`ectory containing exported json data") + flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve") flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token") - flag.StringVar(&addr, "http", "", "`address` to bind HTTP API server") - flag.StringVar(&route, "route", "/interactions/callback", "`path` to serve HTTP API calls") + flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls") flag.StringVar(&pubkey, "key", "", "Discord public key") flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`") flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json") @@ -57,6 +63,16 @@ func main() { } slog.SetDefault(slog.New(lh)) + stat, err := os.Stat(public) + if err != nil { + slog.Error("public", slog.Any("err", err)) + os.Exit(1) + } + if !stat.IsDir() { + slog.Error("public", slog.String("err", "not a directory")) + os.Exit(1) + } + skills, err := loadSkills(filepath.Join(dataDir, "skill.json")) slog.Info("loaded skills", slog.Int("count", len(skills))) groups, err2 := loadSkillGroups(filepath.Join(dataDir, "skill-group.json")) @@ -87,33 +103,52 @@ func main() { r.SelectMenuComponent("/swap", skillSrv.menu) r.ButtonComponent("/swap/{id}", skillSrv.button) }) - - opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)} - if addr != "" { - if pubkey == "" { - slog.Error("Discord public key must be provided when using HTTP API") - os.Exit(1) - } - opts = append(opts, bot.WithHTTPServerConfigOpts(pubkey, - httpserver.WithAddress(addr), - httpserver.WithURL(route), - )) - } - slog.Info("connect", slog.String("disgo", disgo.Version)) - client, err := disgo.New(string(token), opts...) + client, err := disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r)) if err != nil { slog.Error("building bot", slog.Any("err", err)) os.Exit(1) } + mux := http.NewServeMux() + mux.Handle("GET /", http.FileServerFS(os.DirFS(public))) + if pubkey != "" { + pk, err := hex.DecodeString(pubkey) + if err != nil { + slog.Error("pubkey", slog.Any("err", err)) + os.Exit(1) + } + mux.Handle(apiRoute, httpserver.HandleInteraction(httpserver.DefaultVerifier{}, pk, slog.Default(), client.EventManager.HandleHTTPEvent)) + slog.Info("Discord HTTP API enabled", slog.String("pubkey", pubkey)) + } + l, err := net.Listen("tcp", addr) + if err != nil { + slog.Error("listen", slog.String("addr", addr), slog.Any("err", err)) + os.Exit(1) + } + srv := http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + BaseContext: func(l net.Listener) context.Context { return ctx }, + } + go func() { + slog.Info("HTTP", slog.Any("addr", l.Addr())) + err := srv.Serve(l) + if err == http.ErrServerClosed { + return + } + slog.Error("HTTP server closed", slog.Any("err", err)) + }() + if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil { slog.Error("syncing commands", slog.Any("err", err)) os.Exit(1) } - if addr != "" { - slog.Info("start HTTP server", slog.String("address", addr), slog.String("route", route)) + if pubkey != "" { + slog.Info("start HTTP server", slog.String("address", addr), slog.String("route", apiRoute)) if err := client.OpenHTTPServer(); err != nil { slog.Error("starting HTTP server", slog.Any("err", err)) stop() @@ -131,6 +166,10 @@ func main() { ctx, stop = context.WithTimeout(context.Background(), 5*time.Second) defer stop() client.Close(ctx) + if err := srv.Shutdown(ctx); err != nil { + slog.Error("HTTP API shutdown", slog.Any("err", err)) + os.Exit(1) + } } var commands = []discord.ApplicationCommandCreate{ diff --git a/zenno/package-lock.json b/zenno/package-lock.json index 4932687..e8a5dc8 100644 --- a/zenno/package-lock.json +++ b/zenno/package-lock.json @@ -11,6 +11,7 @@ "@eslint/compat": "^2.0.3", "@eslint/js": "^10.0.1", "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.18", @@ -1134,6 +1135,16 @@ "@sveltejs/kit": "^2.0.0" } }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, "node_modules/@sveltejs/kit": { "version": "2.55.0", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", diff --git a/zenno/package.json b/zenno/package.json index 894e683..08ef2a0 100644 --- a/zenno/package.json +++ b/zenno/package.json @@ -19,6 +19,7 @@ "@eslint/compat": "^2.0.3", "@eslint/js": "^10.0.1", "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.18", diff --git a/zenno/src/routes/+layout.ts b/zenno/src/routes/+layout.ts new file mode 100644 index 0000000..c8cacf0 --- /dev/null +++ b/zenno/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = true; \ No newline at end of file diff --git a/zenno/svelte.config.js b/zenno/svelte.config.js index 60e3634..c56c1f4 100644 --- a/zenno/svelte.config.js +++ b/zenno/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-auto'; +import adapter from '@sveltejs/adapter-static'; import { relative, sep } from 'node:path'; /** @type {import('@sveltejs/kit').Config} */ @@ -14,9 +14,6 @@ const config = { }, }, kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), }, };