Browse Source

config rewrite

main
Michael Parker 1 year ago
parent
commit
6c82b998d2
  1. 3
      .gitignore
  2. 12
      README.md
  3. 436
      config.go
  4. 4
      configs/discord/discord.example.yml
  5. 118
      configs/discord/example/server.example.yml
  6. 313
      configsetup.go
  7. 383
      discord.go
  8. 50
      discord_structs.go
  9. 96
      discord_utilities.go
  10. 8
      go.mod
  11. 39
      go.sum
  12. 161
      irc.go
  13. 47
      irc_structs.go
  14. 73
      logging.go
  15. 233
      parkertron.go
  16. 277
      parsing.go
  17. 49
      parsing_structs.go
  18. 1
      slack.go

3
.gitignore

@ -14,4 +14,5 @@ Gopkg\.lock
.vscode/
.idea/
.idea/
file_layout.txt

12
README.md

@ -1,7 +1,7 @@
# parkertron
![Parkertron Logo](images/parkertron_logo.png)
![Parkertron logo](images/parkertron_logo.png)
A simple discord chat bot with a simple configuration. Written using [discordgo](https://github.com/bwmarrin/discordgo).
@ -89,11 +89,11 @@ The checklist so far
- [x] Permissions
- [ ] Permissions management
- [ ] Logging
- [ ] Log user join/leave
- [x] Log chats (only logs channels it is watching to cut on logging)
- [ ] Log edits (show original and edited)
- [ ] Log chats/edits to separate files/folders
- [ ] logging
- [ ] log user join/leave
- [x] log chats (only logs channels it is watching to cut on logging)
- [ ] log edits (show original and edited)
- [ ] log chats/edits to separate files/folders
- [ ] Join voice channels
- [ ] Play audio from links

436
config.go

@ -1,297 +1,247 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/fsnotify/fsnotify"
Log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/goccy/go-yaml"
)
var (
//Bot Config
Bot = viper.New()
//Discord Config
Discord = viper.New()
//IRC Config
IRC = viper.New()
//Command Config
Command = viper.New()
//Keyword Config
Keyword = viper.New()
//Parsing Config
Parsing = viper.New()
)
func setupConfig() {
if configFilecheck() == false {
Log.Error("There was an issue setting up the config", nil)
// File management
func writeJSONToFile(jdata []byte, file string) error {
Log.Printf("updating file %s", file)
// create a file with a supplied name
jsonFile, err := os.Create(file)
if err != nil {
return err
}
//Setting Bot config settings
Bot.SetConfigName("bot")
Bot.AddConfigPath("configs/")
Bot.WatchConfig()
Bot.OnConfigChange(func(e fsnotify.Event) {
Log.Info("Bot config changed")
})
if err := Bot.ReadInConfig(); err != nil {
Log.Fatal("Could not load Bot configuration.", err)
return
_, err = jsonFile.Write(jdata)
if err != nil {
return err
}
for _, cr := range getBotServices() {
if strings.Contains(strings.TrimPrefix(cr, "bot.services."), cr) {
if strings.Contains(cr, "discord") {
//Setting Discord config settings
Discord.SetConfigName("discord")
Discord.AddConfigPath("configs/")
Discord.WatchConfig()
Discord.OnConfigChange(func(e fsnotify.Event) {
Log.Info("Discord config changed")
})
if err := Discord.ReadInConfig(); err != nil {
Log.Fatal("Could not load Discord configuration.", err)
return
}
Discord.SetDefault("discord.command.remove", true)
}
if strings.Contains(cr, "irc") {
//Setting IRC config settings
IRC.SetConfigName("irc")
IRC.AddConfigPath("configs/")
IRC.WatchConfig()
IRC.OnConfigChange(func(e fsnotify.Event) {
Log.Info("IRC config changed")
return nil
}
func readJSONFromFile(file string, v interface{}) error {
})
if err := IRC.ReadInConfig(); err != nil {
Log.Fatal("Could not load irc configuration.", err)
return
}
}
if !doesExist(file) {
Log.Printf("%s does not exist creating it", file)
jsonFile, err := os.Create(file)
if err != nil {
return fmt.Errorf("there was an error loading the file")
}
_, err = jsonFile.Write([]byte("{}"))
if err != nil {
return err
}
}
//Setting Command config settings
Command.SetConfigName("commands")
Command.AddConfigPath("configs/")
Command.WatchConfig()
Command.OnConfigChange(func(e fsnotify.Event) {
Log.Info("Command config changed")
})
if err := Command.ReadInConfig(); err != nil {
Log.Fatal("Could not load Command configuration.", err)
return
}
//Setting Keyword config settings
Keyword.SetConfigName("keywords")
Keyword.AddConfigPath("configs/")
Keyword.WatchConfig()
Keyword.OnConfigChange(func(e fsnotify.Event) {
Log.Info("Keyword config changed")
})
// Open our jsonFile
// Log.Printf("opening json file\n")
jsonFile, err := os.Open(file)
if err := Keyword.ReadInConfig(); err != nil {
Log.Fatal("Could not load Keyword configuration.", err)
return
// if we os.Open returns an error then handle it
if err != nil {
return err
}
//Setting website parsing config settings
Parsing.SetConfigName("parsing")
Parsing.AddConfigPath("configs/")
Parsing.WatchConfig()
Parsing.OnConfigChange(func(e fsnotify.Event) {
Log.Info("Parsing config changed")
})
if err := Parsing.ReadInConfig(); err != nil {
Log.Fatal("Could not load Parsing configuration.", err)
return
// Log.Printf("holding file open\n")
// defer the closing of our jsonFile so that we can parse it later on
defer func() {
if err := jsonFile.Close(); err != nil {
Log.Printf("Error while closing JSON file: %v", err)
}
}()
// Log.Printf("reading file\n")
// read our opened xmlFile as a byte array.
byteValue, _ := ioutil.ReadAll(jsonFile)
err = json.Unmarshal(byteValue, &v)
if err != nil {
return err
}
Log.Info("Bot configs loaded")
}
//Bot Get funcs
func getBotServices() []string {
return Bot.GetStringSlice("bot.services")
}
func getBotConfigBool(req string) bool {
return Bot.GetBool("bot." + req)
}
func getBotConfigString(req string) string {
return Bot.GetString("bot." + req)
}
func getBotConfigInt(req string) int {
return Bot.GetInt("bot." + req)
}
func getBotConfigFloat(req string) float64 {
return Bot.GetFloat64("bot." + req)
}
func setBotConfigString(req string, value string) {
Bot.Set("bot."+req, value)
}
//Discord get funcs
func getDiscordConfigString(req string) string {
return Discord.GetString("discord." + req)
}
func getDiscordConfigInt(req string) int {
return Discord.GetInt("discord." + req)
}
func getDiscordConfigBool(req string) bool {
return Discord.GetBool("discord." + req)
}
func getDiscordChannels() string {
return strings.ToLower(strings.Join(Discord.GetStringSlice("discord.channels.listening"), " "))
// return the json byte value.
return nil
}
func getDiscordGroup(req string) []string {
var groups []string
for x := range Discord.GetStringMapString("discord.permissions.group") {
groups = append(groups, x)
func writeYamlToFile(ydata []byte, file string) error {
Log.Printf("updating file %s", file)
// create a file with a supplied name
yamlFile, err := os.Create(file)
if err != nil {
return err
}
_, err = yamlFile.Write(ydata)
if err != nil {
return err
}
return groups
}
func getDiscordGroupRoles(req string) []string {
roles := Discord.GetStringSlice("discord.permissions.group." + req + ".roles")
return roles
}
func getDiscordGroupUsers(req string) []string {
users := Discord.GetStringSlice("discord.permissions.group." + req + ".users")
return users
return nil
}
func readYamlFromFile(file string, v interface{}) error {
if !doesExist(file) {
Log.Printf("%s does not exist creating it", file)
yamlFile, err := os.Create(file)
if err != nil {
return fmt.Errorf("there was an error loading the file")
}
_, err = yamlFile.Write([]byte(""))
if err != nil {
return err
}
}
func getDiscordBlacklist() string {
return strings.ToLower(strings.Join(Discord.GetStringSlice("discord.permissions.group.blacklist"), " "))
}
// Open our jsonFile
// Log.Printf("opening json file\n")
yamlFile, err := os.Open(file)
func getDiscordKOMChannel(req string) bool {
return Discord.IsSet("discord.kick_on_mention.channel." + req)
}
// if we os.Open returns an error then handle it
if err != nil {
return err
}
func getDiscordKOMID(req string) string {
return strings.ToLower(strings.Join(Discord.GetStringSlice("discord.kick_on_mention.channel."+req), " "))
}
// Log.Printf("holding file open\n")
// defer the closing of our jsonFile so that we can parse it later on
defer func() {
if err := yamlFile.Close(); err != nil {
Log.Printf("Error while closing JSON file: %v", err)
}
}()
// Log.Printf("reading file\n")
// read our opened xmlFile as a byte array.
byteValue, _ := ioutil.ReadAll(yamlFile)
err = yaml.Unmarshal(byteValue, &v)
if err != nil {
return err
}
func getDiscordKOMMessage(req string) string {
return strings.ToLower(strings.Join(Discord.GetStringSlice("discord.kick_on_mention.channel."+req+".message"), "\n"))
// return the json byte value.
return nil
}
//IRC get funcs
func getIRCConfigString(req string) string {
return IRC.GetString("irc." + req)
func doesExist(file string) bool {
if _, err := os.Stat(file); err == nil {
return true
}
return false
}
func getIRCConfigInt(req string) int {
return IRC.GetInt("irc." + req)
}
func loadConfigs(confDir string) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
Log.Error("%s", err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
fmt.Println("event:", event)
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
fmt.Println("error:", err)
}
}
}()
func getIRCConfigBool(req string) bool {
return IRC.GetBool("irc." + req)
}
err = filepath.Walk(confDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
Log.Infof("prevent panic by handling failure accessing a path %q: %v\n", path, err)
return err
}
func getIRCChannels() []string {
return IRC.GetStringSlice("irc.channels.listening")
}
if info.IsDir() && strings.Contains(info.Name(), "example") {
return filepath.SkipDir
}
func getIRCGroupMembers(req string) string {
return strings.ToLower(strings.Join(IRC.GetStringSlice("irc.permissions.group."+req), " "))
}
if strings.HasPrefix(info.Name(), ".") || strings.Contains(info.Name(), "example") {
return nil
}
func getIRCBlacklist() string {
return strings.ToLower(strings.Join(IRC.GetStringSlice("discord.permissions.group.blacklist"), " "))
}
if !info.IsDir() {
fullPath, err := filepath.Abs(path)
if err != nil {
Log.Errorf("error converting path %s\n", err)
}
//Command get funcs
func getCommands() []string {
return Command.AllKeys()
}
Log.Infof("loading config into memory")
func getCommandsString() string {
return strings.ToLower(strings.Replace(strings.Replace(strings.Join(Command.AllKeys(), ", "), "command.", "", -1), ".response", "", -1))
}
Log.Infof("Loading file '%s' for file watcher\n", fullPath)
func getCommandResonse(req string) []string {
return Command.GetStringSlice("command." + req + ".response")
}
err = watcher.Add(fullPath)
if err != nil {
log.Fatal(err)
}
}
func getCommandResponseString(req string) string {
return strings.Join(Command.GetStringSlice("command."+req+".response"), "\n")
}
return nil
})
if err != nil {
return err
}
func getCommandReaction(req string) []string {
return Command.GetStringSlice("command." + req + ".reaction")
}
go func() {
for {
select {
case event := <-watcher.Events:
switch event.Op {
case fsnotify.Write:
func getCommandStatus(req string) bool {
for _, cr := range getCommands() {
if strings.Contains(strings.TrimPrefix(cr, "command."), req) {
return true
}
}
}
}
return false
}
}()
//Keyword get funcs
func getKeywords() []string {
return Keyword.AllKeys()
}
func getKeywordsString() string {
return strings.ToLower(strings.Replace(strings.Replace(strings.Join(Keyword.AllKeys(), ", "), "keyword.", "", -1), ".response", "", -1))
}
Log.Info("watcher started\n")
func getKeywordResponse(req string) []string {
return Keyword.GetStringSlice("keyword." + req + ".response")
<-done
return nil
}
func getKeywordResponseString(req string) string {
return strings.Join(Keyword.GetStringSlice("keyword."+req+".response"), "\n")
}
// LoadConfig loads configs from a file to an interface
func loadFile(file string, v interface{}) error {
if strings.HasSuffix(file, ".json") {
readJSONFromFile(file, v)
} else if strings.HasSuffix(file, ".yml") || strings.HasSuffix(file, ".yaml") {
readYamlFromFile(file, v)
} else {
return errors.New("no supported file type located")
}
func getKeywordReaction(req string) []string {
return Keyword.GetStringSlice("keyword." + req + ".reaction")
return nil
}
//Parsing get funcs
func getParsingPasteKeys() string {
return strings.Replace(strings.Join(Parsing.AllKeys(), ", "), "parse.paste.", "", -1)
}
// SaveConfig saves interfaces to a file
func SaveConfig(file string, v interface{}) error {
// Log.Printf("converting struct data to bytesfor %s", file)
bytes, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Errorf("there was an error converting the user data to json")
}
func getParsingPasteString(key string) string {
return Parsing.GetString("parse.paste." + key)
}
// Log.Printf("writing bytes to file")
if err := writeJSONToFile(bytes, file); err != nil {
return err
}
func getParsingImageFiletypes() []string {
return Parsing.GetStringSlice("parse.image.filetype")
return nil
}

4
configs/discord/discord.example.yml

@ -1,4 +1,4 @@
discord:
token: "This is only an example..." ## You need to create an app and get a token here - https://discordapp.com/developers/
direct:
response: "Please message in the main server."
dm_response: "Please message in the main server."
game: "Half Life 3.0 Alpha"

118
configs/discord/example/server.example.yml

@ -1,56 +1,76 @@
---
server_id: ## example server
## If you want logging to go to discord follow this https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks
webhook: "https://discordapp.com/api/webhooks/GET-YOUR-OWN-WEBHOOK"
command:
settings: ## server settings
prefix: "." ## default if not set is "."
remove: true
## the response the bot gives when it is mention in various channels.
mention:
## bot responds with the following.
response: ":doughnut:"
## bot was mentioned with no message
empty: "Please ask a question or supply your log into this chat"
## response on channels the bot is not listening to.
wrong_channel: "Please use one of the supported channels"
## channel configs
channels: ## each channel is in a group. one to many channels can be in a single group.
- channel: ## example channel
channel_ids:
- ""
commands:
example:
response:
- "Hello, I am a support bot created by `parkervcp` designed to help solve simple problems."
reaction:
- ""
keywords:
example:
response:
- ""
reaction:
- ""
kick_on_mention:
server_channels: ## server channels that are to be used
admin: "" ## should be the channelid in string format
log: "" ## should be the channelid in string format
webhooks:
## If you want logging to go to discord follow this https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks
log: ## "https://discordapp.com/api/webhooks/GET-YOUR-OWN-WEBHOOK"
mentions: ## the response the bot gives when it is mention in various channels.
ping: ## if the bot is pinged in a message
- ""
mention: ## bot was pinged with no message
- ""
permissions: ## only simple permissions for now. These only apply to some filtering rules and some command usage.
group: ## user groups. Currently These are not implimented. The Blacklist will ignore specific users
admin: ## default to server owner having this permission
roles:
- "" ## group ID's
- ""
users:
- "" ## group ID's
message: ## message to send when kicking a user.
- "Fire in the hole."
reason:
- "We don't support the <#linux-help> channel" ## DM the user with why they were kicked.
- ""
mod: ## no default users for this. Needs to be user ID's. Using roleID's is planned eventually.
roles:
- ""
users:
- ""
blacklist: ## users in this group are ignored.
- ""
## only simple permissions for now. These only apply to some filtering rules and some command usage..
permissions:
group: ## user groups. Currently These are not implimented. The Blacklist will ignore specific users
admin: ## default to server owner having this permission
roles:
- ""
users:
- ""
mod: ## no default users for this. Needs to be user ID's. Using roleID's is planned eventually.
clear_commands: true ## clear out commands after processing them
channel_groups: ## each channel is in a group. one to many channels can be in a single group.
- ## example channel
use_global: false
channel_ids:
- ""
use_global: false
mentions: ## the response the bot gives when it is mention in various channels.
ping: ## if the bot is pinged in a message
mention: ## bot was pinged with no message
- "Please ask a question or supply your log into this chat"
commands:
example:
response:
- "Hello, I am a support bot created by `parkervcp` designed to help solve simple problems."
reaction:
- ""
keywords:
example:
response:
- ""
reaction:
- ""
kick_on_mention:
roles:
- ""
- "" ## group ID's
users:
- ""
blacklist: ## users in this group are ignored.
- ""
- "" ## group ID's
message: ## message to send when kicking a user.
- "Fire in the hole."
reason:
- "We don't support the <#linux-help> channel" ## DM the user with why they were kicked.
parsing:
image:
filetype:
- "png"
- "jpg"
paste:
-
name: pastebin
URL: "https://pastebin.com/"
format: "&url&raw/&filename&"
-
name: hastebin
URL: "https://hastebin.com/"
format: "&url&raw/&filename&"

313
configsetup.go

@ -1,313 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
"time"
Log "github.com/sirupsen/logrus"
"github.com/tcnksm/go-input"
)
// configFilecheck to check if the files exist before starting up.
func configFilecheck() bool {
if _, err := os.Stat("configs/"); err != nil {
// create configs folder
if os.IsNotExist(err) {
os.Mkdir("configs/", 0755)
}
} else {
Log.Info("Config folder exists")
if checkConfigExists("bot.yml") == false {
time.Sleep(5 * time.Second)
setupBotConfig()
}
for _, cr := range getBotServices() {
if strings.Contains(strings.TrimPrefix(cr, "bot.services."), cr) {
if strings.Contains(cr, "discord") {
if checkConfigExists("discord.yml") == false {
setupDiscordConfig()
}
}
if strings.Contains(cr, "irc") {
if checkConfigExists("irc.yml") == false {
setupIRCConfig()
}
}
}
}
if checkConfigExists("commands.yml") == false {
setupCommandsConfig()
}
if checkConfigExists("keywords.yml") == false {
setupKeywordsConfig()
}
return true
}
return false
}
func checkConfigExists(file string) bool {
if _, err := os.Stat("configs/" + file); err == nil {
return true
}
Log.Info("Need to generate the " + file + ". Doing that now.")
return false
}
func askBoolQuestion(question string) bool {
var answer bool
ui := &input.UI{
Writer: os.Stdout,
Reader: os.Stdin,
}
_, err := ui.Ask(question, &input.Options{
Required: true,
Loop: true,
ValidateFunc: func(in string) error {
if in == "Y" || in == "y" || in == "Yes" || in == "yes" {
answer = true
} else if in == "N" || in == "n" || in == "No" || in == "no" {
answer = false
} else {
return fmt.Errorf("Need to have a Y/n answer")
}
return nil
},
})
if err != nil {
Log.Fatal("", err)
}
return answer
}
func askStringQuestion(question string) string {
ui := &input.UI{
Writer: os.Stdout,
Reader: os.Stdin,
}
answer, err := ui.Ask(question, &input.Options{
// Read the default val from env var
Loop: true,
})
if err != nil {
Log.Fatal("", err)
}
return answer
}
func setupBotConfig() {
var services []string
if askBoolQuestion("Do you plan on supporting discord? [Y/n]") {
services = append(services, "discord")
Log.Info("Discord enabled")
setupDiscordConfig()
}
if askBoolQuestion("Do you plan on supporting irc? [Y/n]") {
services = append(services, "irc")
Log.Info("IRC enabled")
setupIRCConfig()
}
Bot.Set("bot.services", services)
if askBoolQuestion("Do you want to use the default log location of 'logs/'? [Y/n]") == false {
Bot.Set("bot.log.location", askStringQuestion("Where you like logs to be saved?"))
} else {
Bot.Set("bot.log.location", "logs/")
}
if askBoolQuestion("Do you want to use the default log level of info? [Y/n]") == false {
level := askStringQuestion("what log level would you like to use? [info/debug]")
if level == "info" || level == "debug" {
Bot.Set("bot.log.level", level)
} else {
Log.Info("Invalid log level set with " + level + ". Defaulting to info")
Bot.Set("bot.log.level", "info")
}
} else {
Bot.Set("bot.log.level", "info")
}
Bot.WriteConfigAs("configs/bot.yml")
}
func setupDiscordConfig() {
// get discord bot token
Discord.Set("discord.token", askStringQuestion("What is your discord token?"))
// set bot prefix
if askBoolQuestion("Would you like to use the default prefix? '.' [Y/n]") == false {
Discord.Set("discord.prefix", askStringQuestion("What is the prefix you'd like to use?"))
} else {
Discord.Set("discord.prefix", ".")
}
// set bot owner
if askBoolQuestion("Would you like to set a owner? (defaults to server owner) [Y/n]") {
Discord.Set("discord.owner", askStringQuestion("What is the server owners discord ID?"))
} else {
Log.Info("defaulting to server owner")
}
var listening []string
// set channel filter up
if askBoolQuestion("Do you want the bot to listen on specific channels? [Y/n]") {
if askBoolQuestion("A channel to listen on it required. Would you like to set one now? [Y/n]") {
listening = append(listening, askStringQuestion("What is the ID of the channel you want to listen on?"))
Discord.Set("bot.channels.filter", true)
Discord.Set("bot.channels.listening", listening)
Log.Info("The channel is not verified will only work if correct.")
} else {
Discord.Set("bot.channels.filter", false)
Discord.Set("bot.channels.listening", listening)
}
}
/*
TODO:
Discord.Set("irc.channels.groups.admin", admin)
Discord.Set("irc.channels.groups.mods", mods)
Discord.Set("irc.channels.groups.blacklist", blacklist)
*/
Discord.WriteConfigAs("configs/discord.yml")
}
func setupIRCConfig() {
// get irc server settings
IRC.Set("irc.server.address", askStringQuestion("What is the server address"))
IRC.Set("irc.server.port", askStringQuestion("What is the server port?"))
IRC.Set("irc.server.ssl", askBoolQuestion("Does this connection require ssl [Y/n]"))
// get irc user settings
IRC.Set("irc.ident", askStringQuestion("What is the IRC username you are using?"))
IRC.Set("irc.email", askStringQuestion("What is the email associated with the account?"))
IRC.Set("irc.password", askStringQuestion("What is the password the bot is using?"))
IRC.Set("irc.nick", askStringQuestion("What is the nick that the bot should use?"))
IRC.Set("irc.real", askStringQuestion("What is the \"Real Name\" the bot should use?"))
// get irc text parsing settings
// set bot prefix
if askBoolQuestion("Would you like to use the default prefix? '.' [Y/n]") == false {
IRC.Set("irc.prefix", askStringQuestion("What is the prefix you'd like to use?"))
} else {
IRC.Set("irc.prefix", ".")
}
// get irc channels
var listening []string
if askBoolQuestion("Do you want to add channels to join now? (you can pm the bot) [Y/n]") {
listening = append(listening, askStringQuestion("What channel is the bot supposed to join? (Without the # in the name)"))
for askBoolQuestion("Do you want to add more channels to join now? [Y/n]") {
listening = append(listening, askStringQuestion("What channel is the bot supposed to join? (Without the # in the name)"))
}
}
IRC.Set("irc.channels.listening", listening)
/*
TODO:
IRC.Set("irc.channels.groups.admin", admin)
IRC.Set("irc.channels.groups.mods", mods)
IRC.Set("irc.channels.groups.blacklist", blacklist)
*/
IRC.WriteConfigAs("configs/irc.yml")
}
func setupCommandsConfig() {
var command string
commandmap := make(map[string]interface{})
exit := false
if askBoolQuestion("Do you want to set up custom commands now? [Y/n]: ") == false {
Log.Info("Writing default commands config to file")
} else {
for exit == false {
command = askStringQuestion("What is the command you want to add? (It can have spaces in it ex: 'help command') (leave blank to stop adding commands): ")
if command == "" {
exit = true
} else {
commandmap[command+".response"] = setupStringMap()
}
}
}
Command.Set("command", commandmap)
Command.WriteConfigAs("configs/commands.yml")
}
func setupKeywordsConfig() {
var keyword string
keywordmap := make(map[string]interface{})
exit := false
if askBoolQuestion("Do you want to set up custom keywords now? [Y/n]: ") == false {
Log.Info("Writing default keywords config to file")
} else {
for exit == false {
keyword = askStringQuestion("What is the keyword you want to add? (It can have spaces in it ex: 'i need help') (leave blank to stop adding commands): ")
if keyword == "" {
exit = true
} else {
keywordmap[keyword+".response"] = setupStringMap()
}
}
}
Keyword.Set("keyword", keywordmap)
Keyword.WriteConfigAs("configs/keywords.yml")
}
func setupGroup() {
/*
TODO:
This block is for future work. Namely permissions and other things.
// get irc groups
var group []string
if askBoolQuestion("Do you want to add users to admin group now?? [Y/n]") {
admin = append(admin, askStringQuestion(""))
for askBoolQuestion("Do you want to add more groups to join now? [Y/n]") {
admin = append(admin, askStringQuestion(""))
}
}
*/
}
func setupStringMap() []string {
var array []string
var line string
exit := false
fmt.Println("Multi-line responses are supported, so we will keed adding lines until you specify to stop (blank response)")
for exit == false {
line = askStringQuestion("What do you want this line to say? (leave blank to exit): ")
if line == "" {
exit = true
} else {
array = append(array, line)
}
}
return array
}

383
discord.go

@ -6,284 +6,64 @@ import (
"strings"
"github.com/bwmarrin/discordgo"
Log "github.com/sirupsen/logrus"
)
var (
//BotSession is the DiscordSession
dg *discordgo.Session
stopDiscord = make(chan string)
discordConfig discord
)
// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the autenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
dpack := DataPackage{}
authed := false
dpack.Service = "discord"
// Ignore all messages created by bots
if m.Author.Bot {
Log.Debug("User is a bot and being ignored.")
return
}
// get channel information
channel, err := s.State.Channel(m.ChannelID)
if err != nil {
Log.Fatal("Channel error", err)
return
}
// Respond on DM's
// TODO: Make the response customizable
if channel.Type == 1 {
Log.Debug("This was a DM")
dpack.Response = getDiscordConfigString("direct.response")
sendDiscordMessage(dpack)
return
}
// get guild info
guild, err := s.Guild(channel.GuildID)
if err != nil {
Log.Fatal("Guild error", err)
return
}
authorGuildInfo, err := dg.GuildMember(guild.ID, m.Author.ID)
if err != nil {
Log.Fatal("Author Guild Info error", err)
return
}
//ignore messages from the bot
bot, err := dg.User("@me")
if err != nil {
fmt.Println("error obtaining account details,", err)
}
// quick referrence for information
dpack.Message = m.Content
dpack.MessageID = m.ID
dpack.AuthorID = m.Author.ID
dpack.AuthorName = m.Author.Username
dpack.AuthorRoles = authorGuildInfo.Roles
dpack.BotID = bot.ID
dpack.ChannelID = channel.ID
dpack.GuildID = guild.ID
// get group status. If perms are set and group name. These are note weighted yet.
dpack.Perms, dpack.Group = discordAuthorRolePermissionCheck(dpack.AuthorRoles)
if !dpack.Perms {
dpack.Perms, dpack.Group = discordAuthorUserPermissionCheck(dpack.AuthorID)
}
// setting server owner default to admin perms
if dpack.AuthorID == guild.OwnerID {
dpack.Perms = true
dpack.Group = "admin"
authed = true
}
// debug messaging
if dpack.Perms {
Log.Debug("User has perms and is in the group: " + dpack.Group)
if dpack.Group == "admin" || dpack.Group == "mod" {
authed = true
}
} else {
Log.Debug("User has no perms")
}
// kick for mentioning a group in a specific channel.
if getDiscordKOMChannel(dpack.ChannelID) {
if !dpack.Perms {
Log.Debug("Checking for Kick on Mention group")
// Check if a group is mentioned in message
for _, ment := range m.MentionRoles {
Log.Debug("Group " + ment + " was Mentioned")
if strings.Contains(getDiscordKOMID(dpack.ChannelID+".group"), ment) {
dpack.Mention = dpack.AuthorID
Log.Debug("Sending message to channel")
dpack.Response = getDiscordKOMMessage(dpack.ChannelID)
sendDiscordMessage(dpack)
Log.Debug("Sending message to user")
dpack.Response = getDiscordKOMID(dpack.ChannelID + ".reason")
sendDiscordDirectMessage(dpack)
kickDiscordUser(guild.ID, dpack.AuthorID, dpack.AuthorName, getDiscordKOMID(dpack.ChannelID+".reason"), dpack.BotID)
}
}
}
}
Log.Debug("Passed KOM")
func discordMessageHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
// ignore blacklisted users
if strings.Contains(getDiscordBlacklist(), dpack.AuthorID) {
Log.Debug("User is blacklisted and being ignored.")
}
Log.Debug("Passed Blacklist")
// making a string array for attached images on messages.
for _, y := range m.Attachments {
Log.Debug(y.ProxyURL)
dpack.Attached = append(dpack.Attached, y.ProxyURL)
}
Log.Debug("Attachments grabbed")
// Always parse owner and group commands. Keyswords in matched channels.
if !authed {
// Ignore all channels it's not listening on, with debug messaging.
if !discordChannelFilter(dpack.ChannelID) {
Log.Debug("This channel is being filtered out and ignored.")
for _, ment := range m.Mentions {
if ment.ID == dg.State.User.ID {
Log.Debug("The bot was mentioned")
dpack.Response = getDiscordConfigString("mention.wrong_channel")
sendDiscordMessage(dpack)
}
}
Log.Debug("Message has been ignored.")
return
}
}
Log.Debug("Passed channel filter")
// Check if the bot is mentioned
for _, ment := range m.Mentions {
if ment.ID == dg.State.User.ID {
Log.Debug("The bot was mentioned")
dpack.Response = getDiscordConfigString("mention.response")
sendDiscordMessage(dpack)
if strings.Replace(message, "<@"+dg.State.User.ID+">", "", -1) == "" {
dpack.Response = getDiscordConfigString("mention.empty")
sendDiscordMessage(dpack)
}
}
}
Log.Debug("Passed bot mentions")
//
// Message Handling
//
if dpack.Message != "" || dpack.Attached != nil {
Log.Debug("Message ID: " + dpack.MessageID + "\nMessage Content: " + dpack.Message)
discordMessageHandler(dpack)
return
}
// exists solely because it got here somehow...
Log.Debug("Really...")
}
func sendDiscordMessage(dpack DataPackage) {
if dpack.Response == "" {
return
}
// parse for prefix in the response
dpack.Response = strings.Replace(dpack.Response, "&prefix&", getDiscordConfigString("prefix"), -1)
// parse for reactions in the response
if strings.Contains(dpack.Response, "&react&") {
dpack.Response = strings.Replace(dpack.Response, "&react&", "", -1)
if dpack.MsgTye == "keyword" {
dpack.Reaction = getKeywordReaction(dpack.Keyword)
} else if dpack.MsgTye == "command" {
dpack.Reaction = getCommandReaction(dpack.Command)
}
dpack.ReactAdd = true
}
//parse for user mentions in the response
if strings.Contains(dpack.Response, "&user&") {
if dpack.Mention == "" {
dpack.Mention = dpack.AuthorID
}
dpack.Response = strings.Replace(dpack.Response, "&user&", "<@"+dpack.Mention+">", -1)
}
Log.Debug("ChannelID " + dpack.ChannelID + " \n Discord Message Sent: \n" + dpack.Response)
//Send response to channel
sent, err := dg.ChannelMessageSend(dpack.ChannelID, dpack.Response)
if err != nil {
Log.Fatal("error sending message", err)
return
}
//send reaction to channel
if dpack.ReactAdd {
Log.Debug("Adding Reactions")
sendDiscordReaction(sent.ChannelID, sent.ID, dpack)
}
// remove previous commands if discord.command.remove is true
if getDiscordConfigBool("command.remove") {
if getCommandStatus(dpack.Message) {
deleteDiscordMessage(dpack)
Log.Debug("Cleared command message.")
}
if strings.HasPrefix(dpack.Message, "list") || strings.HasPrefix(dpack.Message, "ggl") {
deleteDiscordMessage(dpack)
Log.Debug("Cleared command message.")
}
}
}
func kickDiscordUser(guild, user, username, reason, authorname string) {
dg.GuildMemberDeleteWithReason(guild, user, reason)
func deleteDiscordMessage(dpack DataPackage) {
Log.Debug("Removing message: " + dpack.Message)
dg.ChannelMessageDelete(dpack.ChannelID, dpack.MessageID)
embed := &discordgo.MessageEmbed{
Title: "Message was deleted",
Title: "User has been kicked",
Color: 0xf39c12,
Fields: []*discordgo.MessageEmbedField{
&discordgo.MessageEmbedField{
Name: "MessageID",
Value: dpack.MessageID,
Name: "User",
Value: username,
Inline: true,
},
&discordgo.MessageEmbedField{
Name: "Message Content",
Value: dpack.Message,
Name: "By",
Value: authorname,
Inline: true,
},
&discordgo.MessageEmbedField{
Name: "Reason",
Value: reason,
Inline: true,
},
},
}
if getDiscordConfigString("embed.audit") != "" {
sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
}
Log.Debug("message was deleted.")
}
fmt.Sprint(embed)
func sendDiscordReaction(channelID string, messageID string, dpack DataPackage) {
for _, reaction := range dpack.Reaction {
Log.Debug("Adding reation \"" + reaction + "\" to message " + dpack.MessageID)
dg.MessageReactionAdd(dpack.ChannelID, dpack.MessageID, reaction)
}
}
// TODO: Need to use new config for this
// sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
func sendDiscordDirectMessage(dpack DataPackage) {
channel, err := dg.UserChannelCreate(dpack.AuthorID)
dpack.ChannelID = channel.ID
if err != nil {
Log.Fatal("error creating direct message channel.", err)
return
}
sendDiscordMessage(dpack)
Log.Info("User " + authorname + " has been kicked from " + guild + " for " + reason)
}
func kickDiscordUser(guild string, user string, username string, reason string, authorname string) {
dg.GuildMemberDeleteWithReason(guild, user, reason)
func banDiscordUser(guild, user, username, reason, authorname string, days int) {
dg.GuildBanCreateWithReason(guild, user, reason, days)
embed := &discordgo.MessageEmbed{
Title: "User has been kicked",
Color: 0xf39c12,
Title: "User has been banned for " + strconv.Itoa(days) + " days",
Color: 0xc0392b,
Fields: []*discordgo.MessageEmbedField{
&discordgo.MessageEmbedField{
Name: "User",
@ -303,63 +83,100 @@ func kickDiscordUser(guild string, user string, username string, reason string,
},
}
if getDiscordConfigString("embed.audit") != "" {
sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
}
fmt.Sprint(embed)
// TODO: Need to use new config for embed audit to log to a webhook
// sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
Log.Info("User " + authorname + " has been kicked from " + guild + " for " + reason)
}
func banDiscordUser(guild string, user string, username string, reason string, days int, authorname string) {
dg.GuildBanCreateWithReason(guild, user, reason, days)
// arbitrary message handling
func deleteDiscordMessage(channelID, messageID, message string) error {
//Log.Debug("Removing message: " + dpack.Message)
dg.ChannelMessageDelete(channelID, messageID)
embed := &discordgo.MessageEmbed{
Title: "User has been banned for " + strconv.Itoa(days) + " days",
Color: 0xc0392b,
Title: "Message was deleted",
Color: 0xf39c12,
Fields: []*discordgo.MessageEmbedField{
&discordgo.MessageEmbedField{
Name: "User",
Value: username,
Inline: true,
},
&discordgo.MessageEmbedField{
Name: "By",
Value: authorname,
Name: "MessageID",
Value: messageID,
Inline: true,
},
&discordgo.MessageEmbedField{
Name: "Reason",
Value: reason,
Name: "Message Content",
Value: message,
Inline: true,
},
},
}
if getDiscordConfigString("embed.audit") != "" {
sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
fmt.Sprint(embed)
// TODO: Need to use new config for embed audit to log to a webhook
// sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed)
Log.Debug("message was deleted.")
return nil
}
// send message handling
func sendDiscordMessage(s *discordgo.Session, channelID, authorID, prefix string, responseArray []string) error {
response := strings.Join(responseArray, "\n")
response = strings.Replace(response, "&user&", authorID, -1)
response = strings.Replace(response, "&prefix&", prefix, -1)
response = strings.Replace(response, "&react&", "", -1)
_, err := s.ChannelMessageSend(channelID, response)
if err != nil {
return err
}
return nil
}
func sendDiscordReaction(s *discordgo.Session, channelID string, messageID string, reactionArray []string) {
for _, reaction := range reactionArray {
Log.Debugf("sending \"%s\" as a reaction to message: %s", reaction, messageID)
err := s.MessageReactionAdd(channelID, messageID, reaction)
if err != nil {
Log.Errorf("There was an error sending the reaction. %s", err)
}
}
Log.Info("User " + authorname + " has been kicked from " + guild + " for " + reason)
}
func sendDiscordEmbed(channelID string, embed *discordgo.MessageEmbed) {
func sendDiscordEmbed(channelID string, embed *discordgo.MessageEmbed) error {
_, err := dg.ChannelMessageSendEmbed(channelID, embed)
if err != nil {
Log.Fatal("Embed send error", err)
return
Log.Fatal("Embed send error")
return err
}
return nil
}
// service handling
func startDiscordConnection() {
//Initializing Discord connection
loadConfigs(confDir)
// Initializing Discord connection
// Create a new Discord session using the provided bot token.
dg, err = discordgo.New("Bot " + getDiscordConfigString("token"))
dg, err = discordgo.New("Bot " + discordConfig.Token)
if err != nil {
Log.Fatal("error creating Discord session,", err)
return
}
// Register ready as a callback for the ready events
dg.AddHandler(readyDiscord)
// Register messageCreate as a callback for the messageCreate events.
dg.AddHandler(messageCreate)
dg.AddHandler(discordMessageHandler)
Log.Debug("Discord service connected\n")
@ -378,5 +195,29 @@ func startDiscordConnection() {
Log.Debug("Invite the bot to your server with https://discordapp.com/oauth2/authorize?client_id=" + bot.ID + "&scope=bot")
ServStat <- "discord_online"
servStat <- "discord_online"
<-stopDiscord
// properly send a shutdown to the discord server so the bot goes offline.
dg.Close()
stopDiscord <- ""
}
// when a shutdown is sent close out services properly
func stopDiscordConnection() {
Log.Infof("stopping discord connection")
stopDiscord <- ""
<-stopDiscord
Log.Infof("discord connection stopped")
shutdown <- ""
}
// This function will be called (due to AddHandler above) when the bot receives
// the "ready" event from Discord.
func readyDiscord(s *discordgo.Session, event *discordgo.Ready) {
err := s.UpdateStatus(0, discordConfig.Token)
if err != nil {
Log.Fatalf("error setting game: %s", err)
return
}
Log.Debugf("set game to: %s", discordConfig.Game)
}

50
discord_structs.go

@ -0,0 +1,50 @@
package main
// global discord config
type discord struct {
Token string `json:"token,omitempty"`
DMResp string `json:"dm_response,omitempty"`
Game string `json:"game,omitempty"`
Servers []discordServer `json:"server,omitempty"`
}
// Server config.
// Can support more than a single server.
// This contains the server info and other settings.
type discordServer struct {
ServerID string `json:"server_id,omitempty"`
Settings discordServerSettings `json:"settings,omitempty"`
ChanGroups []discordChannelGroup `json:"channel_groups,omitempty"`
}
// Server Settings
// This will be things like the prefix and permissions/channels
type discordServerSettings struct {
Prefix string `json:"prefix,omitempty"`
Channels discordServerChannels `json:"server_channels,omitempty"`
Webhooks discordServerWebhooks `json:"webhooks,omitmepty"`
Mentions mentions `json:"mentions,omitempty"`
Permissions []permGroups `json:"permissions,omitempty"`
Clearcommands bool `json:"clear_commands,omitempty"`
}
type discordServerChannels struct {
Admin string `json:"admin,omitempty"`
Log string `json:"log,omitempty"`
}
type discordServerWebhooks struct {
Log string `json:"log,omitempty"`
}
// channel config.
// Can support more than a single channel.
// This contains the channel info and other settings.
type discordChannelGroup struct {
ChannelIDs []string `json:"channels,omitempty"`
UseGlobal bool `json:"use_global,omitempty"`
Mentions mentions `json:"mentions,omitempty"`
Commands []command `json:"commands,omitempty"`
Keywords []keyword `json:"keywords,omitempty"`
Parsing parsing `json:"parsing,omitempty"`
}

96
discord_utilities.go

@ -1,96 +0,0 @@
package main
import (
"math/rand"
"strings"
Log "github.com/sirupsen/logrus"
)
const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var seededRand *rand.Rand
func discordChannelFilter(req string) bool {
if getDiscordConfigBool("channels.filter") {
if strings.Contains(getDiscordChannels(), req) {
return true
}
if getDiscordKOMChannel(req) {
return true
}
return false
}
return true
}
func discordAuthorUserPermissionCheck(authorID string) (bool, string) {
for _, groupUser := range getDiscordGroupUsers("admin") {
if groupUser == authorID {
return true, "admin"
}
}
for _, groupUser := range getDiscordGroupUsers("mod") {
if authorID == groupUser {
return true, "mod"
}
}
return false, ""
}
func discordAuthorRolePermissionCheck(roles []string) (bool, string) {
// checks for all roles the user has
for _, userRole := range roles {
// checks for all roles the admin group has
for _, groupRole := range getDiscordGroupRoles("admin") {
if userRole == groupRole {
return true, "admin"
}
}
// checks for all roles the admin group has
for _, groupRole := range getDiscordGroupRoles("mod") {
if userRole == groupRole {
return true, "mod"
}
}
}
return false, ""
}
func discordMessageHandler(dpack DataPackage) {
Log.Debug("In discord message handler")
// If the string doesn't have the prefix parse as text, if it does parse as a command.
if !strings.HasPrefix(dpack.Message, getDiscordConfigString("prefix")) {
Log.Debug("checking keywords")
dpack.MsgTye = "keyword"
if discordChannelFilter(dpack.ChannelID) {
Log.Debug("No prefix was found parsing for keywords.")
parseKeyword(dpack)
}
} else {
dpack.Message = strings.TrimPrefix(dpack.Message, getDiscordConfigString("prefix"))
dpack.MsgTye = "command"
Log.Debug("Checking commands")
// if there is a prefix check permissions on the user and run commands per group.
if dpack.Perms {
if dpack.Group == "admin" {
parseAdminCommand(dpack)
parseModCommand(dpack)
}
if dpack.Group == "mod" {
parseModCommand(dpack)
}
}
// parse commands for matches
Log.Debug("Prefix was found parsing for commands.")
parseCommand(dpack)
}
}
func discordImageRandGen() string {
b := make([]byte, 12)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}

8
go.mod

@ -7,6 +7,7 @@ require (
github.com/coreos/etcd v3.3.13+incompatible // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/goccy/go-yaml v1.2.0
github.com/h2non/filetype v1.0.8
github.com/husio/irc v0.0.0-20150308150232-bcf322335678
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect