diff --git a/bolt.go b/bolt.go index 104096c..b4ef3fe 100644 --- a/bolt.go +++ b/bolt.go @@ -38,11 +38,11 @@ type Bolt interface { //filtered methods stop() error messageHandler(s *dg.Session, msg *dg.MessageCreate) - handleCommand(msg *dg.MessageCreate, s *dg.Session, server *dg.Guild, channel *dg.Channel, lg int) error + handleCommand(msgEvent *MessageCreateEvent, s *dg.Session, lg int) error createReply(content, message, channel, guild string) *dg.MessageSend getRemainingTimeout(timeout time.Time) string - roleCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (bool, error) - timeoutCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (bool, error) + roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) + timeoutCheck(msgID, channelID, guildID string, s *dg.Session, run Command) (bool, error) } // create a new bolt interface @@ -66,7 +66,7 @@ func New(opts ...Option) (Bolt, error) { //set default command indicator b.indicator = "." - //apply options to bolt + //apply options for _, opt := range opts { opt(b) } @@ -74,8 +74,8 @@ func New(opts ...Option) (Bolt, error) { return b, nil } -// starts the bot, commands are added and the connection to Discord is opened, this is a BLOCKING -// call that handles safe shutdown of the bot +// starts the bot, commands are added and the connection to Discord is opened, this is a BLOCKING call +// that handles safe shutdown of the bot func (b *bolt) Start() error { //register commands and open connection b.AddHandler(b.messageHandler) @@ -109,7 +109,8 @@ func (b *bolt) AddCommands(cmd ...Command) { } } -// handler function that parses message data and executes any command payloads +// handler function that parses message data, handles logging the message based on logLevel, and executes +// the payload function in a goroutine func (b *bolt) messageHandler(s *dg.Session, msg *dg.MessageCreate) { //get server information server, err := s.Guild(msg.GuildID) @@ -131,7 +132,7 @@ func (b *bolt) messageHandler(s *dg.Session, msg *dg.MessageCreate) { //the bot will ignore it's own messages to prevent command loops if msg.Author.ID == s.State.User.ID { - if b.logLvl == LogLevelCmd { + if b.logLvl == LogLevelCmd || b.logLvl == LogLevelAll { //log command responses log.Printf("< %s | %s | %s > %s\n", server.Name, channel.Name, msg.Author.Username, msg.Content) } @@ -146,29 +147,42 @@ func (b *bolt) messageHandler(s *dg.Session, msg *dg.MessageCreate) { //does the message have the command indicator lg := len(b.indicator) if msg.Content[:lg] == b.indicator { - err := b.handleCommand(msg, s, server, channel, lg) - if err != nil { - log.Println(err) - return + mCreate := &MessageCreateEvent{ + AuthorUsername: msg.Author.Username, + AuthorID: msg.Author.ID, + AuthorRoles: msg.Member.Roles, + MsgID: msg.ID, + Msg: msg.Content, + MsgChanID: msg.ChannelID, + MsgGuildID: msg.GuildID, + MsgAttachments: msg.Attachments, } + + if b.logLvl == LogLevelCmd { + //log commands + log.Printf("< %s | %s | %s > %s\n", mCreate.MsgGuildName, mCreate.MsgChanName, mCreate.AuthorUsername, mCreate.Msg) + } + + //handled in its own goroutine to allow for async commands + go func() { + err := b.handleCommand(mCreate, s, lg) + if err != nil { + log.Println(err) + } + }() } } // parses command from message and handles timeout checks, role checks, and command execution. All command responses are sent back to Discord -func (b *bolt) handleCommand(msg *dg.MessageCreate, s *dg.Session, server *dg.Guild, channel *dg.Channel, lg int) error { - if b.logLvl == LogLevelCmd { - //log commands - log.Printf("< %s | %s | %s > %s\n", server.Name, channel.Name, msg.Author.Username, msg.Content) - } - - words := strings.Split(msg.Content, " ") +func (b *bolt) handleCommand(msgEvent *MessageCreateEvent, s *dg.Session, lg int) error { + words := strings.Split(msgEvent.Msg, " ") run, ok := b.commands[words[0][lg:]] if !ok { return nil //command doesn't exist, maybe log or respond to author } //has command met its timeout requirements - tc, err := b.timeoutCheck(msg, s, run) + tc, err := b.timeoutCheck(msgEvent.MsgID, msgEvent.MsgChanID, msgEvent.MsgGuildID, s, run) if err != nil { return fmt.Errorf("failed to calculate timeout for %s\n%e", run.Trigger, err) } @@ -178,32 +192,38 @@ func (b *bolt) handleCommand(msg *dg.MessageCreate, s *dg.Session, server *dg.Gu //does user have correct permissions if run.Roles != nil { - check, err := b.roleCheck(msg, s, run) + check, err := b.roleCheck(msgEvent.MsgGuildID, msgEvent.AuthorRoles, s, run) if err != nil { return fmt.Errorf("failed to perform permission checks for %s\n%e", run.Trigger, err) } if !check { + reply := b.createReply("you do not have permissions to run that command", msgEvent.MsgID, msgEvent.MsgChanID, msgEvent.MsgGuildID) + _, err := s.ChannelMessageSendComplex(msgEvent.MsgChanID, reply) + if err != nil { + return err + } return nil } } + //populate message struct exposed to client plMsg := Message{ - Author: msg.Author.Username, - ID: msg.Author.ID, - msgID: msg.ID, + Author: msgEvent.AuthorUsername, + ID: msgEvent.AuthorID, + msgID: msgEvent.MsgID, Words: words, - Content: msg.Content, - Channel: channel.Name, - channelID: msg.ChannelID, - Server: server.Name, - serverID: server.ID, + Content: msgEvent.Msg, + Channel: msgEvent.MsgChanName, + channelID: msgEvent.MsgChanID, + Server: msgEvent.MsgGuildName, + serverID: msgEvent.MsgGuildID, sesh: b, } //check for file attachments - if len(msg.Attachments) > 0 { + if len(msgEvent.MsgAttachments) > 0 { var att []MessageAttachment - for _, a := range msg.Attachments { + for _, a := range msgEvent.MsgAttachments { att = append(att, MessageAttachment{ ID: a.ID, URL: a.URL, @@ -268,15 +288,15 @@ func (b *bolt) getRemainingTimeout(timeout time.Time) string { } // checks if the author of msg has the correct role to run the requested command -func (b *bolt) roleCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (bool, error) { +func (b *bolt) roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) { var found bool //loop thru author roles, there may be a better way to check for this UNION //TODO: improve role search performance to support bigger lists - for _, r := range msg.Member.Roles { + for _, r := range roles { //get role name from ID - n, err := s.State.Role(msg.GuildID, r) + n, err := s.State.Role(guild, r) if err != nil { - return false, fmt.Errorf("failed to get role from ID %s\n%e", msg.GuildID, err) + return false, fmt.Errorf("failed to get role from ID %s\n%e", guild, err) } //does this role exist in command roles check := slices.Contains(run.Roles, n.Name) @@ -286,13 +306,8 @@ func (b *bolt) roleCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (boo } } - //can't find role, don't run command, alert user of missing permissions + //can't find role, don't run command if !found { - reply := b.createReply("you do not have permissions to run that command", msg.ID, msg.ChannelID, msg.GuildID) - _, err := s.ChannelMessageSendComplex(msg.ChannelID, reply) - if err != nil { - return false, fmt.Errorf("failed to send permission response: %e", err) - } return false, nil } @@ -300,11 +315,11 @@ func (b *bolt) roleCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (boo } // check if the command timeout has been met, responding with remaining time if timeout has not been met yet. -func (b *bolt) timeoutCheck(msg *dg.MessageCreate, s *dg.Session, run Command) (bool, error) { +func (b *bolt) timeoutCheck(msgID, channelID, guildID string, s *dg.Session, run Command) (bool, error) { wait := run.lastRun.Add(run.Timeout) if !time.Now().After(wait) { - reply := b.createReply(fmt.Sprintf("that command cannot be run for another %s", b.getRemainingTimeout(wait)), msg.ID, msg.ChannelID, msg.GuildID) - _, err := s.ChannelMessageSendComplex(msg.ChannelID, reply) + reply := b.createReply(fmt.Sprintf("that command cannot be run for another %s", b.getRemainingTimeout(wait)), msgID, channelID, guildID) + _, err := s.ChannelMessageSendComplex(channelID, reply) if err != nil { return false, fmt.Errorf("failed to send timeout response: %e", err) } diff --git a/message.go b/message.go index c08093c..6d828f7 100644 --- a/message.go +++ b/message.go @@ -1,11 +1,15 @@ package bolt -import "fmt" +import ( + "fmt" -//built-in Discord reactions + dg "github.com/bwmarrin/discordgo" +) + +// built-in Discord reactions type Reaction string -//a few easy-to-use emojis, Discordgo/Discord API requires them to be saved like this. +// a few easy-to-use emojis, Discordgo/Discord API requires them to be saved like this. const ( ReactionThumbsUp Reaction = "👍" ReactionThumbsDown Reaction = "👎" @@ -39,6 +43,20 @@ const ( ReactionDragon Reaction = "🐉" ) +// struct containing message event fields to prevent passing MessageCreate events and holding up routines +type MessageCreateEvent struct { + AuthorUsername string + AuthorID string + AuthorRoles []string + MsgID string + Msg string + MsgChanID string + MsgChanName string + MsgGuildID string + MsgGuildName string + MsgAttachments []*dg.MessageAttachment +} + // information about attachments to messages type MessageAttachment struct { ID string