2 Commits
v0.6.0 ... main

Author SHA1 Message Date
da366509d3 fixing pkg site docs 2026-05-21 17:52:36 -04:00
ef441d3281 README update 2026-03-01 00:26:39 -05:00
4 changed files with 32 additions and 44 deletions

View File

@@ -21,17 +21,21 @@ import (
/* /*
A basic example of a bot with two commands and a general message handler for non-command messages. The bot uses a A basic example of a bot with two commands and a general message handler for non-command messages. The bot uses a
verbose log level which will log everything for debugging purposes, and registers Discord Intents for message and admin verbose log level which will log everything for debugging purposes, and registers Discord Intents for message and
related permissions. This allows the bot to parse messages, send them, delete them, etc. as well as timeout and mute users. admin related permissions. This allows the bot to parse messages, send them, delete them, etc. as well as timeout
and mute users.
This example registers three commands: This example registers three commands:
1. .ping - a basic ping/pong command that can be run by anyone at any time 1. .ping - a basic ping/pong command that can be run by anyone at any time
2. .wait - a dummy command that replies "okay" it can only be run by users with the "user" role and can only be ran once every 25 seconds 2. .wait - a dummy command that replies "okay" it can only be run by users with the "user" role and can only be ran
3. .timeout - a admin command that can only be run by users with an "admin" role, this command will timeout any mentioned users for 5 minutes once every 25 seconds
3. .timeout - a admin command that can only be run by users with an "admin" role, this command will timeout any mentioned
users for 5 minutes
A message handler is also registered in this example, message handlers are used to handle messages that do not contain a command. This enables A message handler is also registered in this example, message handlers are used to handle messages that do not contain a
auto-moderation from the bot without manual intervention. The example message handler does two arbitrary things to demo functionality: command. This enables auto-moderation from the bot without manual intervention. The example message handler does two arbitrary
things to demo functionality:
1. Checks the message content for the phrase "swear word" and, if found, times the user out for 5 minutes 1. Checks the message content for the phrase "swear word" and, if found, times the user out for 5 minutes
2. Checks the message for the phrase "im going to yell in VoiceChat" and mutes the message author until Unmute is called on them 2. Checks the message for the phrase "im going to yell in VoiceChat" and mutes the message author until Unmute is called on them

48
bolt.go
View File

@@ -35,7 +35,7 @@ const (
dg.IntentGuildMessageReactions dg.IntentGuildMessageReactions
) )
type bolt struct { type Bolt struct {
//discordgo internals //discordgo internals
*dg.Session *dg.Session
//maps trigger phrase to command struct for instant lookup //maps trigger phrase to command struct for instant lookup
@@ -54,24 +54,8 @@ type bolt struct {
msgHandlerf Payload msgHandlerf Payload
} }
type Bolt interface {
Start() error
AddCommands(cmd ...Command)
AddMessageHandler(p Payload)
//filtered methods
stop() error
msgEventHandler(s *dg.Session, msg *dg.MessageCreate)
mapEventToMsg(msg *dg.MessageCreate, channel *dg.Channel, server *dg.Guild) Message
handleCommand(msgEvent *Message, lg int) error
handleMessage(event *Message) error
createReply(content, message, channel, guild string) *dg.MessageSend
remainingTimeout(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)
}
// New creates a new bolt instance and applies any supplied options // New creates a new bolt instance and applies any supplied options
func New(opts ...Option) (Bolt, error) { func New(opts ...Option) (*Bolt, error) {
_, check := os.LookupEnv(TOKEN_ENV_VAR) _, check := os.LookupEnv(TOKEN_ENV_VAR)
if !check { if !check {
return nil, fmt.Errorf("environment variable %s must be set", TOKEN_ENV_VAR) return nil, fmt.Errorf("environment variable %s must be set", TOKEN_ENV_VAR)
@@ -82,7 +66,7 @@ func New(opts ...Option) (Bolt, error) {
return nil, fmt.Errorf("failed to create Discord session: %e", err) return nil, fmt.Errorf("failed to create Discord session: %e", err)
} }
b := &bolt{ b := &Bolt{
Session: bot, Session: bot,
commands: make(map[string]Command, 0), commands: make(map[string]Command, 0),
logLvl: LogLevelAll, logLvl: LogLevelAll,
@@ -105,7 +89,7 @@ func New(opts ...Option) (Bolt, error) {
// Start applies the message event handler function to the bot and opens the initial websocket connection // Start applies the message event handler function to the bot and opens the initial websocket connection
// with Discord. Start is a blocking call that also handles safe shutdown, on Interrupt, bolt will give // with Discord. Start is a blocking call that also handles safe shutdown, on Interrupt, bolt will give
// command routines a window to finish before closing the connection. // command routines a window to finish before closing the connection.
func (b *bolt) Start() error { func (b *Bolt) Start() error {
b.AddHandler(b.msgEventHandler) b.AddHandler(b.msgEventHandler)
err := b.Open() err := b.Open()
if err != nil { if err != nil {
@@ -119,7 +103,7 @@ func (b *bolt) Start() error {
//give handler routines a 5 second window to finish processes before closing connection //give handler routines a 5 second window to finish processes before closing connection
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
closeChan := make(chan struct{}, 0) closeChan := make(chan struct{})
go func() { go func() {
b.wg.Wait() b.wg.Wait()
close(closeChan) close(closeChan)
@@ -136,7 +120,7 @@ func (b *bolt) Start() error {
// AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded // AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded
// to the handler // to the handler
func (b *bolt) AddCommands(cmd ...Command) { func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd { for _, c := range cmd {
b.commands[c.Trigger] = c b.commands[c.Trigger] = c
} }
@@ -144,19 +128,19 @@ func (b *bolt) AddCommands(cmd ...Command) {
// AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded // AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded
// to the handler // to the handler
func (b *bolt) AddMessageHandler(p Payload) { func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p b.msgHandlerf = p
} }
// stop closes the websocket connection to Discord // stop closes the websocket connection to Discord
func (b *bolt) stop() error { func (b *Bolt) stop() error {
return b.Close() return b.Close()
} }
// msgEventHandler handles the routing of messages to either the command or message handlers. If LogLvl is set, logging is handled here before // msgEventHandler handles the routing of messages to either the command or message handlers. If LogLvl is set, logging is handled here before
// the event is mapped to the Message struct and forwarded to the handlers. Each message is handled in its own goroutine, the max allowed goroutines // the event is mapped to the Message struct and forwarded to the handlers. Each message is handled in its own goroutine, the max allowed goroutines
// is set as a default and can be altered using options for better performance. // is set as a default and can be altered using options for better performance.
func (b *bolt) msgEventHandler(s *dg.Session, msg *dg.MessageCreate) { func (b *Bolt) msgEventHandler(s *dg.Session, msg *dg.MessageCreate) {
//get server information //get server information
server, err := s.Guild(msg.GuildID) server, err := s.Guild(msg.GuildID)
if err != nil { if err != nil {
@@ -209,7 +193,7 @@ func (b *bolt) msgEventHandler(s *dg.Session, msg *dg.MessageCreate) {
} }
// mapEventToMsg maps the Discord event message struct into bolt's user-friendly Message struct // mapEventToMsg maps the Discord event message struct into bolt's user-friendly Message struct
func (b *bolt) mapEventToMsg(msg *dg.MessageCreate, channel *dg.Channel, server *dg.Guild) Message { func (b *Bolt) mapEventToMsg(msg *dg.MessageCreate, channel *dg.Channel, server *dg.Guild) Message {
m := Message{ m := Message{
Author: Author{ Author: Author{
Name: msg.Author.Username, Name: msg.Author.Username,
@@ -256,7 +240,7 @@ func (b *bolt) mapEventToMsg(msg *dg.MessageCreate, channel *dg.Channel, server
} }
// handleMessage forwards the message data to the handler function, if one is set // handleMessage forwards the message data to the handler function, if one is set
func (b *bolt) handleMessage(event *Message) error { func (b *Bolt) handleMessage(event *Message) error {
if b.msgHandlerf != nil { if b.msgHandlerf != nil {
return b.msgHandlerf(&Context{ return b.msgHandlerf(&Context{
Message: event, Message: event,
@@ -271,7 +255,7 @@ func (b *bolt) handleMessage(event *Message) error {
// data to the handler after checking the timeout and role restrictions. If restrictions have not been met a generic // data to the handler after checking the timeout and role restrictions. If restrictions have not been met a generic
// response is sent to the message // response is sent to the message
// TODO: accept a string for timeout/role rejection messages to allow customization // TODO: accept a string for timeout/role rejection messages to allow customization
func (b *bolt) handleCommand(msg *Message, lg int) error { func (b *Bolt) handleCommand(msg *Message, lg int) error {
run, ok := b.commands[msg.Words[0][lg:]] run, ok := b.commands[msg.Words[0][lg:]]
if !ok { if !ok {
return nil //command doesn't exist return nil //command doesn't exist
@@ -321,7 +305,7 @@ func (b *bolt) handleCommand(msg *Message, lg int) error {
} }
// createReploy is a basic wrapper function to map message data to the actual MessageSend struct // createReploy is a basic wrapper function to map message data to the actual MessageSend struct
func (b *bolt) createReply(content, message, channel, guild string) *dg.MessageSend { func (b *Bolt) createReply(content, message, channel, guild string) *dg.MessageSend {
details := &dg.MessageReference{ details := &dg.MessageReference{
MessageID: message, MessageID: message,
ChannelID: channel, ChannelID: channel,
@@ -336,7 +320,7 @@ func (b *bolt) createReply(content, message, channel, guild string) *dg.MessageS
// remainingTimeout calculates the amount of time left before a command can be run again. Returning a string // remainingTimeout calculates the amount of time left before a command can be run again. Returning a string
// representing a readable version of the time left, example: 1h (1 hour) // representing a readable version of the time left, example: 1h (1 hour)
func (b *bolt) remainingTimeout(timeout time.Time) string { func (b *Bolt) remainingTimeout(timeout time.Time) string {
r := time.Until(timeout) r := time.Until(timeout)
var ( var (
timeLeft int timeLeft int
@@ -361,7 +345,7 @@ func (b *bolt) remainingTimeout(timeout time.Time) string {
// roleCheck loops through the provided user role ID's grabs the role "name" and ensures the role is present // roleCheck loops through the provided user role ID's grabs the role "name" and ensures the role is present
// in the commands whitelist. If the role exists in the Command whitelist a true response is returned // in the commands whitelist. If the role exists in the Command whitelist a true response is returned
func (b *bolt) roleCheck(guild string, roles []string, 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 var found bool
//this needs a bit of love, looping through roles could get messy as server and role lists grow on big servers //this needs a bit of love, looping through roles could get messy as server and role lists grow on big servers
//especially with the Role() call inside of the loop //especially with the Role() call inside of the loop
@@ -384,7 +368,7 @@ func (b *bolt) roleCheck(guild string, roles []string, s *dg.Session, run Comman
// timeoutCheck determines if a Command timeout has expired, if it hasn't expired a response is sent back to the message alerting the user // timeoutCheck determines if a Command timeout has expired, if it hasn't expired a response is sent back to the message alerting the user
// of the remaining time // of the remaining time
func (b *bolt) timeoutCheck(msgID, channelID, guildID string, 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) wait := run.lastRun.Add(run.Timeout)
now := time.Now() now := time.Now()
if !now.After(wait) && !now.Equal(wait) { if !now.After(wait) && !now.Equal(wait) {

View File

@@ -59,7 +59,7 @@ type Message struct {
// some methods to make interaction with the message as easy as possible // some methods to make interaction with the message as easy as possible
type Context struct { type Context struct {
Message *Message Message *Message
bolt *bolt bolt *Bolt
} }
// React applies the reaction to the message // React applies the reaction to the message

View File

@@ -4,7 +4,7 @@ import (
dg "github.com/bwmarrin/discordgo" dg "github.com/bwmarrin/discordgo"
) )
type Option func(b *bolt) type Option func(b *Bolt)
type LogLevel int type LogLevel int
const ( const (
@@ -16,7 +16,7 @@ const (
// WithIntents provides an option to use custom intents for the bot. Bolt comes preconfigured with the basic // WithIntents provides an option to use custom intents for the bot. Bolt comes preconfigured with the basic
// intents needed to run a bot but those are completely overwritten by any supplied here // intents needed to run a bot but those are completely overwritten by any supplied here
func WithIntents(intents ...dg.Intent) Option { func WithIntents(intents ...dg.Intent) Option {
return func(b *bolt) { return func(b *Bolt) {
var full dg.Intent var full dg.Intent
for _, i := range intents { for _, i := range intents {
full |= i full |= i
@@ -29,7 +29,7 @@ func WithIntents(intents ...dg.Intent) Option {
// WithMaxGoroutines limits the amount of handler routines the bot is able to spawn at the same time. A lower value // WithMaxGoroutines limits the amount of handler routines the bot is able to spawn at the same time. A lower value
// may cause higher latency but may reduce resources needed to run bolt // may cause higher latency but may reduce resources needed to run bolt
func WithMaxGoroutines(max int) Option { func WithMaxGoroutines(max int) Option {
return func(b *bolt) { return func(b *Bolt) {
b.maxRoutines = max b.maxRoutines = max
} }
} }
@@ -37,14 +37,14 @@ func WithMaxGoroutines(max int) Option {
// WithIndicator sets the substring that must be present at the beginning of a message to trigger a // WithIndicator sets the substring that must be present at the beginning of a message to trigger a
// command, for example "." or "!" // command, for example "." or "!"
func WithIndicator(i string) Option { func WithIndicator(i string) Option {
return func(b *bolt) { return func(b *Bolt) {
b.indicator = i b.indicator = i
} }
} }
// WithLogLevel adjusts bolt logging verbosity // WithLogLevel adjusts bolt logging verbosity
func WithLogLevel(lvl LogLevel) Option { func WithLogLevel(lvl LogLevel) Option {
return func(b *bolt) { return func(b *Bolt) {
b.logLvl = lvl b.logLvl = lvl
} }
} }