From 435da61405cd127d0fd3261c665a3fbd8ec679a1 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Fri, 24 Jul 2026 17:18:44 -0400 Subject: [PATCH] cmd: move toradl into horsebot Fixes #31. --- cmd/horsebot/README.md | 14 ++++- cmd/horsebot/main.go | 16 +++++ cmd/{toradl/main.go => horsebot/toradl.go} | 71 ++++++++++++++-------- cmd/toradl/README.md | 8 --- cmd/toradl/mdb.go | 51 ---------------- 5 files changed, 72 insertions(+), 88 deletions(-) rename cmd/{toradl/main.go => horsebot/toradl.go} (77%) delete mode 100644 cmd/toradl/README.md delete mode 100644 cmd/toradl/mdb.go diff --git a/cmd/horsebot/README.md b/cmd/horsebot/README.md index 3068a2d..0980a8f 100644 --- a/cmd/horsebot/README.md +++ b/cmd/horsebot/README.md @@ -1,10 +1,20 @@ # horsebot -Discord bot serving horse game data. +Website and Discord bot serving horse game data. Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations. -## Running +The `-mdb` flag is mandatory for both website and Discord bot modes. +It provides data for both the horsebot API and the discord bot. +## Running the Website + +If the `-public` flag is provided, its value gives the directory to serve at `/`. +The `-img` flag gives a separate directory into which images are synced from GameTora; obtain permission from Gertas prior to using it. +The images are served on `/img/`. + +## Running the Discord Bot + +Provide the `-token` flag pointing to a file containing the token. The bot always uses the Gateway API. If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided. diff --git a/cmd/horsebot/main.go b/cmd/horsebot/main.go index 396a89d..f220c08 100644 --- a/cmd/horsebot/main.go +++ b/cmd/horsebot/main.go @@ -36,6 +36,7 @@ func main() { addr string mdbf string public string + img string // discord tokenFile string apiRoute string @@ -47,6 +48,7 @@ func main() { flag.StringVar(&addr, "http", ":13669", "`address` to bind HTTP server") flag.StringVar(&mdbf, "mdb", "", "`path` to master.mdb") flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve") + flag.StringVar(&img, "img", "", "`dir`ectory to save and serve images") flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token") flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls") flag.StringVar(&pubkey, "key", "", "Discord public key") @@ -86,6 +88,17 @@ func main() { os.Exit(1) } + if img != "" { + // Ensure the img directory is in fact a directory + // before we start trying to fill it out. + err := os.MkdirAll(img, 0777) + if err != nil { + slog.Error("img", slog.Any("err", err)) + os.Exit(1) + } + go saveImgs(ctx, db, img) + } + var client *bot.Client if tokenFile != "" { skills, err := mdb.Skills(ctx, db) @@ -128,6 +141,9 @@ func main() { if public != "" { mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public)))) } + if img != "" { + mux.Handle("GET /img/", http.StripPrefix("/img", http.FileServerFS(os.DirFS(img)))) + } mux.Handle("GET /api/global/affinity", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.AffinitySummary))) mux.Handle("GET /api/global/affinity-detail", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.AffinityDetail))) mux.Handle("GET /api/global/character", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Characters))) diff --git a/cmd/toradl/main.go b/cmd/horsebot/toradl.go similarity index 77% rename from cmd/toradl/main.go rename to cmd/horsebot/toradl.go index fa02a31..1343e66 100644 --- a/cmd/toradl/main.go +++ b/cmd/horsebot/toradl.go @@ -3,14 +3,12 @@ package main import ( "context" "errors" - "flag" "fmt" "image" "image/png" "log/slog" "net/http" "os" - "os/signal" "path/filepath" "strconv" "time" @@ -22,31 +20,7 @@ import ( "zombiezen.com/go/sqlite/sqlitex" ) -func main() { - var ( - mdb string - out string - ) - flag.StringVar(&mdb, "mdb", "", "`path` to master.mdb") - flag.StringVar(&out, "o", "", "output `dir`ectory") - flag.Parse() - - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - go func() { - <-ctx.Done() - stop() - }() - - db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) - if err != nil { - slog.Error("mdb", slog.Any("err", err)) - os.Exit(1) - } - if err := os.MkdirAll(out, 0777); err != nil { - slog.Error("output", slog.Any("err", err)) - os.Exit(1) - } - +func saveImgs(ctx context.Context, db *sqlitex.Pool, out string) { skill, err := skillIconIDs(ctx, db) if err != nil { slog.Error("skills", slog.Any("err", err)) @@ -71,6 +45,49 @@ func main() { } } +// load scans all results of sql and appends them to r. +func load[T any](ctx context.Context, db *sqlitex.Pool, r []T, sql string, row func(*sqlite.Stmt) T) ([]T, error) { + conn, err := db.Take(ctx) + defer db.Put(conn) + if err != nil { + return nil, err + } + stmt, err := conn.Prepare(sql) + if err != nil { + return nil, err + } + + for { + ok, err := stmt.Step() + if err != nil { + return r, err + } + if !ok { + break + } + r = append(r, row(stmt)) + } + return r, err +} + +func skillIconIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { + return load(ctx, db, nil, "SELECT DISTINCT icon_id FROM skill_data", func(s *sqlite.Stmt) int32 { + return s.ColumnInt32(0) + }) +} + +func charaIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { + return load(ctx, db, nil, "SELECT id FROM chara_data", func(s *sqlite.Stmt) int32 { + return s.ColumnInt32(0) + }) +} + +func rankIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { + return load(ctx, db, nil, "SELECT id-1 FROM single_mode_rank", func(s *sqlite.Stmt) int32 { + return s.ColumnInt32(0) + }) +} + var pngenc = &png.Encoder{CompressionLevel: png.BestCompression} func idouts(ctx context.Context, g *errgroup.Group, tick *time.Ticker, ids []int32, p, urlf string, sizes ...image.Point) { diff --git a/cmd/toradl/README.md b/cmd/toradl/README.md deleted file mode 100644 index dc3d240..0000000 --- a/cmd/toradl/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# toradl - -Download and process icons from GameTora. -Obtain permission from Gertas before using this program. - -toradl pulls the list of all skill icon IDs, character IDs, and rating IDs from the MDB -Then, for each that has no previous output, it downloads the corresponding icon from GameTora. -Each type is resized to various sizes and saved as PNG and AVIF. diff --git a/cmd/toradl/mdb.go b/cmd/toradl/mdb.go deleted file mode 100644 index 543a796..0000000 --- a/cmd/toradl/mdb.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "context" - - "zombiezen.com/go/sqlite" - "zombiezen.com/go/sqlite/sqlitex" -) - -// load scans all results of sql and appends them to r. -func load[T any](ctx context.Context, db *sqlitex.Pool, r []T, sql string, row func(*sqlite.Stmt) T) ([]T, error) { - conn, err := db.Take(ctx) - defer db.Put(conn) - if err != nil { - return nil, err - } - stmt, err := conn.Prepare(sql) - if err != nil { - return nil, err - } - - for { - ok, err := stmt.Step() - if err != nil { - return r, err - } - if !ok { - break - } - r = append(r, row(stmt)) - } - return r, err -} - -func skillIconIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { - return load(ctx, db, nil, "SELECT DISTINCT icon_id FROM skill_data", func(s *sqlite.Stmt) int32 { - return s.ColumnInt32(0) - }) -} - -func charaIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { - return load(ctx, db, nil, "SELECT id FROM chara_data", func(s *sqlite.Stmt) int32 { - return s.ColumnInt32(0) - }) -} - -func rankIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) { - return load(ctx, db, nil, "SELECT id-1 FROM single_mode_rank", func(s *sqlite.Stmt) int32 { - return s.ColumnInt32(0) - }) -}