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
verbose log level which will log everything for debugging purposes, and registers Discord Intents for message and admin
related permissions. This allows the bot to parse messages, send them, delete them, etc. as well as timeout and mute users.
verbose log level which will log everything for debugging purposes, and registers Discord Intents for message and
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:
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
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
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
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
auto-moderation from the bot without manual intervention. The example message handler does two arbitrary things to demo functionality:
A message handler is also registered in this example, message handlers are used to handle messages that do not contain a
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
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
)
type bolt struct {
type Bolt struct {
//discordgo internals
*dg.Session
//maps trigger phrase to command struct for instant lookup
@@ -54,24 +54,8 @@ type bolt struct {
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
func New(opts ...Option) (Bolt, error) {
func New(opts ...Option) (*Bolt, error) {
_, check := os.LookupEnv(TOKEN_ENV_VAR)
if !check {
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)
}
b := &bolt{
b := &Bolt{
Session: bot,
commands: make(map[string]Command, 0),
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
// 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.
func (b *bolt) Start() error {
func (b *Bolt) Start() error {
b.AddHandler(b.msgEventHandler)
err := b.Open()
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
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
closeChan := make(chan struct{}, 0)
closeChan := make(chan struct{})
go func() {
b.wg.Wait()
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
// to the handler
func (b *bolt) AddCommands(cmd ...Command) {
func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
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
// to the handler
func (b *bolt) AddMessageHandler(p Payload) {
func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p
}
// stop closes the websocket connection to Discord
func (b *bolt) stop() error {
func (b *Bolt) stop() error {
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
// 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.
func (b *bolt) msgEventHandler(s *dg.Session, msg *dg.MessageCreate) {
func (b *Bolt) msgEventHandler(s *dg.Session, msg *dg.MessageCreate) {
//get server information
server, err := s.Guild(msg.GuildID)
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
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{
Author: Author{
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
func (b *bolt) handleMessage(event *Message) error {
func (b *Bolt) handleMessage(event *Message) error {
if b.msgHandlerf != nil {
return b.msgHandlerf(&Context{
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
// response is sent to the message
// 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:]]
if !ok {
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
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{
MessageID: message,
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
// 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)
var (
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
// 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
//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
@@ -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
// 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)
now := time.Now()
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
type Context struct {
Message *Message
bolt *bolt
bolt *Bolt
}
// React applies the reaction to the message

View File

@@ -4,7 +4,7 @@ import (
dg "github.com/bwmarrin/discordgo"
)
type Option func(b *bolt)
type Option func(b *Bolt)
type LogLevel int
const (
@@ -16,7 +16,7 @@ const (
// 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
func WithIntents(intents ...dg.Intent) Option {
return func(b *bolt) {
return func(b *Bolt) {
var full dg.Intent
for _, i := range intents {
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
// may cause higher latency but may reduce resources needed to run bolt
func WithMaxGoroutines(max int) Option {
return func(b *bolt) {
return func(b *Bolt) {
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
// command, for example "." or "!"
func WithIndicator(i string) Option {
return func(b *bolt) {
return func(b *Bolt) {
b.indicator = i
}
}
// WithLogLevel adjusts bolt logging verbosity
func WithLogLevel(lvl LogLevel) Option {
return func(b *bolt) {
return func(b *Bolt) {
b.logLvl = lvl
}
}