From 996155205d71a3d4a19aa6b16cebd401c909fdc0 Mon Sep 17 00:00:00 2001 From: Layla Date: Fri, 16 Jun 2023 01:24:25 +0000 Subject: [PATCH] Add Command Functionality to Discord Package & Implement Feedback Webhooks --- app/bot.go | 5 +- core/configuration.go | 15 ++++- discord/command.go | 112 ++++++++++++++++++++++++++++++++ discord/component_button.go | 4 ++ discord/discord.go | 33 ++++++++-- discord/user.go | 3 +- go.mod | 20 +++--- go.sum | 18 ++++++ main.go | 9 +++ modules/feedback_webhook.go | 125 ++++++++++++++++++++++++++++++++++++ 10 files changed, 324 insertions(+), 20 deletions(-) create mode 100644 discord/command.go create mode 100644 modules/feedback_webhook.go diff --git a/app/bot.go b/app/bot.go index 6091ce9..1f76d81 100644 --- a/app/bot.go +++ b/app/bot.go @@ -46,6 +46,9 @@ func (app *Bot) Initialize(cfg *core.Config) error { if app.guildID == "" { return fmt.Errorf("discord Guild ID is not set") } + if cfg.Discord.ApplicationID == "" { + return fmt.Errorf("discord Application ID is not set") + } if cfg.Mastodon.ClientID != "" && cfg.Mastodon.ClientSecret != "" && cfg.Mastodon.Username != "" && cfg.Mastodon.Password != "" && @@ -54,7 +57,7 @@ func (app *Bot) Initialize(cfg *core.Config) error { cfg.Mastodon.Username, cfg.Mastodon.Password) } - app.Session = discord.New(app.guildID, cfg.Discord.Token) + app.Session = discord.New(cfg.Discord.ApplicationID, app.guildID, cfg.Discord.Token) // Register Event Handlers app.Session.OnReady(app.onReady) diff --git a/core/configuration.go b/core/configuration.go index f22904e..50e32bc 100644 --- a/core/configuration.go +++ b/core/configuration.go @@ -6,13 +6,15 @@ import "strings" type Config struct { Discord DiscordConfig `yaml:"discord"` Mastodon MastodonConfig `yaml:"mastodon"` + Feedback Feedback `yaml:"feedback"` Features Features `yaml:"features"` } // DiscordConfig contains discord specific configuration type DiscordConfig struct { - Token string `yaml:"token" env:"DISCORD_TOKEN"` - GuildID string `yaml:"guild_id" env:"DISCORD_GUILD_ID"` + Token string `yaml:"token" env:"DISCORD_TOKEN"` + ApplicationID string `yaml:"application_id" env:"DISCORD_APPLICATION_ID"` + GuildID string `yaml:"guild_id" env:"DISCORD_GUILD_ID"` EventCategory string `yaml:"event_category" env:"DISCORD_EVENT_CATEGORY"` ArchiveCategory string `yaml:"archive_category" env:"DISCORD_ARCHIVE_CATEGORY"` @@ -21,6 +23,14 @@ type DiscordConfig struct { RoleSelections []RoleSelectionConfig `yaml:"role_selection"` } +type Feedback struct { + WebhookURL string `yaml:"url" env:"BIRD_FEEDBACK_URL"` + PayloadType string `yaml:"type" env:"BIRD_FEEDBACK_TYPE"` + + SuccessMessage string `yaml:"success_message"` + FailureMessage string `yaml:"failure_message"` +} + type RoleSelectionConfig struct { Title string `yaml:"title"` Description string `yaml:"description"` @@ -49,6 +59,7 @@ type Features struct { AnnounceEvents Feature `yaml:"announce_events" env:"BIRD_ANNOUNCE_EVENTS"` RecurringEvents Feature `yaml:"recurring_events" env:"BIRD_RECURRING_EVENTS"` RoleSelection Feature `yaml:"role_selection" env:"BIRD_ROLE_SELECTION"` + Feedback Feature `yaml:"feedback" env:"BIRD_FEEDBACK"` LoadGamePlugins Feature `yaml:"load_game_plugins" env:"BIRD_LOAD_GAME_PLUGINS"` } diff --git a/discord/command.go b/discord/command.go new file mode 100644 index 0000000..0e0fe4c --- /dev/null +++ b/discord/command.go @@ -0,0 +1,112 @@ +package discord + +import ( + "log" + + "github.com/bwmarrin/discordgo" + "github.com/yeslayla/birdbot/common" +) + +type CommandConfiguration struct { + Description string + EphemeralResponse bool + Options map[string]CommandOption +} + +type CommandOption struct { + Description string + Type CommandOptionType + Required bool +} + +type CommandOptionType uint64 + +const ( + CommandTypeString CommandOptionType = CommandOptionType(discordgo.ApplicationCommandOptionString) + CommandTypeInt CommandOptionType = CommandOptionType(discordgo.ApplicationCommandOptionInteger) + CommandTypeBool CommandOptionType = CommandOptionType(discordgo.ApplicationCommandOptionBoolean) + CommandTypeFloat CommandOptionType = CommandOptionType(discordgo.ApplicationCommandOptionNumber) +) + +// RegisterCommand creates an new command that can be used to interact with bird bot +func (discord *Discord) RegisterCommand(name string, config CommandConfiguration, handler func(common.User, map[string]any) string) { + command := &discordgo.ApplicationCommand{ + Name: name, + Description: config.Description, + } + + // Convert options to discordgo objects + command.Options = make([]*discordgo.ApplicationCommandOption, len(config.Options)) + index := 0 + for name, option := range config.Options { + command.Options[index] = &discordgo.ApplicationCommandOption{ + Name: name, + Description: option.Description, + Required: option.Required, + Type: discordgo.ApplicationCommandOptionType(option.Type), + } + index++ + } + + // Register handler + discord.commandHandlers[name] = func(session *discordgo.Session, r *discordgo.InteractionCreate) { + if r.Interaction.Type != discordgo.InteractionApplicationCommand { + return + } + + cmdOptions := r.ApplicationCommandData().Options + + // Parse option types + optionsMap := make(map[string]any, len(cmdOptions)) + for _, opt := range cmdOptions { + switch config.Options[opt.Name].Type { + case CommandTypeString: + optionsMap[opt.Name] = opt.StringValue() + case CommandTypeInt: + optionsMap[opt.Name] = opt.IntValue() + case CommandTypeBool: + optionsMap[opt.Name] = opt.BoolValue() + case CommandTypeFloat: + optionsMap[opt.Name] = opt.FloatValue() + default: + optionsMap[opt.Name] = opt.Value + } + } + + result := handler(NewUser(r.Member.User), optionsMap) + + if result != "" { + // Handle response + responseData := &discordgo.InteractionResponseData{ + Content: result, + } + + if config.EphemeralResponse { + responseData.Flags = discordgo.MessageFlagsEphemeral + } + + session.InteractionRespond(r.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: responseData, + }) + } else { + log.Printf("Command '%s' did not return a response: %v", name, optionsMap) + } + } + + cmd, err := discord.session.ApplicationCommandCreate(discord.applicationID, discord.guildID, command) + if err != nil { + log.Fatalf("Cannot create command '%s': %v", name, err) + } + discord.commands[name] = cmd +} + +// ClearCommands deregisters all commands from the discord API +func (discord *Discord) ClearCommands() { + for _, v := range discord.commands { + err := discord.session.ApplicationCommandDelete(discord.session.State.User.ID, discord.guildID, v.ID) + if err != nil { + log.Fatalf("Cannot delete command '%s': %v", v.Name, err) + } + } +} diff --git a/discord/component_button.go b/discord/component_button.go index 8cff66b..ef3db59 100644 --- a/discord/component_button.go +++ b/discord/component_button.go @@ -24,6 +24,10 @@ func (discord *Discord) NewButton(id string, label string) *Button { // OnClick registers an event when the button is clicked func (button *Button) OnClick(action func(user common.User)) { button.discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.InteractionCreate) { + if r.Interaction.Type != discordgo.InteractionMessageComponent { + return + } + if r.MessageComponentData().CustomID == button.ID { action(NewUser(r.Member.User)) diff --git a/discord/discord.go b/discord/discord.go index 8dddb48..7025fad 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -14,25 +14,32 @@ import ( type Discord struct { mock.Mock - guildID string - session *discordgo.Session + guildID string + applicationID string + session *discordgo.Session + + commands map[string]*discordgo.ApplicationCommand + commandHandlers map[string]func(session *discordgo.Session, i *discordgo.InteractionCreate) // Signal for shutdown stop chan os.Signal } // New creates a new Discord session -func New(guildID string, token string) *Discord { +func New(applicationID string, guildID string, token string) *Discord { // Create Discord Session session, err := discordgo.New(fmt.Sprint("Bot ", token)) if err != nil { log.Fatalf("Failed to create Discord session: %v", err) } - + session.ShouldReconnectOnError = true return &Discord{ - session: session, - guildID: guildID, + session: session, + applicationID: applicationID, + guildID: guildID, + commands: make(map[string]*discordgo.ApplicationCommand), + commandHandlers: make(map[string]func(*discordgo.Session, *discordgo.InteractionCreate)), } } @@ -44,10 +51,24 @@ func (discord *Discord) Run() error { } defer discord.session.Close() + // Register command handler + discord.session.AddHandler(func(session *discordgo.Session, i *discordgo.InteractionCreate) { + if i.GuildID != discord.guildID { + return + } + + if handler, ok := discord.commandHandlers[i.ApplicationCommandData().Name]; ok { + handler(session, i) + } + }) + // Keep alive discord.stop = make(chan os.Signal, 1) signal.Notify(discord.stop, os.Interrupt) <-discord.stop + + discord.ClearCommands() + return nil } diff --git a/discord/user.go b/discord/user.go index eb5776b..72cdb63 100644 --- a/discord/user.go +++ b/discord/user.go @@ -18,7 +18,8 @@ func NewUser(user *discordgo.User) common.User { } return common.User{ - ID: user.ID, + DisplayName: user.Username, + ID: user.ID, } } diff --git a/go.mod b/go.mod index 3431194..e7558ec 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,26 @@ module github.com/yeslayla/birdbot go 1.20 require ( - github.com/bwmarrin/discordgo v0.26.1 - github.com/ilyakaznacheev/cleanenv v1.4.0 - github.com/stretchr/testify v1.8.1 + github.com/bwmarrin/discordgo v0.27.1 + github.com/ilyakaznacheev/cleanenv v1.4.2 + github.com/mattn/go-mastodon v0.0.6 + github.com/mattn/go-sqlite3 v1.14.17 + github.com/rubenv/sql-migrate v1.4.0 + github.com/stretchr/testify v1.8.4 ) require ( - github.com/BurntSushi/toml v1.2.1 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/joho/godotenv v1.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/magefile/mage v1.14.0 // indirect - github.com/mattn/go-mastodon v0.0.5 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rubenv/sql-migrate v1.4.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/sys v0.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 08524eb..ef9f4fa 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -63,6 +65,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE= github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= +github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -226,11 +230,15 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ilyakaznacheev/cleanenv v1.4.0 h1:Gvwxt6wAPUo9OOxyp5Xz9eqhLsAey4AtbCF5zevDnvs= github.com/ilyakaznacheev/cleanenv v1.4.0/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA= +github.com/ilyakaznacheev/cleanenv v1.4.2 h1:nRqiriLMAC7tz7GzjzUTBHfzdzw6SQ7XvTagkFqe/zU= +github.com/ilyakaznacheev/cleanenv v1.4.2/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -273,11 +281,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-mastodon v0.0.5 h1:P0e1/R2v3ho6kM7BUW0noQm8gAqHE0p8Gq1TMapIVAc= github.com/mattn/go-mastodon v0.0.5/go.mod h1:cg7RFk2pcUfHZw/IvKe1FUzmlq5KnLFqs7eV2PHplV8= +github.com/mattn/go-mastodon v0.0.6 h1:lqU1sOeeIapaDsDUL6udDZIzMb2Wqapo347VZlaOzf0= +github.com/mattn/go-mastodon v0.0.6/go.mod h1:cg7RFk2pcUfHZw/IvKe1FUzmlq5KnLFqs7eV2PHplV8= github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -373,6 +385,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= @@ -421,6 +435,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -583,6 +599,8 @@ golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/main.go b/main.go index 4b51fbc..e45d14e 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,15 @@ func main() { } } + if cfg.Features.Feedback.IsEnabled() { + loader.LoadComponent(modules.NewFeedbackWebhookComponent(cfg.Feedback.WebhookURL, modules.FeedbackWebhookConfiguration{ + PayloadType: cfg.Feedback.PayloadType, + + SuccessMessage: cfg.Feedback.SuccessMessage, + FailureMessage: cfg.Feedback.FailureMessage, + }, bot.Session)) + } + if _, err := os.Stat(PluginsDirectory); !os.IsNotExist(err) { components := app.LoadPlugins(PluginsDirectory) for _, comp := range components { diff --git a/modules/feedback_webhook.go b/modules/feedback_webhook.go new file mode 100644 index 0000000..d3445b2 --- /dev/null +++ b/modules/feedback_webhook.go @@ -0,0 +1,125 @@ +package modules + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/yeslayla/birdbot/common" + "github.com/yeslayla/birdbot/discord" +) + +type feedbackWebhookModule struct { + session *discord.Discord + webhookURL string + payloadType string + successMessage string + failureMessage string +} + +type FeedbackWebhookConfiguration struct { + SuccessMessage string + FailureMessage string + PayloadType string +} + +// NewFeedbackWebhookComponent creates a new component +func NewFeedbackWebhookComponent(webhookURL string, config FeedbackWebhookConfiguration, session *discord.Discord) common.Module { + m := &feedbackWebhookModule{ + session: session, + webhookURL: webhookURL, + payloadType: "default", + successMessage: "Feedback recieved!", + failureMessage: "Failed to send feedback!", + } + + if config.SuccessMessage != "" { + m.successMessage = config.SuccessMessage + } + if config.FailureMessage != "" { + m.failureMessage = config.FailureMessage + } + if config.PayloadType != "" { + m.payloadType = config.PayloadType + } + + return m +} + +func (c *feedbackWebhookModule) Initialize(birdbot common.ModuleManager) error { + c.session.RegisterCommand("feedback", discord.CommandConfiguration{ + Description: "Sends a feedback message", + EphemeralResponse: true, + Options: map[string]discord.CommandOption{ + "message": { + Description: "Content of what you'd like to communicate in your feedback.", + Type: discord.CommandTypeString, + Required: true, + }, + }, + }, func(user common.User, args map[string]any) string { + + message, ok := args["message"] + if !ok { + return "Missing content in command" + } + + var data []byte + + // Supported payload types + switch c.payloadType { + case "discord": + data, _ = json.Marshal(map[string]any{ + "content": fmt.Sprintf("%s: %s", user.DisplayName, message), + }) + case "slack": + data, _ = json.Marshal(map[string]any{ + "text": fmt.Sprintf("%s: %s", user.DisplayName, message), + }) + default: + data, _ = json.Marshal(map[string]any{ + "message": message, + "username": user.DisplayName, + }) + } + + body := bytes.NewBuffer(data) + + // Send HTTP request + resp, err := http.Post(c.webhookURL, "application/json", body) + if err != nil { + log.Printf("Failed to post feedback to url '%s': %s", c.webhookURL, err) + return c.failureMessage + } + + // Validate response + if resp.Status[0] != '2' { + log.Printf("Webhook returned %v: %s", resp.Status, message) + return c.failureMessage + } + + // Read body for any special response + response := map[any]any{} + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return c.successMessage + } + + if err = json.Unmarshal(responseBody, &response); err != nil { + return c.successMessage + } + + if message, ok := response["message"]; ok { + v := fmt.Sprint(message) + if len(v) > 0 { + return v + } + } + + return c.successMessage + }) + return nil +} -- 2.51.1