192 lines
5.3 KiB
Go
192 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.sunturtle.xyz/zephyr/horse/horse"
|
|
"git.sunturtle.xyz/zephyr/horse/horse/global"
|
|
"github.com/disgoorg/disgo"
|
|
"github.com/disgoorg/disgo/bot"
|
|
"github.com/disgoorg/disgo/discord"
|
|
"github.com/disgoorg/disgo/handler"
|
|
"github.com/disgoorg/disgo/handler/middleware"
|
|
"github.com/disgoorg/disgo/httpserver"
|
|
"github.com/disgoorg/disgo/rest"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
tokenFile string
|
|
// http api options
|
|
addr string
|
|
route string
|
|
pubkey string
|
|
// logging options
|
|
level slog.Level
|
|
textfmt string
|
|
)
|
|
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(&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")
|
|
flag.Parse()
|
|
|
|
var lh slog.Handler
|
|
switch textfmt {
|
|
case "text":
|
|
lh = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})
|
|
case "json":
|
|
lh = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "invalid log format %q, must be text or json", textfmt)
|
|
os.Exit(1)
|
|
}
|
|
slog.SetDefault(slog.New(lh))
|
|
|
|
token, err := os.ReadFile(tokenFile)
|
|
if err != nil {
|
|
slog.Error("reading token", slog.Any("err", err))
|
|
os.Exit(1)
|
|
}
|
|
token = bytes.TrimSuffix(token, []byte{'\n'})
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
|
|
r := handler.New()
|
|
r.DefaultContext(func() context.Context { return ctx })
|
|
r.Use(middleware.Go)
|
|
r.Use(logMiddleware)
|
|
r.Route("/skill", func(r handler.Router) {
|
|
r.SlashCommand("/", skillHandler)
|
|
r.Autocomplete("/", skillAutocomplete)
|
|
r.ButtonComponent("/{id}", skillButton)
|
|
})
|
|
|
|
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...)
|
|
if err != nil {
|
|
slog.Error("building bot", slog.Any("err", err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
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 err := client.OpenHTTPServer(); err != nil {
|
|
slog.Error("starting HTTP server", slog.Any("err", err))
|
|
stop()
|
|
}
|
|
}
|
|
slog.Info("start gateway")
|
|
if err := client.OpenGateway(ctx); err != nil {
|
|
slog.Error("starting gateway", slog.Any("err", err))
|
|
stop()
|
|
}
|
|
slog.Info("ready")
|
|
<-ctx.Done()
|
|
stop()
|
|
|
|
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer stop()
|
|
client.Close(ctx)
|
|
}
|
|
|
|
var commands = []discord.ApplicationCommandCreate{
|
|
discord.SlashCommandCreate{
|
|
Name: "skill",
|
|
Description: "Umamusume skill data",
|
|
Options: []discord.ApplicationCommandOption{
|
|
discord.ApplicationCommandOptionString{
|
|
Name: "query",
|
|
Description: "Skill name or ID",
|
|
Required: true,
|
|
Autocomplete: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func skillHandler(data discord.SlashCommandInteractionData, e *handler.CommandEvent) error {
|
|
q := data.String("query")
|
|
id, err := strconv.ParseInt(q, 10, 32)
|
|
if err == nil {
|
|
// note inverted condition; this is when we have an id
|
|
id = int64(global.AllSkills[horse.SkillID(id)].ID)
|
|
}
|
|
if id == 0 {
|
|
// Either we weren't given a number or the number doesn't match any skill ID.
|
|
v := global.SkillNameToID[q]
|
|
if v == 0 {
|
|
// No such skill.
|
|
m := discord.MessageCreate{
|
|
Content: "No such skill.",
|
|
Flags: discord.MessageFlagEphemeral,
|
|
}
|
|
return e.CreateMessage(m)
|
|
}
|
|
id = int64(v)
|
|
}
|
|
// TODO(zeph): search conditions and effects, give a list
|
|
m := discord.MessageCreate{
|
|
Components: []discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)},
|
|
Flags: discord.MessageFlagIsComponentsV2,
|
|
}
|
|
return e.CreateMessage(m)
|
|
}
|
|
|
|
func skillAutocomplete(e *handler.AutocompleteEvent) error {
|
|
q := strings.ToLower(e.Data.String("query"))
|
|
r := make([]discord.AutocompleteChoice, 0, 25)
|
|
for k, _ := range global.SkillNameToID {
|
|
if strings.HasPrefix(strings.ToLower(k), q) {
|
|
r = append(r, discord.AutocompleteChoiceString{Name: k, Value: k})
|
|
if len(r) == cap(r) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return e.AutocompleteResult(r)
|
|
}
|
|
|
|
func skillButton(data discord.ButtonInteractionData, e *handler.ComponentEvent) error {
|
|
id, err := strconv.ParseInt(e.Vars["id"], 10, 32)
|
|
if err != nil {
|
|
m := discord.MessageCreate{
|
|
Content: "That button produced an invalid skill ID. That's not supposed to happen.",
|
|
Flags: discord.MessageFlagEphemeral,
|
|
}
|
|
return e.CreateMessage(m)
|
|
}
|
|
m := discord.MessageUpdate{
|
|
Components: &[]discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)},
|
|
}
|
|
return e.UpdateMessage(m)
|
|
}
|