From eb64bdf8037321d5c3a1d761d2f8472d26932661 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 17 Aug 2025 20:37:22 -0400 Subject: [PATCH 1/2] starting research for vc support --- bolt.go | 131 +++++++++++++++++++++++++++++++++++------------------ message.go | 24 ++++++++-- 2 files changed, 108 insertions(+), 47 deletions(-) diff --git a/bolt.go b/bolt.go index 104096c..13f2c23 100644 --- a/bolt.go +++ b/bolt.go @@ -1,6 +1,7 @@ package bolt import ( + "errors" "fmt" "log" "os" @@ -38,11 +39,12 @@ 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) + joinUserVoice(guild, user string, s *dg.Session) (*dg.VoiceConnection, error) } // create a new bolt interface @@ -66,7 +68,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 +76,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 +111,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 +134,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 +149,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 +194,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 +290,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 +308,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 +317,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) } @@ -313,3 +330,29 @@ func (b *bolt) timeoutCheck(msg *dg.MessageCreate, s *dg.Session, run Command) ( return true, nil } + +func (b *bolt) joinUserVoice(guild, user string, s *dg.Session) (*dg.VoiceConnection, error) { + g, err := s.State.Guild(guild) + if err != nil { + return nil, err + } + + var chanID string + for _, x := range g.VoiceStates { + if user == x.UserID { + chanID = x.ChannelID + } + } + + if chanID == "" { + return nil, errors.New("user is not in a voice channel") + } + + //joining voice channel: false = not muted, true = deafened + dgv, err := s.ChannelVoiceJoin(guild, chanID, false, true) + if err != nil { + return nil, err + } + + return dgv, nil +} 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 From a310fd4533dd5d6e5ff7daf27113aa2e926197f1 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 17 Aug 2025 20:45:51 -0400 Subject: [PATCH 2/2] better async handling --- bolt.go | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/bolt.go b/bolt.go index 13f2c23..b4ef3fe 100644 --- a/bolt.go +++ b/bolt.go @@ -1,7 +1,6 @@ package bolt import ( - "errors" "fmt" "log" "os" @@ -44,7 +43,6 @@ type Bolt interface { getRemainingTimeout(timeout time.Time) string roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) timeoutCheck(msgID, channelID, guildID string, s *dg.Session, run Command) (bool, error) - joinUserVoice(guild, user string, s *dg.Session) (*dg.VoiceConnection, error) } // create a new bolt interface @@ -330,29 +328,3 @@ func (b *bolt) timeoutCheck(msgID, channelID, guildID string, s *dg.Session, run return true, nil } - -func (b *bolt) joinUserVoice(guild, user string, s *dg.Session) (*dg.VoiceConnection, error) { - g, err := s.State.Guild(guild) - if err != nil { - return nil, err - } - - var chanID string - for _, x := range g.VoiceStates { - if user == x.UserID { - chanID = x.ChannelID - } - } - - if chanID == "" { - return nil, errors.New("user is not in a voice channel") - } - - //joining voice channel: false = not muted, true = deafened - dgv, err := s.ChannelVoiceJoin(guild, chanID, false, true) - if err != nil { - return nil, err - } - - return dgv, nil -}