From 9de994c73cbf3d1e370126f778ec53bad879f9e0 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Mon, 22 Aug 2022 23:45:07 -0700 Subject: [PATCH] examples: Add commands-hybrid --- 0-examples/commands-hybrid/README.md | 23 ++++ 0-examples/commands-hybrid/main.go | 178 +++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 0-examples/commands-hybrid/README.md create mode 100644 0-examples/commands-hybrid/main.go diff --git a/0-examples/commands-hybrid/README.md b/0-examples/commands-hybrid/README.md new file mode 100644 index 00000000..2e6183bd --- /dev/null +++ b/0-examples/commands-hybrid/README.md @@ -0,0 +1,23 @@ +# commands-hybrid + +commands-hybrid is an alternative variant of commands, where the program permits +being hosted either as a Gateway-based daemon or as a web server using the +Interactions Webhook API. + +## Usage + +### Gateway Mode + +```sh +BOT_TOKEN="" go run . +``` + +### Interactions Webhook Mode + +```sh +BOT_TOKEN="" WEBHOOK_ADDR="localhost:29485" WEBHOOK_PUBKEY="" go run . +``` + +The endpoint will be `http://localhost:29485/`. I recommend using something like +[srv.us](https://srv.us) to expose this endpoint as a public one, which can then +be used by Discord. diff --git a/0-examples/commands-hybrid/main.go b/0-examples/commands-hybrid/main.go new file mode 100644 index 00000000..6badf91c --- /dev/null +++ b/0-examples/commands-hybrid/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "time" + + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/api/webhook" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/state" + "github.com/diamondburned/arikawa/v3/utils/json/option" + "github.com/pkg/errors" +) + +func main() { + token := os.Getenv("BOT_TOKEN") + if token == "" { + log.Fatalln("No $BOT_TOKEN given.") + } + + var h handler + + var ( + webhookAddr = os.Getenv("WEBHOOK_ADDR") + webhookPubkey = os.Getenv("WEBHOOK_PUBKEY") + ) + + if webhookAddr != "" { + h.s = state.NewAPIOnlyState(token, nil) + + srv, err := webhook.NewInteractionServer(webhookPubkey, &h) + if err != nil { + log.Fatalln("cannot create interaction server:", err) + } + + if err := overwriteCommands(h.s); err != nil { + log.Fatalln("cannot update commands:", err) + } + + log.Println("listening and serving at", webhookAddr+"/") + log.Fatalln(http.ListenAndServe(webhookAddr, srv)) + } else { + h.s = state.New("Bot " + token) + h.s.AddInteractionHandler(&h) + h.s.AddIntents(gateway.IntentGuilds) + h.s.AddHandler(func(*gateway.ReadyEvent) { + me, _ := h.s.Me() + log.Println("connected to the gateway as", me.Tag()) + }) + + if err := overwriteCommands(h.s); err != nil { + log.Fatalln("cannot update commands:", err) + } + + if err := h.s.Connect(context.Background()); err != nil { + log.Fatalln("cannot connect:", err) + } + } +} + +var commands = []api.CreateCommandData{ + { + Name: "ping", + Description: "ping pong!", + }, + { + Name: "echo", + Description: "echo back the argument", + Options: []discord.CommandOption{ + &discord.StringOption{ + OptionName: "argument", + Description: "what's echoed back", + Required: true, + }, + }, + }, + { + Name: "thonk", + Description: "biiiig thonk", + }, +} + +func overwriteCommands(s *state.State) error { + app, err := s.CurrentApplication() + if err != nil { + return errors.Wrap(err, "cannot get current app ID") + } + + _, err = s.BulkOverwriteCommands(app.ID, commands) + return err +} + +type handler struct { + s *state.State +} + +func (h *handler) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse { + switch data := ev.Data.(type) { + case *discord.CommandInteraction: + switch data.Name { + case "ping": + return h.cmdPing(ev, data) + case "echo": + return h.cmdEcho(ev, data) + case "thonk": + return h.cmdThonk(ev, data) + default: + return errorResponse(fmt.Errorf("unknown command %q", data.Name)) + } + default: + return errorResponse(fmt.Errorf("unknown interaction %T", ev.Data)) + } +} + +func (h *handler) cmdPing(ev *discord.InteractionEvent, _ *discord.CommandInteraction) *api.InteractionResponse { + return &api.InteractionResponse{ + Type: api.MessageInteractionWithSource, + Data: &api.InteractionResponseData{ + Content: option.NewNullableString("Pong!"), + }, + } +} + +func (h *handler) cmdEcho(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse { + var options struct { + Arg string `discord:"argument"` + } + + if err := data.Options.Unmarshal(&options); err != nil { + return errorResponse(err) + } + + return &api.InteractionResponse{ + Type: api.MessageInteractionWithSource, + Data: &api.InteractionResponseData{ + Content: option.NewNullableString(options.Arg), + AllowedMentions: &api.AllowedMentions{}, + }, + } +} + +func (h *handler) cmdThonk(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse { + go func() { + time.Sleep(time.Duration(3+rand.Intn(5)) * time.Second) + + h.s.FollowUpInteraction(ev.AppID, ev.Token, api.InteractionResponseData{ + Content: option.NewNullableString("https://tenor.com/view/thonk-thinking-sun-thonk-sun-thinking-sun-gif-14999983"), + }) + }() + + return deferResponse(0) +} + +func errorResponse(err error) *api.InteractionResponse { + return &api.InteractionResponse{ + Type: api.MessageInteractionWithSource, + Data: &api.InteractionResponseData{ + Content: option.NewNullableString("**Error:** " + err.Error()), + Flags: discord.EphemeralMessage, + AllowedMentions: &api.AllowedMentions{ /* none */ }, + }, + } +} + +func deferResponse(flags discord.MessageFlags) *api.InteractionResponse { + return &api.InteractionResponse{ + Type: api.DeferredMessageInteractionWithSource, + Data: &api.InteractionResponseData{ + Flags: flags, + }, + } +}