Skip to content

Commit

Permalink
Merge pull request #131 from lazybytez/feature/command-logging
Browse files Browse the repository at this point in the history
Log commands executed by the bot
  • Loading branch information
elias-knodel authored Jan 18, 2023
2 parents fbf2730 + 43ac934 commit 29809f0
Show file tree
Hide file tree
Showing 2 changed files with 318 additions and 0 deletions.
109 changes: 109 additions & 0 deletions api/slash_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -162,15 +170,116 @@ 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 {
// This is not a subcommand for sure.
return ""
}

if len(option.Options) == 0 {
return fmt.Sprintf("%s %s", option.Name, "<no subcommand declared>")
}

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() {
Expand Down
209 changes: 209 additions & 0 deletions api/slash_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand Down

0 comments on commit 29809f0

Please sign in to comment.