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{