Merge pull request #1 from QPixel/feature/databases

(feature) Database support
This commit is contained in:
vel 2021-08-21 19:50:51 -07:00 committed by GitHub
commit fc702ed425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 105 deletions

58
core.go
View File

@ -1,12 +1,10 @@
package framework package framework
import ( import (
"fmt"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
tlog "github.com/ubergeek77/tinylog" tlog "github.com/ubergeek77/tinylog"
"os" "os"
"os/signal" "os/signal"
"runtime"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -15,9 +13,9 @@ import (
// core.go // core.go
// This file contains the main code responsible for driving core bot functionality // This file contains the main code responsible for driving core bot functionality
// messageState // MessageState
// Tells discordgo the amount of messages to cache // Tells discordgo the amount of messages to cache
var messageState = 500 var MessageState = 500
// log // log
// The logger for the core bot // The logger for the core bot
@ -56,8 +54,20 @@ var ColorSuccess = 0x55F485
var ColorFailure = 0xF45555 var ColorFailure = 0xF45555
// BotPresence // BotPresence
// Presence data to send when the bot is logging in
var botPresence discordgo.GatewayStatusUpdate var botPresence discordgo.GatewayStatusUpdate
// initProvider
// Stores and allows for the calling of the chosen GuildProvider
var initProvider func() GuildProvider
// SetInitProvider
// Sets the init provider
func SetInitProvider(provider func() GuildProvider) {
initProvider = provider
return
}
// SetPresence // SetPresence
// Sets the gateway field for bot presence // Sets the gateway field for bot presence
func SetPresence(presence discordgo.GatewayStatusUpdate) { func SetPresence(presence discordgo.GatewayStatusUpdate) {
@ -99,34 +109,16 @@ func IsCommand(trigger string) bool {
return false return false
} }
// dgoLog
// Allows for discordgo to call tinylog
func dgoLog(msgL, caller int, format string, a ...interface{}) {
pc, file, line, _ := runtime.Caller(caller)
files := strings.Split(file, "/")
file = files[len(files)-1]
name := runtime.FuncForPC(pc).Name()
fns := strings.Split(name, ".")
name = fns[len(fns)-1]
msg := fmt.Sprintf(format, a...)
switch msgL {
case discordgo.LogError:
dlog.Errorf("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogWarning:
dlog.Warningf("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogInformational:
dlog.Infof("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogDebug:
dlog.Debugf("%s:%d:%s() %s", file, line, name, msg)
}
}
// Start the bot. // Start the bot.
func Start() { func Start() {
discordgo.Logger = dgoLog discordgo.Logger = dgoLog
// Load all the guilds // Load all the guilds
loadGuilds() if initProvider == nil {
log.Fatalf("You have not chosen a database provider. Please refer to the docs")
}
currentProvider = initProvider()
Guilds = loadGuilds()
// We need a token // We need a token
if botToken == "" { if botToken == "" {
@ -141,12 +133,14 @@ func Start() {
log.Fatalf("Failed to create Discord session: %s", err) log.Fatalf("Failed to create Discord session: %s", err)
} }
// Setup State specific variables // Setup State specific variables
Session.State.MaxMessageCount = messageState Session.State.MaxMessageCount = MessageState
Session.LogLevel = discordgo.LogWarning Session.LogLevel = discordgo.LogWarning
Session.SyncEvents = false Session.SyncEvents = false
Session.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll) Session.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll)
// Set the bots status // Set the bots status
Session.Identify.Presence = botPresence Session.Identify.Presence = botPresence
// Open the session // Open the session
log.Info("Connecting to Discord...") log.Info("Connecting to Discord...")
err = Session.Open() err = Session.Open()
@ -155,13 +149,13 @@ func Start() {
} }
// Add the commandHandler to the list of user-defined handlers // Add the commandHandler to the list of user-defined handlers
AddHandler(commandHandler) AddDGOHandler(commandHandler)
// Add the slash command handler to the list of user-defined handlers // Add the slash command handler to the list of user-defined handlers
AddHandler(handleInteraction) AddDGOHandler(handleInteraction)
// Add the handlers to the session // Add the handlers to the session
addHandlers() addDGoHandlers()
// Log that the login succeeded // Log that the login succeeded
log.Infof("Bot logged in as \"" + Session.State.Ready.User.Username + "#" + Session.State.Ready.User.Discriminator + "\"") log.Infof("Bot logged in as \"" + Session.State.Ready.User.Username + "#" + Session.State.Ready.User.Discriminator + "\"")

5
go.mod
View File

@ -9,3 +9,8 @@ require (
github.com/ubergeek77/tinylog v1.0.0 github.com/ubergeek77/tinylog v1.0.0
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68
) )
require (
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
)

View File

@ -28,6 +28,14 @@ type GuildInfo struct {
WhitelistIds []string `json:"whitelist_ids"` WhitelistIds []string `json:"whitelist_ids"`
} }
//GuildProvider
// Type that holds functions that can be easily modified to support a wide range
// of storage types
type GuildProvider struct {
Save func(guild *Guild)
Load func() map[string]*Guild
}
// Guild // Guild
// The definition of a guild, which is simply its ID and Info // The definition of a guild, which is simply its ID and Info
type Guild struct { type Guild struct {
@ -41,6 +49,11 @@ type Guild struct {
// Otherwise, there will be information desync // Otherwise, there will be information desync
var Guilds = make(map[string]*Guild) var Guilds = make(map[string]*Guild)
// currentProvider
// A reference to a struct of functions that provides the guild info system with a database
// Or similar system to save guild data.
var currentProvider GuildProvider
// getGuild // getGuild
// Return a Guild object corresponding to the given guildId // Return a Guild object corresponding to the given guildId
// If the guild doesn't exist, initialize a new guild and save it before returning // If the guild doesn't exist, initialize a new guild and save it before returning
@ -90,7 +103,7 @@ func getGuild(guildId string) *Guild {
// Add the new guild to the map of guilds // Add the new guild to the map of guilds
Guilds[guildId] = &newGuild Guilds[guildId] = &newGuild
// Save the guild to .json // Save the guild to database
// A failed save is fatal, so we can count on this being successful // A failed save is fatal, so we can count on this being successful
newGuild.save() newGuild.save()
@ -101,6 +114,18 @@ func getGuild(guildId string) *Guild {
} }
} }
// loadGuilds
// Load all known guilds from the database
func loadGuilds() map[string]*Guild {
return currentProvider.Load()
}
// save
// saves guild data to the database
func (g *Guild) save() {
currentProvider.Save(g)
}
// GetMember // GetMember
// Convenience function to get a member in this guild // Convenience function to get a member in this guild
// This function handles cleaning of the string so you don't have to // This function handles cleaning of the string so you don't have to
@ -674,7 +699,7 @@ func (g *Guild) EnableCommandInChannel(command string, channelId string) error {
// DisableCommandInChannel // DisableCommandInChannel
// Given a command and channel ID, add that command to that channel's list of blocked commands // Given a command and channel ID, add that command to that channel's list of blocked commands
func (g *Guild) DisableTriggerInChannel(command string, channelId string) error { func (g *Guild) DisableCommandInChannel(command string, channelId string) error {
cleanedId := CleanId(channelId) cleanedId := CleanId(channelId)
if cleanedId == "" { if cleanedId == "" {
return errors.New("provided channel ID is invalid") return errors.New("provided channel ID is invalid")
@ -716,7 +741,7 @@ func (g *Guild) SetResponseChannel(channelId string) error {
} }
// Kick // Kick
// Kick a member // Kicks a member
func (g *Guild) Kick(userId string, reason string) error { func (g *Guild) Kick(userId string, reason string) error {
// Make sure the member exists // Make sure the member exists
member, err := g.GetMember(userId) member, err := g.GetMember(userId)
@ -733,7 +758,7 @@ func (g *Guild) Kick(userId string, reason string) error {
} }
// Ban // Ban
// Ban a user, who may not be a member // Bans a user, who may not be a member
func (g *Guild) Ban(userId string, reason string, deleteDays int) error { func (g *Guild) Ban(userId string, reason string, deleteDays int) error {
// Make sure the USER exists, because they may not be a member // Make sure the USER exists, because they may not be a member
user, err := GetUser(userId) user, err := GetUser(userId)

View File

@ -1,29 +1,29 @@
package framework package framework
// handlers.go // handlers.go
// Everything required for commands to pass their own handlers to discordgo // Everything required for commands to pass their own handlers to discordgo and the framework itself.
// handlers // handlers
// This list stores all the handlers that can be added to the bot // This list stores all the handlers that can be added to the bot
// It's basically a passthroughs for discordgo.AddHandler, but having a list // It's basically a passthroughs for discordgo.AddHandler, but having a list
// allows them to be collected ahead of time and then added all at once // allows them to be collected ahead of time and then added all at once
var handlers []interface{} var dGOHandlers []interface{}
// AddHandler // AddDGOHandler
// This provides a way for commands to pass handler functions through to discorgo, // This provides a way for commands to pass handler functions through to discordgo,
// and have them added properly during bot startup // and have them added properly during bot startup
func AddHandler(handler interface{}) { func AddDGOHandler(handler interface{}) {
handlers = append(handlers, handler) dGOHandlers = append(dGOHandlers, handler)
} }
// addHandlers // addHandlers
// Given all the handlers that have been pre-added to the handlers list, add them to the discordgo session // Given all the handlers that have been pre-added to the handlers list, add them to the discordgo session
func addHandlers() { func addDGoHandlers() {
if len(handlers) == 0 { if len(dGOHandlers) == 0 {
return return
} }
for _, handler := range handlers { for _, handler := range dGOHandlers {
Session.AddHandler(handler) Session.AddHandler(handler)
} }
} }

View File

@ -1,10 +1,12 @@
//go:build windows //go:build windows
// +build windows // +build windows
package framework package fs
import ( import (
"encoding/json" "encoding/json"
"github.com/qpixel/framework"
tlog "github.com/ubergeek77/tinylog"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"io/ioutil" "io/ioutil"
"os" "os"
@ -16,10 +18,12 @@ import (
// fs.go // fs.go
// This file contains functions that pertain to interacting with the filesystem, including mutex locking of files // This file contains functions that pertain to interacting with the filesystem, including mutex locking of files
var log = tlog.NewTaggedLogger("BotCore", tlog.NewColor("38;5;111"))
// GuildsDir // GuildsDir
// The directory to use for reading and writing guild .json files. Defaults to ./guilds // The directory to use for reading and writing guild .json files. Defaults to ./guilds
// todo abstract this into a database module (being completed in feature/database) // todo abstract this into a database module (being completed in feature/database)
var GuildsDir = "" var GuildsDir = "./guilds"
// saveLock // saveLock
// A map that stores mutexes for each guild, which will be locked every time that guild's data is written // A map that stores mutexes for each guild, which will be locked every time that guild's data is written
@ -66,7 +70,7 @@ func loadGuilds() {
// - Add up to at least 17 characters (it must be a Discord snowflake) // - Add up to at least 17 characters (it must be a Discord snowflake)
// - Are all numbers // - Are all numbers
guildId := strings.Split(fName, ".json")[0] guildId := strings.Split(fName, ".json")[0]
if len(guildId) < 17 || guildId != EnsureNumbers(guildId) { if len(guildId) < 17 || guildId != framework.EnsureNumbers(guildId) {
continue continue
} }
@ -88,7 +92,7 @@ func loadGuilds() {
} }
// Unmarshal the json // Unmarshal the json
var gInfo GuildInfo var gInfo framework.GuildInfo
err = json.Unmarshal(jsonBytes, &gInfo) err = json.Unmarshal(jsonBytes, &gInfo)
if err != nil { if err != nil {
log.Errorf("Failed to unmarshal \"%s\"; guild %s WILL NOT be loaded! (%s)", fPath, guildId, err) log.Errorf("Failed to unmarshal \"%s\"; guild %s WILL NOT be loaded! (%s)", fPath, guildId, err)
@ -96,29 +100,29 @@ func loadGuilds() {
} }
// Add the loaded guild to the map // Add the loaded guild to the map
Guilds[guildId] = &Guild{ framework.Guilds[guildId] = &framework.Guild{
ID: guildId, ID: guildId,
Info: gInfo, Info: gInfo,
} }
} }
if len(Guilds) == 0 { if len(framework.Guilds) == 0 {
log.Warningf("There are no guilds to load; data for new guilds will be saved to \"%s\"", GuildsDir) log.Warningf("There are no guilds to load; data for new guilds will be saved to \"%s\"", GuildsDir)
return return
} }
// :) // :)
plural := "" plural := ""
if len(Guilds) != 1 { if len(framework.Guilds) != 1 {
plural = "s" plural = "s"
} }
log.Infof("Loaded %d guild%s", len(Guilds), plural) log.Infof("Loaded %d guild%s", len(framework.Guilds), plural)
} }
// save // save
// Save a given guild object to .json // Save a given guild object to .json
func (g *Guild) save() { func save(g *framework.Guild) {
// See if a mutex exists for this guild, and create if not // See if a mutex exists for this guild, and create if not
if _, ok := saveLock[g.ID]; !ok { if _, ok := saveLock[g.ID]; !ok {
saveLock[g.ID] = &sync.Mutex{} saveLock[g.ID] = &sync.Mutex{}
@ -153,24 +157,9 @@ func (g *Guild) save() {
} }
} }
// ReadDefaults func InitProvider() framework.GuildProvider {
// TODO: WRITE DOCUMENTATION FOR THIS LMAO return framework.GuildProvider{
func ReadDefaults(filePath string) (result []string) { Save: save,
fPath := path.Clean(filePath) Load: loadGuilds,
if _, existErr := os.Stat(fPath); os.IsNotExist(existErr) {
log.Errorf("Failed to find \"%s\"; File WILL NOT be loaded! (%s)", fPath, existErr)
return
} }
jsonBytes, err := ioutil.ReadFile(fPath)
if err != nil {
log.Errorf("Failed to read \"%s\"; File WILL NOT be loaded! (%s)", fPath, err)
return
}
err = json.Unmarshal(jsonBytes, &result)
if err != nil {
log.Errorf("Failed to unmarshal \"%s\"; File WILL NOT be loaded! (%s)", fPath, err)
}
return
} }

View File

@ -1,10 +1,12 @@
//go:build darwin || linux //go:build darwin || linux
// +build darwin linux // +build darwin linux
package framework package fs
import ( import (
"encoding/json" "encoding/json"
"github.com/qpixel/framework"
tlog "github.com/ubergeek77/tinylog"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"io/ioutil" "io/ioutil"
"os" "os"
@ -16,10 +18,12 @@ import (
// fs.go // fs.go
// This file contains functions that pertain to interacting with the filesystem, including mutex locking of files // This file contains functions that pertain to interacting with the filesystem, including mutex locking of files
var log = tlog.NewTaggedLogger("BotCore", tlog.NewColor("38;5;111"))
// GuildsDir // GuildsDir
// The directory to use for reading and writing guild .json files. Defaults to ./guilds // The directory to use for reading and writing guild .json files. Defaults to ./guilds
// todo remind me to abstract this into a database // todo abstract this into a database module (being completed in feature/database)
var GuildsDir = "" var GuildsDir = "./guilds"
// saveLock // saveLock
// A map that stores mutexes for each guild, which will be locked every time that guild's data is written // A map that stores mutexes for each guild, which will be locked every time that guild's data is written
@ -28,7 +32,7 @@ var saveLock = make(map[string]*sync.Mutex)
// loadGuilds // loadGuilds
// Load all known guilds from the filesystem, from inside GuildsDir // Load all known guilds from the filesystem, from inside GuildsDir
func loadGuilds() { func loadGuilds() (guilds map[string]*framework.Guild) {
// Check if the configured guild directory exists, and create it if otherwise // Check if the configured guild directory exists, and create it if otherwise
if _, existErr := os.Stat(GuildsDir); os.IsNotExist(existErr) { if _, existErr := os.Stat(GuildsDir); os.IsNotExist(existErr) {
mkErr := os.MkdirAll(GuildsDir, 0755) mkErr := os.MkdirAll(GuildsDir, 0755)
@ -38,10 +42,11 @@ func loadGuilds() {
log.Warningf("There are no Guilds to load; data for new Guilds will be saved to: %s", GuildsDir) log.Warningf("There are no Guilds to load; data for new Guilds will be saved to: %s", GuildsDir)
// There are no guilds to load, so we can return early // There are no guilds to load, so we can return early
return return guilds
} }
// Get a list of files in the directory // Get a list of files in the directory
guilds = make(map[string]*framework.Guild)
files, rdErr := ioutil.ReadDir(GuildsDir) files, rdErr := ioutil.ReadDir(GuildsDir)
if rdErr != nil { if rdErr != nil {
log.Fatalf("Failed to read guild directory: %s", rdErr) log.Fatalf("Failed to read guild directory: %s", rdErr)
@ -66,7 +71,7 @@ func loadGuilds() {
// - Add up to at least 17 characters (it must be a Discord snowflake) // - Add up to at least 17 characters (it must be a Discord snowflake)
// - Are all numbers // - Are all numbers
guildId := strings.Split(fName, ".json")[0] guildId := strings.Split(fName, ".json")[0]
if len(guildId) < 17 || guildId != EnsureNumbers(guildId) { if len(guildId) < 17 || guildId != framework.EnsureNumbers(guildId) {
continue continue
} }
@ -86,7 +91,7 @@ func loadGuilds() {
} }
// Unmarshal the json // Unmarshal the json
var gInfo GuildInfo var gInfo framework.GuildInfo
err = json.Unmarshal(jsonBytes, &gInfo) err = json.Unmarshal(jsonBytes, &gInfo)
if err != nil { if err != nil {
log.Errorf("Failed to unmarshal \"%s\"; guild %s WILL NOT be loaded! (%s)", fPath, guildId, err) log.Errorf("Failed to unmarshal \"%s\"; guild %s WILL NOT be loaded! (%s)", fPath, guildId, err)
@ -94,29 +99,30 @@ func loadGuilds() {
} }
// Add the loaded guild to the map // Add the loaded guild to the map
Guilds[guildId] = &Guild{ guilds[guildId] = &framework.Guild{
ID: guildId, ID: guildId,
Info: gInfo, Info: gInfo,
} }
} }
if len(Guilds) == 0 { if len(guilds) == 0 {
log.Warningf("There are no guilds to load; data for new guilds will be saved to \"%s\"", GuildsDir) log.Warningf("There are no guilds to load; data for new guilds will be saved to \"%s\"", GuildsDir)
return return guilds
} }
// :) // :)
plural := "" plural := ""
if len(Guilds) != 1 { if len(framework.Guilds) != 1 {
plural = "s" plural = "s"
} }
log.Infof("Loaded %d guild%s", len(Guilds), plural) log.Infof("Loaded %d guild%s", len(guilds), plural)
return guilds
} }
// save // save
// Save a given guild object to .json // Save a given guild object to .json
func (g *Guild) save() { func save(g *framework.Guild) {
// See if a mutex exists for this guild, and create if not // See if a mutex exists for this guild, and create if not
if _, ok := saveLock[g.ID]; !ok { if _, ok := saveLock[g.ID]; !ok {
saveLock[g.ID] = &sync.Mutex{} saveLock[g.ID] = &sync.Mutex{}
@ -151,24 +157,11 @@ func (g *Guild) save() {
} }
} }
// ReadDefaults // InitProvider
// TODO: WRITE DOCUMENTATION FOR THIS LMAO // Inits the filesystem provider
func ReadDefaults(filePath string) (result []string) { func InitProvider() framework.GuildProvider {
fPath := path.Clean(filePath) return framework.GuildProvider{
if _, existErr := os.Stat(fPath); os.IsNotExist(existErr) { Save: save,
log.Errorf("Failed to find \"%s\"; File WILL NOT be loaded! (%s)", fPath, existErr) Load: loadGuilds,
return
} }
jsonBytes, err := ioutil.ReadFile(fPath)
if err != nil {
log.Errorf("Failed to read \"%s\"; File WILL NOT be loaded! (%s)", fPath, err)
return
}
err = json.Unmarshal(jsonBytes, &result)
if err != nil {
log.Errorf("Failed to unmarshal \"%s\"; File WILL NOT be loaded! (%s)", fPath, err)
}
return
} }

24
util.go
View File

@ -6,6 +6,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
) )
@ -355,3 +356,26 @@ func FindAllString(re *regexp2.Regexp, s string) []string {
} }
return matches return matches
} }
// dgoLog
// Allows for discordgo to call tinylog
func dgoLog(msgL, caller int, format string, a ...interface{}) {
pc, file, line, _ := runtime.Caller(caller)
files := strings.Split(file, "/")
file = files[len(files)-1]
name := runtime.FuncForPC(pc).Name()
fns := strings.Split(name, ".")
name = fns[len(fns)-1]
msg := fmt.Sprintf(format, a...)
switch msgL {
case discordgo.LogError:
dlog.Errorf("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogWarning:
dlog.Warningf("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogInformational:
dlog.Infof("%s:%d:%s() %s", file, line, name, msg)
case discordgo.LogDebug:
dlog.Debugf("%s:%d:%s() %s", file, line, name, msg)
}
}