init commit

This commit is contained in:
jake 2025-06-04 16:00:53 -04:00
parent a172762edc
commit e36e178f45
5 changed files with 217 additions and 1 deletions

View File

@ -1,3 +1,6 @@
# bolt
Discord bot base framework
Base Discord bot framework
## Introduction
bolt is a wrapper for Discordgo to provide very quick and easy setup for simple Discord bots. The only code required to run bolt is the command handler functions, this provides developers with the ability to have text-based commands on a Discord server without all the bootstrapping and setup usually required.

160
bolt.go Normal file
View File

@ -0,0 +1,160 @@
package bolt
import (
"fmt"
"log"
"os"
"strings"
"time"
dg "github.com/bwmarrin/discordgo"
)
const (
TOKEN_ENV_VAR = "DISCORD_TOKEN" //label for token environment variable
BOT_INTENTS = dg.IntentGuildMembers | //discord permissions for the bot
dg.IntentGuildPresences |
dg.IntentMessageContent
)
// basic bot structure containing discordgo connection as well as the command map
type bolt struct {
*dg.Session //holds discordgo internals
Commands map[string]Command //maps trigger phrase to command struct for fast lookup
}
type Bolt interface {
Start() error
Stop() error
AddCommands(cmd Command)
messageHandler(s *dg.Session, msg *dg.MessageCreate)
createReply(content, message, channel, guild string) *dg.MessageSend
}
// setup
func init() {
//validate environment variables
_, check := os.LookupEnv(TOKEN_ENV_VAR)
if !check {
log.Fatalf("the %s environment variable must be set", TOKEN_ENV_VAR)
}
}
// create a new bolt interface
func New() *bolt {
bot, err := dg.New(fmt.Sprintf("Bot %s", os.Getenv(TOKEN_ENV_VAR)))
if err != nil {
log.Fatal(err)
}
bot.Identify.Intents = BOT_INTENTS
return &bolt{
Session: bot,
}
}
// adds command handler and starts the bot
func (b *bolt) Start() error {
//register commands and open connection
b.AddHandler(b.messageHandler)
return b.Open()
}
// stops the bot
func (b *bolt) Stop() error {
return b.Close()
}
// adds commands to bot command map for use
func (b *bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
b.Commands[c.Trigger] = c
}
}
// handler function that parses message data and executes any command payloads
func (b *bolt) messageHandler(s *dg.Session, msg *dg.MessageCreate) {
//get server information
server, err := s.Guild(msg.GuildID)
if err != nil {
log.Println(err)
return
}
channel, err := s.Channel(msg.ChannelID)
if err != nil {
log.Println(err)
return
}
//if there is no content it is likely an image or a GIF, updating message content for
//better logging and to avoid confusion
if len(msg.Content) == 0 {
msg.Content = "GIF/IMAGE"
}
//log message
log.Printf("< %s | %s | %s > %s\n", server.Name, channel.Name, msg.Author.Username, msg.Content)
//the bot will ignore it's own messages to prevent command loops
if msg.Author.ID == s.State.User.ID {
return
}
//does the message have the command indicator "."
if msg.Content[:1] == "." {
words := strings.Split(msg.Content, " ")
run, ok := b.Commands[words[0]]
if !ok {
return //command doesn't exist, maybe log or respond to author
}
//has command met its timeout requirements
if !time.Now().After(run.LastRun.Add(run.Timeout)) {
return //running too soon, maybe respond letting know time remaining
}
//run command payload
res, err := run.Payload(Message{
Author: Author{
Username: msg.Author.Username,
Roles: msg.Member.Roles,
},
Words: words,
Message: msg.Content,
Channel: channel.Name,
Server: server.Name,
})
if err != nil {
log.Println(err)
return
}
//if a reply is returned send back to Discord
if res != "" {
reply := b.createReply(res, msg.ID, msg.ChannelID, msg.GuildID)
_, err := s.ChannelMessageSendComplex(msg.ChannelID, reply)
if err != nil {
log.Println(err)
return
}
}
}
}
// basic wrapper function to create easy Discord responses
func (b *bolt) createReply(content, message, channel, guild string) *dg.MessageSend {
details := &dg.MessageReference{
MessageID: message,
ChannelID: channel,
GuildID: guild,
}
return &dg.MessageSend{
Content: content,
Reference: details,
}
}

30
command.go Normal file
View File

@ -0,0 +1,30 @@
package bolt
import "time"
type Command struct {
Trigger string //.command that triggers payload NOT including the '.'
Payload Payload //payload function to run when a command is detected
Timeout time.Duration //the amount of time before command can run again
LastRun time.Time //timestamp of last command run
Roles []string //roles that can use command
}
// payload function type handling commands. The returned error is parsed and, if no error,
// is detected then the response string (res) will be sent in response to the command message
type Payload func(msg Message) (res string, err error)
// contains the basic information needed for a message command
type Message struct {
Author Author
Words []string //words from message split on whitespace
Message string //entire message content
Channel string //channel message was sent in
Server string //guild message was sent in
}
// username and roles of message authors
type Author struct {
Username string
Roles []string
}

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module code.jakeyoungdev.com/jake/bolt
go 1.24.0
require github.com/bwmarrin/discordgo v0.29.0
require (
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=