From f9ccea9192efb6c3aa884b50f22b1237fdcd44d3 Mon Sep 17 00:00:00 2001
From: Pascal Zarrad
Date: Thu, 22 Dec 2022 20:45:15 +0100
Subject: [PATCH 1/2] feat(api): log commands received by the bot
Refs: #130
---
api/slash_commands.go | 108 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git a/api/slash_commands.go b/api/slash_commands.go
index acbe001..6ac7e6f 100644
--- a/api/slash_commands.go
+++ b/api/slash_commands.go
@@ -117,6 +117,14 @@ func InitCommandHandling(session *discordgo.Session) error {
// states do not end in prohibited execution of a command.
func handleCommandDispatch(s *discordgo.Session, i *discordgo.InteractionCreate) {
if command, ok := componentCommandMap[i.ApplicationCommandData().Name]; ok {
+ user := i.User
+ if nil == user {
+ user = i.Member.User
+ }
+ if nil == user {
+ command.c.Logger().Warn("Cannot handle the command \"%s\" without a user!", command.Cmd.Name)
+ }
+
if !IsComponentEnabled(command.c, i.GuildID) {
resp := &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
@@ -162,15 +170,115 @@ func handleCommandDispatch(s *discordgo.Session, i *discordgo.InteractionCreate)
if nil != err {
command.c.Logger().Err(err, "Failed to deliver interaction response on slash-command!")
+
+ return
}
+ command.c.Logger().Info("The user \"%s#%s\" with id \"%s\" tried to execute the "+
+ "disabled command \"%s\" with options \"%s\" %s",
+ user.Username,
+ user.Discriminator,
+ user.ID,
+ command.c.SlashCommandManager().computeFullCommandStringFromInteractionData(i.ApplicationCommandData()),
+ command.c.SlashCommandManager().computeConfiguredOptionsString(i.ApplicationCommandData().Options),
+ getGuildOrGlobalLogPart(i.GuildID, "on"))
+
return
}
+ command.c.Logger().Info("The user \"%s#%s\" with id \"%s\" executed the "+
+ "command \"%s\" with options \"%s\" %s",
+ user.Username,
+ user.Discriminator,
+ user.ID,
+ command.c.SlashCommandManager().computeFullCommandStringFromInteractionData(i.ApplicationCommandData()),
+ command.c.SlashCommandManager().computeConfiguredOptionsString(i.ApplicationCommandData().Options),
+ getGuildOrGlobalLogPart(i.GuildID, "on"))
+
command.Handler(s, i)
}
}
+// computeFullCommandStringFromInteractionData returns the full command name (e.g. jojo module enable) from the
+// passed discordgo.ApplicationCommandInteractionData.
+func (c *SlashCommandManager) computeFullCommandStringFromInteractionData(
+ cmd discordgo.ApplicationCommandInteractionData,
+) string {
+ if len(cmd.Options) < 1 {
+ return cmd.Name
+ }
+
+ option := cmd.Options[0]
+ if option == nil {
+ return cmd.Name
+ }
+
+ subCommand := c.computeSubCommandStringFromInteractionData(option)
+ if subCommand == "" {
+ return cmd.Name
+ }
+
+ return fmt.Sprintf("%s %s", cmd.Name, subCommand)
+}
+
+// computeSubCommandStringFromInteractionData returns, if available, the concatenated subcommand group name
+// and subcommand name of the passed option.
+func (c *SlashCommandManager) computeSubCommandStringFromInteractionData(
+ option *discordgo.ApplicationCommandInteractionDataOption,
+) string {
+ if option.Type == discordgo.ApplicationCommandOptionSubCommand {
+ return option.Name
+ }
+
+ if option.Type != discordgo.ApplicationCommandOptionSubCommandGroup {
+ return ""
+ }
+
+ if len(option.Options) == 0 {
+ return fmt.Sprintf("%s %s", option.Name, "")
+ }
+
+ subCommand := option.Options[0]
+ return fmt.Sprintf("%s %s", option.Name, c.computeSubCommandStringFromInteractionData(subCommand))
+}
+
+// computeConfiguredOptions creates a string out of all configured options of a application command interaction.
+func (c *SlashCommandManager) computeConfiguredOptionsString(
+ options []*discordgo.ApplicationCommandInteractionDataOption,
+) string {
+ configuredOptions := ""
+
+ if len(options) == 0 {
+ return ""
+ }
+
+ if options[0].Type == discordgo.ApplicationCommandOptionSubCommand {
+ return c.computeConfiguredOptionsString(options[0].Options)
+ }
+
+ if options[0].Type == discordgo.ApplicationCommandOptionSubCommandGroup {
+ subCommandGroup := options[0]
+
+ if len(subCommandGroup.Options) == 0 {
+ return ""
+ }
+
+ return c.computeConfiguredOptionsString(subCommandGroup.Options)
+ }
+
+ for _, option := range options {
+ if "" == configuredOptions {
+ configuredOptions = fmt.Sprintf("%s=%v", option.Name, option.Value)
+
+ continue
+ }
+
+ configuredOptions = fmt.Sprintf("%s; %s=%v", configuredOptions, option.Name, option.Value)
+ }
+
+ return configuredOptions
+}
+
// DeinitCommandHandling unregisters the event Handler
// that is registered by InitCommandHandling.
func DeinitCommandHandling() {
From 43ac934e1bdf83e33354e0b4ec8d19207910c3e9 Mon Sep 17 00:00:00 2001
From: Pascal Zarrad
Date: Thu, 22 Dec 2022 23:20:36 +0100
Subject: [PATCH 2/2] test(api): add tests for command logging
Refs: #130
---
api/slash_commands.go | 3 +-
api/slash_commands_test.go | 209 +++++++++++++++++++++++++++++++++++++
2 files changed, 211 insertions(+), 1 deletion(-)
diff --git a/api/slash_commands.go b/api/slash_commands.go
index 6ac7e6f..b23b105 100644
--- a/api/slash_commands.go
+++ b/api/slash_commands.go
@@ -231,7 +231,8 @@ func (c *SlashCommandManager) computeSubCommandStringFromInteractionData(
}
if option.Type != discordgo.ApplicationCommandOptionSubCommandGroup {
- return ""
+ // This is not a subcommand for sure.
+ return ""
}
if len(option.Options) == 0 {
diff --git a/api/slash_commands_test.go b/api/slash_commands_test.go
index 32978ff..dae634f 100644
--- a/api/slash_commands_test.go
+++ b/api/slash_commands_test.go
@@ -19,6 +19,7 @@
package api
import (
+ "github.com/bwmarrin/discordgo"
"github.com/lazybytez/jojo-discord-bot/api/entities"
"github.com/stretchr/testify/suite"
"testing"
@@ -38,6 +39,214 @@ func (suite *SlashCommandManagerTestSuite) SetupTest() {
suite.slashCommandManager = SlashCommandManager{owner: suite.owningComponent}
}
+func (suite *SlashCommandManagerTestSuite) TestComputeFullCommandStringFromInteractionData() {
+ tables := []struct {
+ input discordgo.ApplicationCommandInteractionData
+ expected string
+ }{
+ {
+ input: discordgo.ApplicationCommandInteractionData{
+ ID: "123451234512345",
+ Name: "dice",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionInteger,
+ Name: "dice-side-number",
+ Value: 5,
+ },
+ },
+ },
+ expected: "dice",
+ },
+ {
+ input: discordgo.ApplicationCommandInteractionData{
+ ID: "123451234512345",
+ Name: "jojo",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "sync-commands",
+ Value: &discordgo.ApplicationCommandInteractionDataOption{
+ Name: "sync-commands",
+ },
+ },
+ },
+ },
+ expected: "jojo sync-commands",
+ },
+ {
+ input: discordgo.ApplicationCommandInteractionData{
+ ID: "123451234512345",
+ Name: "jojo",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "sync-commands",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionString,
+ Name: "some-random-option",
+ Value: "test",
+ },
+ {
+ Type: discordgo.ApplicationCommandOptionInteger,
+ Name: "some-second-option",
+ Value: 2,
+ },
+ },
+ },
+ },
+ },
+ expected: "jojo sync-commands",
+ },
+ {
+ input: discordgo.ApplicationCommandInteractionData{
+ ID: "123451234512345",
+ Name: "jojo",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommandGroup,
+ Name: "module",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "list",
+ },
+ },
+ },
+ },
+ },
+ expected: "jojo module list",
+ },
+ {
+ input: discordgo.ApplicationCommandInteractionData{
+ ID: "123451234512345",
+ Name: "jojo",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommandGroup,
+ Name: "module",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "enable",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionString,
+ Name: "module",
+ Value: "ping_pong",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ expected: "jojo module enable",
+ },
+ }
+
+ for _, table := range tables {
+ result := suite.slashCommandManager.computeFullCommandStringFromInteractionData(table.input)
+
+ suite.Equal(table.expected, result)
+ }
+}
+
+func (suite *SlashCommandManagerTestSuite) TestComputeConfiguredOptionsString() {
+ tables := []struct {
+ input []*discordgo.ApplicationCommandInteractionDataOption
+ expected string
+ }{
+ {
+ input: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionInteger,
+ Name: "dice-side-number",
+ Value: 5,
+ },
+ },
+ expected: "dice-side-number=5",
+ },
+ {
+ input: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "sync-commands",
+ Value: &discordgo.ApplicationCommandInteractionDataOption{
+ Name: "sync-commands",
+ },
+ },
+ },
+ expected: "",
+ },
+ {
+ input: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "sync-commands",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionString,
+ Name: "some-random-option",
+ Value: "test",
+ },
+ {
+ Type: discordgo.ApplicationCommandOptionInteger,
+ Name: "some-second-option",
+ Value: 2,
+ },
+ },
+ },
+ },
+ expected: "some-random-option=test; some-second-option=2",
+ },
+ {
+ input: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommandGroup,
+ Name: "module",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "list",
+ },
+ },
+ },
+ },
+ expected: "",
+ },
+ {
+ input: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommandGroup,
+ Name: "module",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionSubCommand,
+ Name: "enable",
+ Options: []*discordgo.ApplicationCommandInteractionDataOption{
+ {
+ Type: discordgo.ApplicationCommandOptionString,
+ Name: "module",
+ Value: "ping_pong",
+ },
+ },
+ },
+ },
+ },
+ },
+ expected: "module=ping_pong",
+ },
+ }
+
+ for _, table := range tables {
+ result := suite.slashCommandManager.computeConfiguredOptionsString(table.input)
+
+ suite.Equal(table.expected, result)
+ }
+}
+
func (suite *SlashCommandManagerTestSuite) TestGetCommandsForComponentWithoutMatchingCommands() {
testComponentCode := entities.ComponentCode("no_commands_component")
componentCommandMap = map[string]*Command{