...
 
Commits (7)
......@@ -3,7 +3,7 @@ module gitlab.com/bclindner/valerius/v0.3
require (
github.com/alecthomas/gometalinter v2.0.12+incompatible // indirect
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/bclindner/iasipgenerator v0.0.0-20181215041843-992a05812300
github.com/bclindner/iasipgenerator v0.0.0-20181217195122-d337ee89954c
github.com/bwmarrin/discordgo v0.19.0
github.com/cosiner/argv v0.0.1 // indirect
github.com/davidrjenni/reftools v0.0.0-20180914123528-654d0ba4f96d // indirect
......@@ -32,9 +32,9 @@ require (
github.com/zmb3/gogetdoc v0.0.0-20181208215853-c5ca8f4d4936 // indirect
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/lint v0.0.0-20181212231659-93c0bb5c8393 // indirect
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect
golang.org/x/tools v0.0.0-20181214171254-3c39ce7b6105 // indirect
golang.org/x/tools v0.0.0-20181217182337-728ed46ae025 // indirect
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
......
......@@ -8,6 +8,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/bclindner/iasipgenerator v0.0.0-20181215041843-992a05812300 h1:y+bqlLqHWMy2HsaW0eeLOrJ+AGnAJxK7PuoQk85Ptok=
github.com/bclindner/iasipgenerator v0.0.0-20181215041843-992a05812300/go.mod h1:sqvGzUcCr3RHiTD+tB2vPDlkw4KgZKU6AJDnf1K/phU=
github.com/bclindner/iasipgenerator v0.0.0-20181217195122-d337ee89954c h1:PObpo7aM3l4G0wbCqYIfWFFp31DbtQzihrUV1Kpx8gk=
github.com/bclindner/iasipgenerator v0.0.0-20181217195122-d337ee89954c/go.mod h1:sqvGzUcCr3RHiTD+tB2vPDlkw4KgZKU6AJDnf1K/phU=
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY=
github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
github.com/cosiner/argv v0.0.1 h1:2iAFN+sWPktbZ4tvxm33Ei8VY66FPCxdOxpncUGpAXE=
......@@ -98,6 +100,7 @@ golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86h
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181212231659-93c0bb5c8393/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
......@@ -110,6 +113,7 @@ golang.org/x/tools v0.0.0-20181201035826-d0ca3933b724 h1:eV9myT/I6o1p8salzgZ0f1p
golang.org/x/tools v0.0.0-20181201035826-d0ca3933b724/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181214171254-3c39ce7b6105/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181217182337-728ed46ae025/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y=
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
......
......@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"github.com/bwmarrin/discordgo" // for running the bot
log "github.com/sirupsen/logrus" // logging suite
)
......@@ -19,7 +20,7 @@ type Command interface {
// Returns an error that the handler can log.
Run(*discordgo.Session, *discordgo.MessageCreate) error
// Checks if the command can be used on a given guild and channel ID.
Check(string, string) bool
Check(guildID string, channelID string, userID string) bool
}
// Checks if a list contains something.
......@@ -45,15 +46,21 @@ type BaseCommand struct {
// Optional channel whitelist.
// If set, only channels in this list can use this command.
ChannelWhitelist []string `json:"channelwhitelist"`
// Optional channel blacklist.
// Optional channel whitelist.
// If set, channels in this list cannot use this command.
ChannelBlacklist []string `json:"channelblacklist"`
// Optional guild blacklist.
// If set, guilds in this list cannot use this command.
GuildWhitelist []string `json:"guildblacklist"`
// Optional guild blacklist.
GuildWhitelist []string `json:"guildwhitelist"`
// Optional guild whitelist.
// If set, guilds in this list cannot use this command.
GuildBlacklist []string `json:"guildwhitelist"`
GuildBlacklist []string `json:"guildblacklist"`
// Optional user whitelist.
// If set, users in this list cannot use this command.
UserWhitelist []string `json:"userwhitelist"`
// Optional user blacklist.
// If set, users in this list cannot use this command.
UserBlacklist []string `json:"userblacklist"`
// JSON-encoded list of options for the command.
// This is intended to be parsed and handled by the "NewXCommand" factory function
// after utilizing this BaseCommand.
......@@ -71,7 +78,7 @@ func (b BaseCommand) GetType() string {
}
// Check ensures the command passes whitelist and blacklist checks.
func (b BaseCommand) Check(guildID, channelID string) bool {
func (b BaseCommand) Check(guildID, channelID, userID string) bool {
if len(b.ChannelWhitelist) > 0 && !listContains(b.ChannelWhitelist, channelID) {
return false
}
......@@ -84,6 +91,12 @@ func (b BaseCommand) Check(guildID, channelID string) bool {
if len(b.GuildBlacklist) > 0 && listContains(b.GuildBlacklist, guildID) {
return false
}
if len(b.UserWhitelist) > 0 && !listContains(b.UserWhitelist, userID) {
return false
}
if len(b.UserBlacklist) > 0 && listContains(b.UserBlacklist, userID) {
return false
}
return true
}
......@@ -93,6 +106,8 @@ type MessageHandler struct {
Handler
// List of commands to test.
commands []Command
// Command to disconnect the handler from the bot.
DestroySelf func()
}
// Handler is the interface for the bot message handler.
......@@ -103,10 +118,39 @@ type Handler interface {
}
// NewMessageHandler creates a new handler and binds it to a Session.
func NewMessageHandler(bot *discordgo.Session) *MessageHandler {
func NewMessageHandler(bot *discordgo.Session, commands []BaseCommand) (*MessageHandler, error) {
handler := MessageHandler{}
bot.AddHandler(handler.Handle)
return &handler
// set variables for use in the loop
var (
err error
cmd Command
)
// add handler commands
for _, config := range commands {
switch config.Type {
case "pingpong":
cmd, err = NewPingPongCommand(config)
case "iasip":
cmd, err = NewIASIPCommand(config)
case "rest":
cmd, err = NewRESTCommand(config)
case "reload":
cmd, err = NewReloadCommand(config)
default:
return &handler, errors.New("Command " + config.Name + " is of invalid type (" + config.Type + "). Exiting.")
}
// handle any errors
if err != nil {
return &handler, errors.New("Error with command " + config.Name + ": " + err.Error())
}
// add the command
handler.Add(cmd)
}
// log how many commands we parsed
log.Info("Parsed ", len(handler.commands), " commands")
// register self with the bot, and get the function necessary to detach from bot
handler.DestroySelf = bot.AddHandler(handler.Handle)
return &handler, nil
}
// Handle handles a Discord message. This just runs the Test() function of each command,
......@@ -126,27 +170,31 @@ func (c *MessageHandler) Handle(bot *discordgo.Session, evt *discordgo.MessageCr
// Handle it as a goroutine to speed things up
go func(cmd Command) {
// Test the command
if cmd.Check(evt.Message.GuildID, evt.Message.ChannelID) && cmd.Test(bot, evt) {
if cmd.Check(evt.Message.GuildID, evt.Message.ChannelID, evt.Message.Author.ID) && cmd.Test(bot, evt) {
// If it passed, log it,
author := *evt.Message.Author
log.WithFields(log.Fields{
"text": evt.Message.Content,
"command": cmd.GetName(),
"type": cmd.GetType(),
"userID": author.ID,
"username": author.Username + "#" + author.Discriminator,
"text": evt.Message.Content,
"command": cmd.GetName(),
"type": cmd.GetType(),
"userID": author.ID,
"username": author.Username + "#" + author.Discriminator,
"guildID": evt.Message.GuildID,
"channelID": evt.Message.ChannelID,
}).Info("Command fired")
// and run the command
err := cmd.Run(bot, evt)
if err != nil {
// Log if it failed, too
log.WithFields(log.Fields{
"text": evt.Message.Content,
"command": cmd.GetName(),
"type": cmd.GetType(),
"userID": author.ID,
"username": author.Username + "#" + author.Discriminator,
"error": err,
"text": evt.Message.Content,
"command": cmd.GetName(),
"type": cmd.GetType(),
"userID": author.ID,
"guildID": evt.Message.GuildID,
"channelID": evt.Message.ChannelID,
"username": author.Username + "#" + author.Discriminator,
"error": err,
}).Error("Command failed")
}
}
......@@ -155,7 +203,6 @@ func (c *MessageHandler) Handle(bot *discordgo.Session, evt *discordgo.MessageCr
}
// Add commands to the handler, validating whitelists/blacklists as well.
func (c *MessageHandler) Add(cmd Command) error {
func (c *MessageHandler) Add(cmd Command) {
c.commands = append(c.commands, cmd)
return nil
}
......@@ -43,7 +43,7 @@ func NewIASIPCommand(config BaseCommand) (cmd IASIPCommand, err error) {
if err != nil {
return cmd, err
}
regex, err := regexp.Compile("^" + options.Prefix + " (.*)$")
regex, err := regexp.Compile(`^` + options.Prefix + ` ([\S\s]*)$`)
if err != nil {
return cmd, err
}
......
package main
import (
"encoding/json"
"fmt"
"github.com/bwmarrin/discordgo"
)
// ReloadCommand is a meta-command which reloads commands.
// This should generally only be triggered by admins and bot owners.
// Keep it properly whitelisted.
type ReloadCommand struct {
BaseCommand
ReloadConfig
}
// ReloadConfig is the config for the ReloadCommand.
type ReloadConfig struct {
Trigger string `json:"trigger"`
}
// NewReloadCommand generates a new ReloadCommand.
// Aside from the trigger, no configuration is needed, so this is particularly short.
func NewReloadCommand(config BaseCommand) (cmd ReloadCommand, err error) {
options := ReloadConfig{}
err = json.Unmarshal(config.Options, &options)
if err != nil {
return cmd, err
}
cmd = ReloadCommand{
BaseCommand: config,
ReloadConfig: options,
}
return cmd, nil
}
// Test checks if the trigger was sent.
func (c ReloadCommand) Test(bot *discordgo.Session, evt *discordgo.MessageCreate) bool {
return c.Trigger == evt.Message.Content
}
// Run reloads commands.
func (c ReloadCommand) Run(bot *discordgo.Session, evt *discordgo.MessageCreate) error {
// Re-read bot config
var err error
config, err = ReadBotConfig(*configPath)
if err != nil {
bot.ChannelMessageSend(evt.Message.ChannelID, "Failed to reload commands.")
return err
}
// Try to make the new handler
newhandler, err := NewMessageHandler(bot, config.Commands)
if err != nil {
bot.ChannelMessageSend(evt.Message.ChannelID, "Failed to reload commands.")
return err
}
// Destroy the old one and use this new one
// NOTE Doesn't this mean a small window in which commands could double-trigger?
handler.DestroySelf()
handler = newhandler
// Log the success
_, err = bot.ChannelMessageSend(evt.Message.ChannelID, fmt.Sprintf("Commands reloaded! Parsed %d commands.", len(handler.commands)))
if err != nil {
return err
}
return nil
}
......@@ -4,9 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/bwmarrin/discordgo" // for running the bot
"github.com/tidwall/gjson" // for getting items in dot notation
"io/ioutil" // for opening response body
"github.com/bwmarrin/discordgo" // for running the bot
log "github.com/sirupsen/logrus" // logging suite
"github.com/tidwall/gjson" // for getting items in dot notation
"io/ioutil" // for opening response body
"net/http"
"net/url"
"regexp"
......@@ -29,6 +30,7 @@ type RESTConfig struct {
Method string `json:"method"`
Response []string `json:"response"`
Responses [][]string `json:"responses"`
ErrorMessage string `json:"errorMessage"`
}
// NewRESTCommand generates a new RESTCommand.
......@@ -80,6 +82,12 @@ func NewRESTCommand(config BaseCommand) (command RESTCommand, err error) {
return command, nil
}
func (r RESTCommand) SendErrorMessage(bot *discordgo.Session, evt *discordgo.MessageCreate) {
if len(r.ErrorMessage) > 0 {
bot.ChannelMessageSend(evt.Message.ChannelID, r.ErrorMessage)
}
}
// Test ensures the compiled regex passes.
func (r RESTCommand) Test(bot *discordgo.Session, evt *discordgo.MessageCreate) bool {
return r.regexp.MatchString(evt.Message.Content)
......@@ -97,19 +105,33 @@ func (r RESTCommand) Run(bot *discordgo.Session, evt *discordgo.MessageCreate) (
// Construct request based on this endpoint
request, err := http.NewRequest(r.Method, endpoint, nil)
if err != nil {
r.SendErrorMessage(bot, evt)
return err
}
// Log that we're about to send the request, in case someone's trying something nasty
log.WithFields(log.Fields{
"endpoint": endpoint,
"method": r.Method,
}).Info("Making HTTP request")
// Send request, ensure nothing failed, get JSON bytes
resp, err := r.client.Do(request)
if err != nil {
r.SendErrorMessage(bot, evt)
return errors.New("could not make request: " + err.Error())
}
// Log some response metadata, again, in case someone's being nasty
log.WithFields(log.Fields{
"endpoint": endpoint,
"response": resp.Status,
}).Info("HTTP request result")
if resp.StatusCode >= 400 {
r.SendErrorMessage(bot, evt)
return errors.New("request failed with status " + resp.Status)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
r.SendErrorMessage(bot, evt)
return errors.New("could not parse request body: " + err.Error())
}
// For single-response:
......@@ -148,5 +170,6 @@ func (r RESTCommand) Run(bot *discordgo.Session, evt *discordgo.MessageCreate) (
}
}
// If this code is reached, no response was valid, which probably shouldn't happen, so we'll throw an error
r.SendErrorMessage(bot, evt)
return errors.New("No valid response schema")
}
package main
import (
"encoding/json" // for parsing config file
"encoding/json" // for parsing config file
"errors"
"flag" // for parsing args at runtime
"github.com/bwmarrin/discordgo" // for running the bot
log "github.com/sirupsen/logrus" // logging suite
......@@ -15,16 +16,20 @@ import (
type BotConfiguration struct {
// Token that the bot logs in with.
BotToken string `json:"botToken"`
// Bot status message (when initialized).
Status string `json:"status"`
// List of commands to try and create.
Commands []BaseCommand `json:"commands"`
}
var config BotConfiguration
var (
config BotConfiguration
handler *MessageHandler
logPath = flag.String("log", "", "Path to the logfile, if used.")
configPath = flag.String("conf", "valerius.json", "Path to the config file.")
)
func init() {
// set up flags
logPath := flag.String("log", "", "Path to the logfile, if used.")
configPath := flag.String("conf", "valerius.json", "Path to the config file.")
// parse flags
flag.Parse()
// setup log
......@@ -38,15 +43,12 @@ func init() {
// set up the output and formatter
log.SetOutput(io.MultiWriter(os.Stdout, logfile))
}
// load bot config file
configFile, err := ioutil.ReadFile(*configPath)
if err != nil {
log.Fatal("Unable to read config file: ", err)
}
// parse bot config file
err = json.Unmarshal(configFile, &config)
// have to set err here so config goes in as a global var, 'cuz Go
// (this is probably the wrong way to do it, though)
var err error
config, err = ReadBotConfig(*configPath)
if err != nil {
log.Fatal("Unable to read config file: ", err)
log.Fatal(err)
}
}
......@@ -65,6 +67,21 @@ func initBot() (bot *discordgo.Session, err error) {
return
}
// ReadBotConfig reads a config file from a path and parses it into a BotConfiguration.
func ReadBotConfig(path string) (config BotConfiguration, err error) {
// load bot config file
configFile, err := ioutil.ReadFile(path)
if err != nil {
return config, errors.New("Unable to read config file: " + err.Error())
}
// parse bot config file
err = json.Unmarshal(configFile, &config)
if err != nil {
return config, errors.New("Unable to read config file: " + err.Error())
}
return config, nil
}
func main() {
// initialize the bot
bot, err := initBot()
......@@ -72,45 +89,20 @@ func main() {
log.Fatal("Failed to initialize bot: ", err)
}
defer bot.Close()
// instantiate and register the handler
handler := NewMessageHandler(bot)
// add handler commands
for _, config := range config.Commands {
// TODO find a cleaner way to register commands
switch config.Type {
case "pingpong":
cmd, err := NewPingPongCommand(config)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
handler.Add(cmd)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
case "iasip":
cmd, err := NewIASIPCommand(config)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
handler.Add(cmd)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
case "rest":
cmd, err := NewRESTCommand(config)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
handler.Add(cmd)
if err != nil {
log.Fatal("Error with command "+config.Name+": ", err)
}
default:
log.Fatal("Command " + config.Name + " is of invalid type (" + config.Type + "). Exiting.")
}
// instantiate the handler
handler, err = NewMessageHandler(bot, config.Commands)
if err != nil {
log.Fatal(err)
}
// open the bot to be used
bot.Open()
// set our status
if len(config.Status) > 0 {
err = bot.UpdateStatus(0, config.Status)
if err != nil {
log.Error("Error setting status:", err)
}
}
// wait for OS interrupt (ctrl-c or a kill or something)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, os.Kill)
......