├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── config.go ├── config_examples.go ├── config_handlers.go ├── configs ├── .gitignore ├── discord │ ├── .gitignore │ └── example-bot │ │ ├── example-server │ │ └── example-server.yml │ │ └── example.yml ├── irc │ ├── .gitignore │ └── example-bot │ │ └── irc.example.yml ├── parkertron.example.yml └── slack │ ├── .gitignore │ └── example-bot │ └── slack.example.yml ├── discord.go ├── discord_structs.go ├── go.mod ├── go.sum ├── images └── parkertron_logo.png ├── irc.go ├── irc_structs.go ├── parkertron.go ├── parsing.go ├── parsing_structs.go └── slack.go /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | .vscode/ 3 | .idea/ 4 | 5 | parkertron 6 | 7 | Gopkg\.lock 8 | 9 | configs/ 10 | pkg/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.14.x 5 | - master 6 | 7 | dist: bionic 8 | addons: 9 | apt: 10 | packages: 11 | - libleptonica-dev 12 | - tesseract-ocr 13 | - tesseract-ocr-eng 14 | - libtesseract-dev 15 | 16 | deploy: 17 | provider: releases 18 | api_key: "GITHUB OAUTH TOKEN" 19 | file: "parkertron" 20 | skip_cleanup: true 21 | on: 22 | tags: true 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ---------------------------------- 2 | # parkertron dockerfile 3 | # ---------------------------------- 4 | 5 | FROM golang:1.22-bookworm 6 | 7 | COPY . /parkertron 8 | 9 | WORKDIR /parkertron 10 | 11 | RUN apt update -y \ 12 | && apt install -y tesseract-ocr tesseract-ocr-eng libtesseract-dev \ 13 | && go mod tidy \ 14 | && go build -o parkertron 15 | 16 | FROM debian:bookworm-slim 17 | 18 | RUN apt update -y \ 19 | && apt install -y iproute2 ca-certificates libtesseract-dev tesseract-ocr-eng 20 | 21 | WORKDIR /app/ 22 | 23 | COPY --from=0 /parkertron/parkertron /app/ 24 | 25 | VOLUME /app/configs 26 | VOLUME /app/logs 27 | 28 | CMD ["./parkertron"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Michael Parker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # parkertron 3 | 4 | ![Parkertron logo](images/parkertron_logo.png) 5 | 6 | A simple discord chat bot with a simple configuration. Written using [discordgo](https://github.com/bwmarrin/discordgo). 7 | 8 | 9 | # Large changes to the keyword/command configs 10 | 11 | This most recent update will break your current keyword and command configs by moving the response one level deeper. This is to allow for reactions to be added on keywords. The bot can also mention a user for matched using the &user& string in the keyword response. 12 | 13 | Reactions need to be copied in as a unicode emoji character. 14 | 15 | ### Old: 16 | ``` 17 | keyword: 18 | help: 19 | - "Please check the github page at " 20 | - "The default config is a good example of how to set commands up. Try `.help command`" 21 | - "My base chat parsing function is also available. Try `.help keyword` for more info" 22 | ``` 23 | 24 | ### New: 25 | ``` 26 | keyword: 27 | help: 28 | reaction: 29 | - "💪" 30 | response: 31 | - "&user& Please check the github page at " 32 | - "The default config is a good example of how to set commands up. Try `.help command`" 33 | - "My base chat parsing function is also available. Try `.help keyword` for more info" 34 | ``` 35 | 36 | 37 | ### Requirements: 38 | tesseract-ocr w/ english training files (May support other languages but has not been tested.) 39 | libleptonica (for tesseract) 40 | 41 | Working on adding other services and additions. 42 | 43 | The checklist so far 44 | 45 | - support multiple services 46 | - [x] Discord 47 | - [ ] Slack 48 | - [x] IRC 49 | 50 | #### Discord specific support 51 | - [ ] Support @mentions for the bot 52 | - [ ] Use @mentions for other users 53 | - [ ] Watch for @mentions on groups 54 | - [ ] Respond with multi-line output in a single message 55 | 56 | #### IRC specific support 57 | - [x] Logging into service and validating 58 | - [x] Create account on a server 59 | - [x] Freenode 60 | - [ ] Others 61 | 62 | #### General support 63 | - [x] Get inbound messages 64 | - [x] Listen to specific channels 65 | - [x] per-channel configs 66 | - [x] Listen for mentions 67 | - [ ] respond according to context 68 | 69 | - [x] Respond to inbound messages 70 | - [x] respond to commands with prefix 71 | - [x] respond to key words/phrases 72 | - [x] Comma separated lists 73 | - [ ] Separate server commands 74 | 75 | - [x] Image parsing 76 | - [x] image from url support (others may work) 77 | - [x] png support 78 | - [x] jpg support 79 | - [x] direct embedded images 80 | 81 | - [x] Respond with correct output from configs 82 | 83 | - [x] Impliment blacklist/whitelist mode (Blacklist by User ID only) 84 | 85 | - [ ] Mitigate spam with cooldown per user/channel/global 86 | - [ ] global cooldown 87 | - [ ] channel cooldown 88 | - [ ] user cooldown 89 | 90 | - Permissions 91 | - Server own gets all perms 92 | - [ ] Permissions management 93 | 94 | - logging 95 | - [ ] log user join/leave 96 | - [x] log chats (only logs channels it is watching to cut minimize logging) 97 | - [ ] log edits (show original and edited) 98 | - [ ] log chats/edits to separate files/folders 99 | 100 | - [ ] Join voice channels 101 | - [ ] Play audio from links 102 | 103 | 104 | So far I have the chat bot part down with no limiting or administration. 105 | 106 | Configuration is done in yaml/json. 107 | If you have a Bot account already you can add the token and client ID's on your own. 108 | If you don't you will need to set your own account up. 109 | 110 | The "owner" option in the configs is basically a super admin that will not be able to be blacklisted. 111 | 112 | The prefix is the command prefix and is customizable. 113 | Set to "." by default it can be changed to whatever you want. 114 | 115 | 116 | The Commands set up is simple and is also in json. 117 | See the commands.json for examples. 118 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/fsnotify/fsnotify" 15 | "github.com/goccy/go-yaml" 16 | ) 17 | 18 | var ( 19 | // stores info on files for easier loading 20 | files confFiles 21 | 22 | // literally to make sure files load in order 23 | fileLoad = make(chan string) 24 | ) 25 | 26 | type confFiles struct { 27 | Files []confFile 28 | } 29 | 30 | type confFile struct { 31 | Location string // full file path can be a dir or file 32 | Context string // conf, botConf, serverConf 33 | Service string // parkertron, discord, irc, slack 34 | BotName string // variable based on folder name 35 | } 36 | 37 | func initConfig(confDir string) (err error) { 38 | if err = loadConfDirs(confDir); err != nil { 39 | return nil 40 | } 41 | 42 | Log.Debug(files) 43 | 44 | // sort all files to load in the correct order. 45 | files = fileSort() 46 | 47 | Log.Debug(files) 48 | 49 | // for all files pass it to fsnotify. 50 | Log.Debugf("loading files into file watcher") 51 | for _, file := range files.Files { 52 | go loadNWatch(file) 53 | <-fileLoad 54 | } 55 | 56 | Log.Debugf("Files that were loaded.") 57 | for _, v := range files.Files { 58 | Log.Debugf("%+v", v) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func fileSort() (newFiles confFiles) { 65 | // sort bot config 66 | for _, file := range files.Files { 67 | if file.Context == "conf" { 68 | newFiles.Files = append(newFiles.Files, file) 69 | } 70 | } 71 | 72 | // sort server config 73 | for _, file := range files.Files { 74 | if file.Context == "botConf" { 75 | newFiles.Files = append(newFiles.Files, file) 76 | } 77 | } 78 | 79 | // sort channel config 80 | for _, file := range files.Files { 81 | if file.Context == "serverConf" { 82 | newFiles.Files = append(newFiles.Files, file) 83 | } 84 | } 85 | return 86 | } 87 | 88 | func loadConfDirs(confdir string) (err error) { 89 | cleanConfDir := path.Clean(confdir) 90 | 91 | // Log.Debugf("reading from %s", cleanConfDir) 92 | confFullPath, err := filepath.Abs(cleanConfDir) 93 | if err != nil { 94 | Log.Errorf("error converting path %s\n", err) 95 | } 96 | 97 | // walk config dir supplied in startup single dir deep. 98 | err = filepath.Walk(confDir, func(fpath string, info os.FileInfo, err error) error { 99 | // if there are errors log the error 100 | if err != nil { 101 | Log.Infof("prevent panic by handling failure accessing a path %q: %+v\n", fpath, err) 102 | return err 103 | } 104 | 105 | // Log.Debugf("walking '%s'", fpath) 106 | 107 | // if an object has example in the name skip it 108 | if strings.Contains(info.Name(), "example") || strings.HasPrefix(info.Name(), ".") { 109 | return nil 110 | } 111 | 112 | fileFullPath, err := filepath.Abs(fpath) 113 | if err != nil { 114 | Log.Errorf("error converting path %s\n", err) 115 | } 116 | 117 | // Log.Debugf("passing '%s' to depthCounter", fpath) 118 | depth, err := depthCounter(confFullPath, fileFullPath) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | // don't add to the file struct if it's a folder 124 | if info.IsDir() { 125 | return nil 126 | } 127 | 128 | // Log.Debugf(fpath) 129 | if len(strings.Split(fpath, "/")) >= 4 { 130 | // Log.Debug(strings.Split(fpath, "/")[2]) 131 | files.Files = append(files.Files, confFile{fileFullPath, getFileType(depth), getFileService(fileFullPath), strings.Split(fpath, "/")[2]}) 132 | } else { 133 | files.Files = append(files.Files, confFile{fileFullPath, getFileType(depth), getFileService(fileFullPath), ""}) 134 | } 135 | 136 | return nil 137 | }) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func depthCounter(confFullPath, fileFullPath string) (depth int, err error) { 146 | // Log.Debugf("checking on file %s\n", fileFullPath) 147 | // get depth of folder/file 148 | for depth := 1; depth <= 4; depth++ { 149 | // Log.Debugf("checking on %d", depth) 150 | // each /* is another file depth. 151 | match, err := path.Match(confFullPath+strings.Repeat("/*", depth), fileFullPath) 152 | if err != nil { 153 | return 0, err 154 | } 155 | if match { 156 | // Log.Debugf("Match on depth %d", depth) 157 | return depth, nil 158 | } 159 | } 160 | 161 | return 0, nil 162 | } 163 | 164 | func getFileType(depth int) (fileType string) { 165 | switch depth { 166 | case 1: 167 | fileType = "conf" 168 | case 3: 169 | fileType = "botConf" 170 | case 4: 171 | fileType = "serverConf" 172 | } 173 | 174 | return fileType 175 | } 176 | 177 | func getFileService(filePath string) (service string) { 178 | if strings.Contains(filePath, "discord") { 179 | service = "discord" 180 | } else if strings.Contains(filePath, "irc") { 181 | service = "irc" 182 | } else if strings.Contains(filePath, "slack") { 183 | service = "slack" 184 | } else { 185 | service = "parkertron" 186 | } 187 | 188 | return service 189 | } 190 | 191 | func loadNWatch(file confFile) { 192 | Log.Debugf("Loading file") 193 | if err := loadConf(file); err != nil { 194 | Log.Error(err) 195 | } 196 | 197 | Log.Debugf("Setting up watcher") 198 | watcher, err := fsnotify.NewWatcher() 199 | if err != nil { 200 | Log.Errorf("%+v", err) 201 | return 202 | } 203 | 204 | Log.Debugf("defer closing watcher") 205 | defer watcher.Close() 206 | 207 | done := make(chan bool) 208 | go func() { 209 | for { 210 | select { 211 | case event := <-watcher.Events: 212 | switch event.Op { 213 | case fsnotify.Write: 214 | Log.Infof("file changed: %s", event.Name) 215 | if err := loadConf(file); err != nil { 216 | Log.Errorf("%+v", err) 217 | } 218 | } 219 | case err := <-watcher.Errors: 220 | Log.Errorf("%+v", err) 221 | } 222 | } 223 | }() 224 | 225 | if err := watcher.Add(file.Location); err != nil { 226 | Log.Error(err) 227 | } 228 | 229 | fileLoad <- "" 230 | <-done 231 | } 232 | 233 | func loadConf(conf confFile) (err error) { 234 | switch conf.Service { 235 | case "parkertron": 236 | if err = loadFromFile(conf.Location, &botConfig); err != nil { 237 | Log.Error() 238 | } 239 | 240 | // discord conf loading 241 | case "discord": 242 | if conf.Context == "botConf" { 243 | Log.Debugf("loading config for discord bot %s", conf.BotName) 244 | // set up new temp var for the bot 245 | var tempBot discordBot 246 | tempBot.BotName = conf.BotName 247 | 248 | if err = loadFromFile(conf.Location, &tempBot); err != nil { 249 | Log.Errorf("there was an error loading configs") 250 | Log.Error(err) 251 | return 252 | } 253 | for bid, bot := range discordGlobal.Bots { 254 | if bot.BotName == conf.BotName { 255 | discordGlobal.Bots[bid].Config.Game = tempBot.Config.Game 256 | discordGlobal.Bots[bid].Config.DMResp = tempBot.Config.DMResp 257 | return 258 | } 259 | } 260 | 261 | // add bot to discord bots array. 262 | discordGlobal.Bots = append(discordGlobal.Bots, tempBot) 263 | 264 | } else if conf.Context == "serverConf" { 265 | Log.Debugf("loading discord server config for %s", conf.BotName) 266 | // set up new temp var for the server 267 | var tempServer discordServer 268 | 269 | if err = loadFromFile(conf.Location, &tempServer); err != nil { 270 | Log.Error(err) 271 | return 272 | } 273 | 274 | for i := range tempServer.ChanGroups { 275 | Log.Infof("channel groups %+v", tempServer.ChanGroups[i].ChannelIDs) 276 | } 277 | 278 | for bid, bot := range discordGlobal.Bots { 279 | if bot.BotName == conf.BotName { 280 | for sid, server := range discordGlobal.Bots[bid].Servers { 281 | // if the server exists drop it and re-append config 282 | if server.ServerID == tempServer.ServerID { 283 | discordGlobal.Bots[bid].Servers[sid].ChanGroups = tempServer.ChanGroups 284 | discordGlobal.Bots[bid].Servers[sid].Config = tempServer.Config 285 | discordGlobal.Bots[bid].Servers[sid].Permissions = tempServer.Permissions 286 | return 287 | } 288 | } 289 | // if the server isn't in the list append it. 290 | discordGlobal.Bots[bid].Servers = append(discordGlobal.Bots[bid].Servers, tempServer) 291 | Log.Debugf("loaded server %s for bot %s", tempServer.ServerID, bot.BotName) 292 | } 293 | } 294 | } 295 | // irc conf loading 296 | case "irc": 297 | if conf.Context == "botConf" { 298 | Log.Debugf("loading config for irc bot %s", conf.BotName) 299 | 300 | var tempBot ircBot 301 | tempBot.BotName = conf.BotName 302 | 303 | // if there is an error loading the config return 304 | if err = loadFromFile(conf.Location, &tempBot); err != nil { 305 | return 306 | } 307 | 308 | // add bot to irc bots array 309 | ircGlobal.Bots = append(ircGlobal.Bots, tempBot) 310 | } 311 | // slack config loading 312 | case "slack": 313 | 314 | } 315 | 316 | return nil 317 | } 318 | 319 | // LoadConfig loads configs from a file to an interface 320 | func loadFromFile(file string, iface interface{}) (err error) { 321 | if strings.HasSuffix(file, ".json") { // if json file 322 | Log.Debug("loading json file") 323 | if err = readJSONFromFile(file, iface); err != nil { 324 | Log.Error(err) 325 | return 326 | } 327 | } else if strings.HasSuffix(file, ".yml") || strings.HasSuffix(file, ".yaml") { // if yaml file 328 | Log.Debugf("loading yaml file %s", file) 329 | if err = readYamlFromFile(file, iface); err != nil { 330 | Log.Error(err) 331 | return 332 | } 333 | // Log.Debugf("interface %+v", iface) 334 | } else { 335 | return errors.New("no supported file type located") 336 | } 337 | 338 | return nil 339 | } 340 | 341 | // SaveConfig saves interfaces to a file 342 | func saveConfig(file string, iface interface{}) error { 343 | // Log.Printf("converting struct data to bytesfor %s", file) 344 | bytes, err := json.MarshalIndent(iface, "", " ") 345 | if err != nil { 346 | return fmt.Errorf("there was an error converting the user data to json") 347 | } 348 | 349 | // Log.Printf("writing bytes to file") 350 | if err := writeJSONToFile(file, bytes); err != nil { 351 | return err 352 | } 353 | 354 | return nil 355 | } 356 | 357 | // File management 358 | func writeJSONToFile(file string, iface interface{}) (err error) { 359 | jdata, err := json.MarshalIndent(iface, "", " ") 360 | if err != nil { 361 | return 362 | } 363 | 364 | // create a file with a supplied name 365 | if jsonFile, err := os.Create(file); err != nil { 366 | return err 367 | } else if _, err = jsonFile.Write(jdata); err != nil { 368 | return err 369 | } 370 | 371 | return nil 372 | } 373 | 374 | func readJSONFromFile(file string, iface interface{}) error { 375 | // Log.Printf("opening json file\n") 376 | jsonFile, err := os.Open(file) 377 | // if we os.Open returns an error then handle it 378 | if err != nil { 379 | return err 380 | } 381 | 382 | // Log.Printf("holding file open\n") 383 | // defer the closing of our jsonFile so that we can parse it later on 384 | defer func() { 385 | if err := jsonFile.Close(); err != nil { 386 | Log.Printf("Error while closing JSON file: %+v", err) 387 | } 388 | }() 389 | 390 | // Log.Printf("reading file\n") 391 | // read our opened xmlFile as a byte array. 392 | byteValue, _ := ioutil.ReadAll(jsonFile) 393 | if err = json.Unmarshal(byteValue, iface); err != nil { 394 | return err 395 | } 396 | 397 | // return the json byte value. 398 | return nil 399 | } 400 | 401 | func writeYamlToFile(file string, iface interface{}) (err error) { 402 | ydata, err := yaml.Marshal(iface) 403 | if err != nil { 404 | return 405 | } 406 | 407 | // create a file with a supplied name 408 | yamlFile, err := os.Create(file) 409 | if err != nil { 410 | return 411 | } 412 | 413 | if _, err = yamlFile.Write(ydata); err != nil { 414 | return 415 | } 416 | 417 | return 418 | } 419 | 420 | func readYamlFromFile(file string, iface interface{}) (err error) { 421 | // Log.Debugf("opening yaml file\n") 422 | yamlFile, err := os.Open(file) 423 | if err != nil { 424 | return 425 | } 426 | 427 | // Log.Debugf("holding file open\n") 428 | // defer the closing of our jsonFile so that we can parse it later on 429 | defer func() { 430 | if err := yamlFile.Close(); err != nil { 431 | return 432 | } 433 | }() 434 | 435 | // Log.Printf("reading file\n") 436 | byteValue, _ := ioutil.ReadAll(yamlFile) 437 | if err = yaml.Unmarshal(byteValue, iface); err != nil { 438 | return 439 | } 440 | 441 | return 442 | } 443 | 444 | // Exists reports whether the named file or directory exists. 445 | func createIfDoesntExist(name string) (err error) { 446 | p, file := path.Split(name) 447 | 448 | // if confdir exists carry on 449 | if _, err := os.Stat(name); err != nil { 450 | // if file doesn't exist 451 | if os.IsNotExist(err) { 452 | // stat 453 | if _, err = os.Stat(name); err != nil { 454 | if file == "" { 455 | if err = os.Mkdir(p, 0755); err != nil { 456 | } 457 | } else { 458 | if fileCheck, err := os.OpenFile(name, os.O_RDONLY|os.O_CREATE, 0644); err != nil { 459 | } else { 460 | if err := fileCheck.Close(); err != nil { 461 | return err 462 | } 463 | } 464 | } 465 | } 466 | } 467 | } 468 | return 469 | } 470 | 471 | func loadInitConfig(confDir, conf, verbose string) (botConfig parkertron, err error) { 472 | // All of this is pre-Logrus init 473 | if verbose == "debug" { 474 | log.Printf("Checking for dir at %s", confDir) 475 | } 476 | 477 | // if the config dir doesn't exist make it 478 | if err := createIfDoesntExist(confDir); err != nil { 479 | return botConfig, err 480 | // if we can't make a confdir log a fatal error. 481 | } 482 | 483 | // if confdir exists carry on 484 | info, err := os.Stat(confDir) 485 | if err != nil { 486 | return botConfig, err 487 | } 488 | 489 | // if not a directory error out 490 | if !info.IsDir() { 491 | return botConfig, errors.New("given file not directory") 492 | } 493 | 494 | if verbose == "debug" { 495 | log.Printf("loading initial bot config at %s%s", confDir, conf) 496 | } 497 | 498 | // if config doesn't exist make it. 499 | if err = createIfDoesntExist(confDir + conf); err != nil { 500 | if verbose == "debug" { 501 | log.Printf("creating config %s", confDir+conf) 502 | if err := createExampleBotConfig(confDir, conf, verbose); err != nil { 503 | return parkertron{}, err 504 | } 505 | } 506 | } 507 | 508 | // if conf file exists carry on 509 | if verbose == "debug" { 510 | log.Printf("file %s%s exists", confDir, conf) 511 | } 512 | 513 | // if confdir exists carry on 514 | file, err := os.Stat(confDir + conf) 515 | if err != nil { 516 | return botConfig, err 517 | } 518 | 519 | if file.Size() == 0 { 520 | if err := createExampleBotConfig(confDir, conf, verbose); err != nil { 521 | return parkertron{}, err 522 | } 523 | } 524 | 525 | if strings.HasSuffix(conf, "yaml") || strings.HasSuffix(conf, "yml") { 526 | if err = readYamlFromFile(confDir+conf, &botConfig); err != nil { 527 | return 528 | } 529 | } else if strings.HasSuffix(conf, "json") { 530 | if err = readJSONFromFile(confDir+conf, &botConfig); err != nil { 531 | return botConfig, err 532 | } 533 | } 534 | 535 | return 536 | } 537 | -------------------------------------------------------------------------------- /config_examples.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func createExampleBotConfig(confDir, conf, verbose string) (err error) { 10 | newBot := parkertron{ 11 | Services: []string{"discord"}, 12 | Log: logConf{ 13 | Level: "info", 14 | Location: "logs/", 15 | }, 16 | Parsing: botParseConfig{ 17 | Max: 5, 18 | Response: []string{"There were too many logs to read &user&. Please post 5 or less."}, 19 | }, 20 | } 21 | 22 | // create file 23 | fileCheck, err := os.OpenFile(confDir+conf, os.O_RDONLY|os.O_CREATE, 0644) 24 | if err != nil { 25 | return 26 | } 27 | if err := fileCheck.Close(); err != nil { 28 | return err 29 | } 30 | 31 | file, err := os.Stat(confDir + conf) 32 | if err != nil { 33 | return 34 | } 35 | 36 | if file.Size() == 0 { 37 | if strings.HasSuffix(conf, "yaml") || strings.HasSuffix(conf, "yml") { 38 | // if config is yaml 39 | if verbose == "debug" { 40 | log.Printf("file %s%s is yaml", confDir, conf) 41 | } 42 | 43 | if verbose == "debug" { 44 | log.Printf("writing to %s%s", confDir, conf) 45 | } 46 | if err = writeYamlToFile(confDir+conf, newBot); err != nil { 47 | return 48 | } 49 | } else if strings.HasSuffix(conf, "json") { 50 | // if config is json 51 | if verbose == "debug" { 52 | log.Printf("file %s%s is json", confDir, conf) 53 | } 54 | 55 | if verbose == "debug" { 56 | log.Printf("writing to %s%s", confDir, conf) 57 | } 58 | 59 | if err = writeJSONToFile(confDir+conf, newBot); err != nil { 60 | return 61 | } 62 | } 63 | } 64 | 65 | return 66 | } 67 | 68 | func createExampleDiscordConfig(confDir string) (err error) { 69 | // if the config dir doesn't exist make it 70 | Log.Debugf("creating example config folder %s if it doesn't exist", confDir) 71 | if err = createIfDoesntExist(confDir); err != nil { 72 | return 73 | } 74 | 75 | // if the config dir doesn't exist make it 76 | Log.Debugf("creating example config folder %s if it doesn't exist", confDir+"example-bot/") 77 | if err = createIfDoesntExist(confDir + "example-bot/"); err != nil { 78 | 79 | } 80 | 81 | // if the config dir doesn't exist make it 82 | Log.Debugf("creating example config file %s if it doesn't exist", confDir+"example-bot/example.yml") 83 | if err = createIfDoesntExist(confDir + "example-bot/example.yml"); err != nil { 84 | return 85 | } 86 | 87 | newDiscordBot := discordBot{} 88 | 89 | newDiscordBotConfig := discordBotConfig{ 90 | Token: "An example token", 91 | Game: "Supporting Humans", 92 | DMResp: responseArray{ 93 | Response: []string{""}, 94 | Reaction: []string{""}, 95 | }, 96 | } 97 | 98 | Log.Debugf("writing example config to file %s", confDir+"example-bot/example.yml") 99 | 100 | newDiscordBot.Config = newDiscordBotConfig 101 | 102 | if err = writeYamlToFile(confDir+"example-bot/example.yml", newDiscordBot); err != nil { 103 | return 104 | } 105 | 106 | // if the config dir doesn't exist make it 107 | Log.Debugf("creating example server config folder %s if it doesn't exist", confDir+"example-bot/example-server/") 108 | if err = createIfDoesntExist(confDir + "example-bot/example-server/"); err != nil { 109 | return 110 | } 111 | 112 | newServer := discordServer{ 113 | ServerID: "a-server-id", 114 | Config: discordServerConfig{ 115 | Prefix: ".", 116 | Clear: true, 117 | }, 118 | ChanGroups: []channelGroup{ 119 | { 120 | ChannelIDs: []string{ 121 | "a-channel-id", 122 | "another-channel-id", 123 | }, 124 | Mentions: mentions{ 125 | Ping: responseArray{ 126 | Reaction: []string{"👋"}, 127 | Response: []string{"I see I was pinged.", "Please just post the issue you are having", " Or you can gather your logs and post them"}, 128 | }, 129 | Mention: responseArray{ 130 | Reaction: []string{"👋"}, 131 | Response: []string{""}, 132 | }, 133 | }, 134 | Commands: []command{ 135 | { 136 | Command: "example", 137 | Response: []string{"This is the response to the &prefix&example command"}, 138 | }, 139 | }, 140 | Keywords: []keyword{ 141 | { 142 | Keyword: "example", 143 | Reaction: []string{""}, 144 | Response: []string{"I have responded to seeing the word example."}, 145 | }, 146 | }, 147 | Regex: []pattern{ 148 | { 149 | Pattern: ".*example.*", 150 | Reaction: []string{""}, 151 | Response: []string{"I have found the word example somewhere in there."}, 152 | }, 153 | }, 154 | Parsing: parsing{ 155 | Image: parsingImageConfig{ 156 | FileTypes: []string{ 157 | "png", 158 | "jpg"}, 159 | Sites: []parsingConfig{ 160 | { 161 | Name: "pastebin", 162 | URL: "'https://pastebin.com/'", 163 | Format: "'https://pastebin.com/raw/&filename&'", 164 | }, 165 | { 166 | Name: "hastebin", 167 | URL: "'https://hastebin.com/'", 168 | Format: "'https://hastebin.com/raw/&filename&'", 169 | }, 170 | }, 171 | }, 172 | Paste: parsingPasteConfig{ 173 | Sites: []parsingConfig{}, 174 | }, 175 | }, 176 | KOM: discordKickOnMention{}, 177 | }, 178 | }, 179 | Permissions: []permission{ 180 | { 181 | Group: "admin", 182 | Users: []string{}, 183 | Roles: []string{}, 184 | Blacklisted: false, 185 | }, 186 | }, 187 | Filters: []filter{ 188 | { 189 | Term: "a bad word", 190 | Reason: []string{ 191 | "the message was removed because it had 'a bad word' in it", 192 | }, 193 | }, 194 | }, 195 | } 196 | 197 | Log.Debugf("writing example server config to file %s", confDir+"example-bot/example-server/example.yml") 198 | if err = writeYamlToFile(confDir+"example-bot/example-server/example-server.yml", newServer); err != nil { 199 | return 200 | } 201 | 202 | return 203 | } 204 | 205 | func createExampleIRCConfig(confDir string) (err error) { 206 | // if the config dir doesn't exist make it 207 | Log.Debugf("creating example config folder %s if it doesn't exist", confDir) 208 | if err = createIfDoesntExist(confDir); err != nil { 209 | return 210 | } 211 | 212 | // if the config dir doesn't exist make it 213 | Log.Debugf("creating example config folder %s if it doesn't exist", confDir+"example-bot/") 214 | if err = createIfDoesntExist(confDir + "example-bot/"); err != nil { 215 | 216 | } 217 | 218 | // if the config dir doesn't exist make it 219 | Log.Debugf("creating example config file %s if it doesn't exist", confDir+"example-bot/example-bot.yml") 220 | if err = createIfDoesntExist(confDir + "example-bot/example.yml"); err != nil { 221 | return 222 | } 223 | 224 | newIrc := ircBot{ 225 | Config: ircBotConfig{ 226 | Server: ircServerConfig{ 227 | Address: "irc.freenode.net", 228 | Port: 6667, 229 | SSLEnable: true, 230 | Ident: "parkertron", 231 | Password: "Set-Your-Own", 232 | Nickname: "parkertron", 233 | RealName: "Parker McBot", 234 | }, 235 | DMResp: responseArray{ 236 | Response: []string{}, 237 | }, 238 | Prefix: ".", 239 | }, 240 | ChanGroups: []channelGroup{ 241 | { 242 | ChannelIDs: []string{ 243 | "a-channel-name", 244 | "another-channel-name", 245 | }, 246 | Mentions: mentions{ 247 | Ping: responseArray{ 248 | Response: []string{}, 249 | }, 250 | Mention: responseArray{ 251 | Response: []string{}, 252 | }, 253 | }, 254 | Commands: []command{ 255 | { 256 | Command: "example", 257 | Reaction: []string{""}, 258 | Response: []string{"This is the response to the &prefix&example command"}, 259 | }, 260 | }, 261 | Keywords: []keyword{ 262 | { 263 | Keyword: "example", 264 | Response: []string{"I have responded to seeing the word example."}, 265 | }, 266 | }, 267 | Regex: []pattern{ 268 | { 269 | Pattern: ".*example.*", 270 | Reaction: []string{""}, 271 | Response: []string{"I have found the word example somewhere in there."}, 272 | }, 273 | }, 274 | Parsing: parsing{ 275 | Image: parsingImageConfig{ 276 | FileTypes: []string{}, 277 | Sites: []parsingConfig{}, 278 | }, 279 | Paste: parsingPasteConfig{ 280 | Sites: []parsingConfig{}, 281 | }, 282 | }, 283 | Permissions: []permission{ 284 | { 285 | Group: "admin", 286 | Users: []string{}, 287 | Roles: []string{}, 288 | Blacklisted: false, 289 | }, 290 | }, 291 | }, 292 | }, 293 | } 294 | 295 | Log.Debugf("writing example config to file %s", confDir+"example-bot/example-bot.yml") 296 | 297 | if err = writeYamlToFile(confDir+"example-bot/example.yml", newIrc); err != nil { 298 | return 299 | } 300 | 301 | return 302 | } 303 | -------------------------------------------------------------------------------- /config_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func getBlacklist(inService, botName, inServer, inChannel string) (blacklist []string) { 4 | perms := []permission{} 5 | 6 | switch inService { 7 | case "discord": 8 | for _, bot := range discordGlobal.Bots { 9 | if bot.BotName == botName { 10 | for _, server := range bot.Servers { 11 | if inServer == server.ServerID { 12 | perms = server.Permissions 13 | } 14 | } 15 | } 16 | } 17 | case "irc": 18 | for _, group := range getChannelGroups(inService, botName, inServer, inChannel) { 19 | for _, channel := range group.ChannelIDs { 20 | if channel == inChannel { 21 | perms = group.Permissions 22 | } 23 | } 24 | } 25 | default: 26 | } 27 | 28 | // load users that are in blacklisted groups 29 | for _, perm := range perms { 30 | if perm.Blacklisted { 31 | for _, user := range perm.Users { 32 | blacklist = append(blacklist, user) 33 | } 34 | } 35 | } 36 | 37 | return 38 | } 39 | 40 | func getChannels(inService, botName, inServer string) (channels []string) { 41 | Log.Debugf("service: %s, bot: %s, server: %s", inService, botName, inServer) 42 | switch inService { 43 | case "discord": 44 | for bid := range discordGlobal.Bots { 45 | Log.Debugf("checking for bot: %s", discordGlobal.Bots[bid].BotName) 46 | if botName == discordGlobal.Bots[bid].BotName { 47 | Log.Debugf("matched for %s", discordGlobal.Bots[bid].BotName) 48 | for sid := range discordGlobal.Bots[bid].Servers { 49 | Log.Debugf("checking for server: %s", discordGlobal.Bots[bid].Servers[sid].ServerID) 50 | if inServer == discordGlobal.Bots[bid].Servers[sid].ServerID { 51 | Log.Debugf("matched for %s", discordGlobal.Bots[bid].Servers[sid].ServerID) 52 | for gid := range discordGlobal.Bots[bid].Servers[sid].ChanGroups { 53 | Log.Debugf("%s", discordGlobal.Bots[bid].Servers[sid].ChanGroups[gid].ChannelIDs) 54 | for _, channel := range discordGlobal.Bots[bid].Servers[sid].ChanGroups[gid].ChannelIDs { 55 | channels = append(channels, channel) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | case "irc": 63 | for _, bot := range ircGlobal.Bots { 64 | if bot.BotName == botName { 65 | for _, group := range bot.ChanGroups { 66 | for _, channel := range group.ChannelIDs { 67 | channels = append(channels, channel) 68 | } 69 | } 70 | } 71 | } 72 | default: 73 | } 74 | 75 | Log.Debugf("handing channels back with a value of %s", channels) 76 | 77 | return 78 | } 79 | 80 | func getChannelGroups(inService, botName, inServer, inChannel string) (chanGroups []channelGroup) { 81 | switch inService { 82 | case "discord": 83 | for _, bot := range discordGlobal.Bots { 84 | if bot.BotName == botName { 85 | for _, server := range bot.Servers { 86 | if inServer == server.ServerID { 87 | chanGroups = server.ChanGroups 88 | } 89 | } 90 | } 91 | } 92 | case "irc": 93 | for _, bot := range ircGlobal.Bots { 94 | if bot.BotName == botName { 95 | chanGroups = bot.ChanGroups 96 | } 97 | } 98 | default: 99 | } 100 | 101 | return 102 | } 103 | 104 | func getCommands(inService, botName, inServer, inChannel string) (commands []command) { 105 | // prep stuff for passing to the parser 106 | for _, group := range getChannelGroups(inService, botName, inServer, inChannel) { 107 | for _, channel := range group.ChannelIDs { 108 | if inChannel == channel { 109 | for _, command := range group.Commands { 110 | commands = append(commands, command) 111 | } 112 | } 113 | } 114 | } 115 | 116 | return 117 | } 118 | 119 | func getKeywords(inService, botName, inServer, inChannel string) (keywords []keyword) { 120 | // prep stuff for passing to the parser 121 | for _, group := range getChannelGroups(inService, botName, inServer, inChannel) { 122 | for _, channel := range group.ChannelIDs { 123 | if inChannel == channel { 124 | for _, keyword := range group.Keywords { 125 | keywords = append(keywords, keyword) 126 | } 127 | } 128 | } 129 | } 130 | 131 | return 132 | } 133 | 134 | func getRegexPatterns(inService, botName, inServer, inChannel string) (patterns []pattern) { 135 | // prep stuff for passing to the parser 136 | for _, group := range getChannelGroups(inService, botName, inServer, inChannel) { 137 | for _, channel := range group.ChannelIDs { 138 | if inChannel == channel { 139 | for _, pat := range group.Regex { 140 | patterns = append(patterns, pat) 141 | } 142 | } 143 | } 144 | } 145 | 146 | return 147 | } 148 | 149 | func getMentions(inService, botName, inServer, inChannel string) (ping, mention responseArray) { 150 | switch inService { 151 | case "discord": 152 | for _, bot := range discordGlobal.Bots { 153 | if bot.BotName == botName { 154 | for _, server := range bot.Servers { 155 | if inServer == server.ServerID { 156 | if inChannel == "DirectMessage" { 157 | mention = bot.Config.DMResp 158 | } else { 159 | for _, group := range server.ChanGroups { 160 | for _, channel := range group.ChannelIDs { 161 | if inChannel == channel { 162 | Log.Debugf("bot was mentioned on channel %s", channel) 163 | Log.Debugf("ping resp %s", group.Mentions.Ping) 164 | Log.Debugf("mention resp %s", group.Mentions.Mention) 165 | ping = group.Mentions.Ping 166 | mention = group.Mentions.Mention 167 | return 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | case "irc": 177 | for _, bot := range ircGlobal.Bots { 178 | if bot.BotName == botName { 179 | if inChannel == bot.Config.Server.Nickname { 180 | mention = bot.Config.DMResp 181 | } else { 182 | for _, group := range bot.ChanGroups { 183 | for _, channel := range group.ChannelIDs { 184 | if inChannel == channel { 185 | ping = group.Mentions.Ping 186 | mention = group.Mentions.Mention 187 | return 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | default: 195 | } 196 | 197 | return 198 | } 199 | 200 | func getParsing(inService, botName, inServer, inChannel string) (parseConf parsing) { 201 | // prep stuff for passing to the parser 202 | for _, group := range getChannelGroups(inService, botName, inServer, inChannel) { 203 | for _, channel := range group.ChannelIDs { 204 | if inChannel == channel { 205 | parseConf = group.Parsing 206 | } 207 | } 208 | } 209 | 210 | return 211 | } 212 | 213 | func getFilter(inService, botName, inServer string) (filters []filter) { 214 | // prep stuff for passing to the parser 215 | switch inService { 216 | case "discord": 217 | for _, bot := range discordGlobal.Bots { 218 | if bot.BotName == botName { 219 | for _, server := range bot.Servers { 220 | if inServer == server.ServerID { 221 | filters = server.Filters 222 | } 223 | } 224 | } 225 | } 226 | case "irc": 227 | default: 228 | } 229 | 230 | return 231 | } 232 | 233 | func getBotParseConfig() (maxLogs int, response, reaction []string, allowIP bool) { 234 | return botConfig.Parsing.Max, botConfig.Parsing.Response, botConfig.Parsing.Reaction, botConfig.Parsing.AllowIP 235 | } 236 | 237 | func getPrefix(inService, botName, inServer string) (prefix string) { 238 | switch inService { 239 | case "discord": 240 | for _, bot := range discordGlobal.Bots { 241 | if bot.BotName == botName { 242 | for _, server := range bot.Servers { 243 | if inServer == server.ServerID { 244 | prefix = server.Config.Prefix 245 | } 246 | } 247 | } 248 | } 249 | case "irc": 250 | for _, bot := range ircGlobal.Bots { 251 | if bot.BotName == botName { 252 | prefix = bot.Config.Prefix 253 | } 254 | } 255 | default: 256 | } 257 | 258 | return 259 | } 260 | 261 | func getCommandClear(inService, botName, inServer string) (clear bool) { 262 | switch inService { 263 | case "discord": 264 | for _, bot := range discordGlobal.Bots { 265 | if bot.BotName == botName { 266 | for _, server := range bot.Servers { 267 | if inServer == server.ServerID { 268 | clear = server.Config.Clear 269 | } 270 | } 271 | } 272 | } 273 | default: 274 | } 275 | 276 | return 277 | } 278 | -------------------------------------------------------------------------------- /configs/.gitignore: -------------------------------------------------------------------------------- 1 | parkertron.yml -------------------------------------------------------------------------------- /configs/discord/.gitignore: -------------------------------------------------------------------------------- 1 | parkertron/ 2 | testytron/ 3 | pterytron/ -------------------------------------------------------------------------------- /configs/discord/example-bot/example-server/example-server.yml: -------------------------------------------------------------------------------- 1 | server_id: a-server-id 2 | config: 3 | prefix: . 4 | clear_commands: true 5 | channel_groups: 6 | - channels: 7 | - a-channel-id 8 | - another-channel-id 9 | mentions: 10 | ping: 11 | reaction: 12 | - 👋 13 | response: 14 | - I see I was pinged. 15 | - Please just post the issue you are having 16 | - Or you can gather your logs and post them 17 | mention: 18 | reaction: 19 | - 👋 20 | response: 21 | - "" 22 | commands: 23 | - command: example 24 | response: 25 | - This is the response to the &prefix&example command 26 | keywords: 27 | - keyword: example 28 | reaction: 29 | - "" 30 | response: 31 | - I have responded to seeing the word example. 32 | regex: 33 | - pattern: ".*example.*" 34 | reaction: 35 | - "" 36 | response: 37 | - I have responded to matching the example regex pattern. 38 | parsing: 39 | image: 40 | filetypes: 41 | - png 42 | - jpg 43 | paste: 44 | sites: 45 | - name: pastebin 46 | url: 'https://pastebin.com/' 47 | format: 'https://pastebin.com/raw/&filename&' 48 | - name: hastebin 49 | url: 'https://hastebin.com/' 50 | format: 'https://hastebin.com/raw/&filename&' 51 | permissions: 52 | - group: admin 53 | filters: 54 | - term: a bad word 55 | reason: 56 | - the message was removed because it had 'a bad word' in it 57 | -------------------------------------------------------------------------------- /configs/discord/example-bot/example.yml: -------------------------------------------------------------------------------- 1 | config: 2 | token: An example token 3 | game: Supporting Humans 4 | dm_response: 5 | reaction: 6 | - "" 7 | response: 8 | - "" 9 | -------------------------------------------------------------------------------- /configs/irc/.gitignore: -------------------------------------------------------------------------------- 1 | freenode-parkertron/ -------------------------------------------------------------------------------- /configs/irc/example-bot/irc.example.yml: -------------------------------------------------------------------------------- 1 | config: 2 | server: 3 | address: "irc.freenode.net" 4 | port: 6667 5 | ssl: true 6 | ident: "parkertron" 7 | email: "" 8 | password: "" 9 | nickname: "parkertron" 10 | real_name: "Parkertron McBot" 11 | 12 | channel_groups: 13 | - channels: ## an array of channels. Can hold one or many. 14 | - "#a_channel_1" 15 | 16 | mentions: 17 | ping: ## bot gets pinged with no other text 18 | response: 19 | - "" 20 | mention: ## bot gets mention in a message that it doesn't match anything in 21 | response: 22 | - "" 23 | 24 | commands: ## an array of commands 25 | - command: help ## each command needs this layout or the config will fail to load. 26 | response: 27 | - "Hello, I am a bot created by `parkervcp` designed to respond to commands and keywords." 28 | reastion: 29 | - "" 30 | 31 | - command: help command 32 | response: 33 | - "You can add commands manually in the config file." 34 | 35 | - command: help keyword 36 | response: 37 | - "You can add keywords manually in the config file." 38 | 39 | keywords: ## an array of keywords 40 | - keyword: example ## each keyword needs this layout or the config will fail to load. 41 | response: 42 | - "This is an example keyword match" 43 | 44 | - keyword: hello 45 | response: 46 | - "This is an example keyword match" 47 | exact: true 48 | 49 | parsing: 50 | image: 51 | filetypes: 52 | - "png" 53 | - "jpg" 54 | 55 | sites: 56 | - url: "https://gyazo.com" 57 | format: "https://i.gyazo.com/&filename&.png" 58 | 59 | paste: 60 | sites: 61 | - name: pastebin 62 | url: "https://pastebin.com/" 63 | format: "&url&raw/&filename&" 64 | - name: hastebin 65 | url: "https://hastebin.com/" 66 | format: "&url&raw/&filename&" 67 | 68 | permissions: ## discord permissions are per-server 69 | - group: admin ## server owners automatically get all permissions. 70 | users: 71 | - "userID" 72 | 73 | - group: mod 74 | users: 75 | - "Community Moderator" 76 | 77 | - group: blasklist 78 | blacklisted: true -------------------------------------------------------------------------------- /configs/parkertron.example.yml: -------------------------------------------------------------------------------- 1 | services: ## currently the only real service is the discord service. 2 | - "discord" 3 | 4 | log: 5 | level: info 6 | location: logs/ 7 | 8 | parsing: 9 | max: 5 ## max number of files to parse (bin/image/attachment) 10 | response: 11 | - "There were too many logs to read &user&. Please post 5 or less." 12 | reaction: 13 | - "" -------------------------------------------------------------------------------- /configs/slack/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkervcp/parkertron/35393d928b61000b71f471437ca97ec18762725b/configs/slack/.gitignore -------------------------------------------------------------------------------- /configs/slack/example-bot/slack.example.yml: -------------------------------------------------------------------------------- 1 | slack: 2 | token: "" 3 | direct: 4 | response: "Please message in the main server." -------------------------------------------------------------------------------- /discord.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Thank Stroom on the discordgopher discord for helping me with embedded functions in the handlers 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/diamondburned/arikawa/v3/api" 13 | "github.com/diamondburned/arikawa/v3/discord" 14 | "github.com/diamondburned/arikawa/v3/gateway" 15 | "github.com/diamondburned/arikawa/v3/session" 16 | "github.com/diamondburned/arikawa/v3/utils/json/option" 17 | "mvdan.cc/xurls/v2" 18 | ) 19 | 20 | var ( 21 | stopDiscordServer = make(chan string) 22 | discordServerStopped = make(chan string) 23 | 24 | discordGlobal discordBase 25 | 26 | discordLoad = make(chan string) 27 | ) 28 | 29 | // This function will be called (due to AddHandler) when the bot receives 30 | // the "ready" event from Discord. 31 | func readyDiscord(botSession *session.Session, game string) { 32 | if !discord.EmojiID(985546330271252530).IsValid() { 33 | Log.Debug("emoji is invalid") 34 | } 35 | 36 | activities := []discord.Activity{ 37 | { 38 | Name: "custom", 39 | Type: 4, 40 | State: game, 41 | }, 42 | } 43 | 44 | status := &gateway.UpdatePresenceCommand{ 45 | Activities: activities, 46 | } 47 | // if there is an error setting the game log and return 48 | if err := botSession.Gateway().Send(context.Background(), status); err != nil { 49 | Log.Fatalf("error setting game: %s", err) 50 | return 51 | } 52 | 53 | Log.Debugf("set game to: %s", game) 54 | } 55 | 56 | // This function will be called (due to AddHandler) every time a new 57 | // message is created on any channel that the authenticated bot has access to. 58 | func discordMessageHandler(botSession *session.Session, messageEvent *gateway.MessageCreateEvent, botName string) { 59 | Log.Debugf("bot is %s", botName) 60 | Log.Debugf("message '%s'", messageEvent.Content) 61 | 62 | // data to send to discord 63 | var response []string 64 | var reaction []string 65 | 66 | botUser, err := botSession.Me() 67 | if err != nil { 68 | fmt.Println("error obtaining account details,", err) 69 | syscall.Exit(2) 70 | } 71 | 72 | // Ignore all messages created by bots (stops the bot uprising) 73 | if messageEvent.Author.Bot { 74 | Log.Debug("User is a bot and being ignored.") 75 | return 76 | } 77 | 78 | // get channel information 79 | channel, err := botSession.Channel(messageEvent.ChannelID) 80 | if err != nil { 81 | Log.Fatal("Channel error ", err) 82 | return 83 | } 84 | 85 | message := messageEvent.Message 86 | 87 | botID := botUser.ID 88 | 89 | guildID := channel.GuildID.String() 90 | chanID := messageEvent.ChannelID.String() 91 | 92 | // if the channel type is a thread use the parent id for the config 93 | if channel.Type == 11 { 94 | chanID = channel.ParentID.String() 95 | } 96 | 97 | Log.Debugf("prefix: %s", getPrefix("discord", botName, guildID)) 98 | 99 | // if the channel is a DM 100 | if channel.Type == 1 { 101 | _, dmResp := getMentions("discord", botName, guildID, "DirectMessage") 102 | if err := sendDiscordMessage(botSession, channel, messageEvent.Author, dmResp.Reaction, botName); err != nil { 103 | Log.Error(err) 104 | } 105 | 106 | if err := sendDiscordReaction(botSession, channel, message, dmResp.Reaction); err != nil { 107 | Log.Error(err) 108 | } 109 | 110 | return 111 | } 112 | 113 | // bot level configs for log reading 114 | maxLogs, logResponse, logReaction, allowIP := getBotParseConfig() 115 | 116 | //filter logic 117 | Log.Debug("filtering messages") 118 | if len(getFilter("discord", botName, guildID)) == 0 { 119 | Log.Debugf("no filtered terms found") 120 | } else { 121 | for _, filter := range getFilter("discord", botName, guildID) { 122 | if strings.Contains(messageEvent.Content, filter.Term) { 123 | Log.Infof("message was removed for containing %s", filter.Term) 124 | if err := deleteDiscordMessages(botSession, channel, []discord.MessageID{0: messageEvent.ID}, ""); err != nil { 125 | Log.Error(err) 126 | } 127 | 128 | if err := sendDiscordMessage(botSession, channel, messageEvent.Author, filter.Reason, botName); err != nil { 129 | Log.Error(err) 130 | } 131 | return 132 | } else { 133 | continue 134 | } 135 | } 136 | } 137 | 138 | // if the channel isn't in a group drop the message 139 | Log.Debugf("checking channels") 140 | if !contains(getChannels("discord", botName, guildID), chanID) { 141 | Log.Debugf("channel not found") 142 | return 143 | } 144 | 145 | Log.Debugf("checking blacklist") 146 | 147 | // drop messages from blacklisted users 148 | for _, user := range getBlacklist("discord", botName, guildID, chanID) { 149 | if user == messageEvent.Author.ID.String() { 150 | Log.Debugf("user %s is blacklisted username is %s", messageEvent.Author.ID.String(), messageEvent.Author.Username) 151 | return 152 | } 153 | } 154 | 155 | Log.Debugf("checking attachments") 156 | 157 | // for all attachment urls 158 | var attachmentURLs []string 159 | for _, url := range messageEvent.Attachments { 160 | attachmentURLs = append(attachmentURLs, url.Proxy) 161 | } 162 | 163 | // this was for debugging/testing only 164 | 165 | Log.Debugf("all attachments %s", attachmentURLs) 166 | Log.Debugf("all ignores %+v", getParsing("discord", botName, guildID, chanID).Paste.Ignore) 167 | 168 | Log.Debugf("checking for any urls in the message") 169 | var allURLS []string 170 | for _, url := range xurls.Relaxed().FindAllString(messageEvent.Content, -1) { 171 | Log.Debugf("checking on %s", url) 172 | // if the url is an ip filter it out 173 | if match, err := regexp.Match("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])", []byte(url)); err != nil { 174 | Log.Error(err) 175 | } else if match && !allowIP { 176 | Log.Debugf("adding %s to the list", url) 177 | continue 178 | } 179 | 180 | Log.Debugf("looking for ignored domains") 181 | if len(getParsing("discord", botName, guildID, chanID).Paste.Ignore) == 0 { 182 | Log.Debugf("appending %s to allURLS", url) 183 | allURLS = append(allURLS, url) 184 | Log.Debugf("no ignored domain found") 185 | continue 186 | } else { 187 | var ignored bool 188 | for _, ignoreURL := range getParsing("discord", botName, guildID, chanID).Paste.Ignore { 189 | Log.Debugf("url should be ignored: %t", strings.HasPrefix(url, ignoreURL.URL)) 190 | if strings.HasPrefix(url, ignoreURL.URL) { 191 | ignored = true 192 | Log.Debugf("domain %s is being ignored.", ignoreURL.URL) 193 | break 194 | } 195 | } 196 | if ignored { 197 | } else { 198 | Log.Debugf("appending %s to allURLS", url) 199 | allURLS = append(allURLS, url) 200 | } 201 | } 202 | } 203 | 204 | // add all urls together 205 | Log.Debug("adding attachment URLS to allURLS") 206 | for i := 0; i < len(attachmentURLs); i++ { 207 | allURLS = append(allURLS, attachmentURLs[i]) 208 | } 209 | 210 | // Log.Debug(allURLS) 211 | Log.Debugf("checking mentions") 212 | if len(messageEvent.Mentions) != 0 { 213 | ping, mention := getMentions("discord", botName, guildID, chanID) 214 | 215 | if messageEvent.Mentions[0].ID == botID && messageEvent.Content == fmt.Sprintf("<@%s>", botID.String()) { 216 | Log.Debugf("bot was pinged") 217 | response = ping.Response 218 | reaction = ping.Reaction 219 | } else { 220 | for _, mentioned := range messageEvent.Mentions { 221 | if mentioned.ID == botID { 222 | Log.Debugf("bot was mentioned") 223 | response = mention.Response 224 | reaction = mention.Reaction 225 | } 226 | } 227 | } 228 | } else { 229 | Log.Debugf("no mentions found") 230 | } 231 | 232 | if strings.HasPrefix(messageEvent.Content, getPrefix("discord", botName, guildID)) { 233 | // command 234 | response, reaction = parseCommand(strings.TrimPrefix(messageEvent.Content, getPrefix("discord", botName, guildID)), botName, getCommands("discord", botName, guildID, chanID)) 235 | // if the flag for clearing commands is set and there is a response 236 | if getCommandClear("discord", botName, guildID) && len(response) > 0 { 237 | Log.Debugf("removing command message %s", messageEvent.ID.String()) 238 | if err := deleteDiscordMessages(botSession, channel, []discord.MessageID{0: messageEvent.ID}, ""); err != nil { 239 | Log.Error(err) 240 | } 241 | } 242 | } else { 243 | // regex -- priority over keywords 244 | response, reaction = parseRegex(messageEvent.Content, botName, getRegexPatterns("discord", botName, guildID, chanID), getParsing("discord", botName, guildID, chanID)) 245 | 246 | // keyword 247 | if response == nil { 248 | response, reaction = parseKeyword(messageEvent.Content, botName, getKeywords("discord", botName, guildID, chanID), getParsing("discord", botName, guildID, chanID)) 249 | } 250 | } 251 | 252 | if len(getParsing("discord", botName, guildID, chanID).Image.FileTypes) == 0 && len(getParsing("discord", botName, guildID, chanID).Paste.Sites) == 0 { 253 | Log.Debugf("no parsing configs found") 254 | } else { 255 | Log.Debugf("allURLS: %s", allURLS) 256 | Log.Debugf("allURLS count: %d", len(allURLS)) 257 | 258 | // if we have too many logs ignore it. 259 | if len(allURLS) == 0 { 260 | Log.Debugf("no URLs to read") 261 | } else if len(allURLS) > maxLogs { 262 | Log.Debug("too many logs or screenshots to try and read.") 263 | if err := sendDiscordMessage(botSession, channel, messageEvent.Author, logResponse, botName); err != nil { 264 | Log.Error(err) 265 | } 266 | if err := sendDiscordReaction(botSession, channel, message, logReaction); err != nil { 267 | Log.Error(err) 268 | } 269 | return 270 | } else { 271 | Log.Debugf("reading logs") 272 | if err := sendDiscordReaction(botSession, channel, message, []string{"👀"}); err != nil { 273 | Log.Error(err) 274 | } 275 | 276 | // get parsed content for each url/attachment 277 | Log.Debugf("reading all attachments and logs") 278 | allParsed := make(map[string]string) 279 | for _, url := range allURLS { 280 | allParsed[url] = parseURL(url, getParsing("discord", botName, guildID, chanID)) 281 | } 282 | 283 | //parse logs and append to current response. 284 | for _, url := range allURLS { 285 | Log.Debugf("passing %s to keyword parser", url) 286 | urlResponse, _ := parseKeyword(allParsed[url], botName, getKeywords("discord", botName, guildID, chanID), getParsing("discord", botName, guildID, chanID)) 287 | Log.Debugf("response length = %d", len(urlResponse)) 288 | if len(urlResponse) == 1 && urlResponse[0] == "" || len(urlResponse) == 0 { 289 | 290 | } else { 291 | response = append(response, fmt.Sprintf("I have found the following for: <%s>", url)) 292 | for _, singleLine := range urlResponse { 293 | response = append(response, singleLine) 294 | } 295 | } 296 | } 297 | } 298 | } 299 | 300 | // send response to channel 301 | Log.Debugf("sending response %s to %s", response, chanID) 302 | if err := sendDiscordMessage(botSession, channel, messageEvent.Author, response, botName); err != nil { 303 | Log.Error(err) 304 | } 305 | 306 | // send reaction to channel 307 | Log.Debugf("sending reaction %s", reaction) 308 | if err := sendDiscordReaction(botSession, channel, message, reaction); err != nil { 309 | Log.Error(err) 310 | } 311 | } 312 | 313 | // This function will be called (due to AddHandler) every time a new 314 | // thread is created on any channel that the authenticated bot has access to. 315 | func discordNewThreadHandler(botSession *session.Session, m *gateway.ThreadCreateEvent, botName string) { 316 | 317 | } 318 | 319 | // This function will be called (due to AddHandler) every time a new 320 | // thread is created on any channel that the authenticated bot has access to. 321 | func discordDelThreadHandler(botSession *session.Session, m *gateway.ThreadDeleteEvent, botName string) { 322 | 323 | } 324 | 325 | // kick a user and log it to a channel if configured 326 | // session, guild, user being kicked, 327 | func kickDiscordUser(botSession *session.Session, guild discord.Guild, user discord.User, username, reason, authorname string) (err error) { 328 | if err = botSession.Kick(guild.ID, user.ID, api.AuditLogReason(reason)); err != nil { 329 | return 330 | } 331 | 332 | // TODO: Need to use new config for this 333 | // sendDiscordEmbed(getDiscordConfigString("embed.audit"), embed) 334 | 335 | Log.Info("User " + user.Username + " has been kicked from " + guild.Name + " for " + reason) 336 | 337 | return 338 | } 339 | 340 | // ban a user and log it to a channel if configured 341 | func banDiscordUser(botSession *session.Session, guild discord.Guild, user discord.User, username, reason, authorname string, days int) (err error) { 342 | banData := api.BanData{ 343 | DeleteDays: option.NewUint(uint(days)), 344 | AuditLogReason: api.AuditLogReason(reason), 345 | } 346 | 347 | if err = botSession.Ban(guild.ID, user.ID, banData); err != nil { 348 | return 349 | } 350 | 351 | // TODO: Need to use new config for embed audit to log to a webhook 352 | 353 | Log.Info("User " + user.Username + " has been banned from " + guild.Name + " for " + reason) 354 | 355 | return 356 | } 357 | 358 | // clean up messages if configured to 359 | func deleteDiscordMessages(botSession *session.Session, channel *discord.Channel, messages []discord.MessageID, reason string) (err error) { 360 | Log.Debugf("Removing messages from %s", channel.Name) 361 | 362 | if err = botSession.DeleteMessages(channel.ID, messages, api.AuditLogReason(reason)); err != nil { 363 | return 364 | } 365 | 366 | // TODO: Need to use new config for embed audit to log to a webhook 367 | 368 | Log.Debug("messages were deleted.") 369 | 370 | return 371 | } 372 | 373 | // send message handling 374 | func sendDiscordMessage(botSession *session.Session, channel *discord.Channel, author discord.User, responseArray []string, botName string) (err error) { 375 | // if there is no response to send just return 376 | if len(responseArray) == 0 { 377 | return 378 | } 379 | 380 | prefix := getPrefix("discord", botName, channel.GuildID.String()) 381 | 382 | response := strings.Join(responseArray, "\n") 383 | response = strings.Replace(response, "&user&", "<@"+author.ID.String()+">", -1) 384 | response = strings.Replace(response, "&prefix&", prefix, -1) 385 | response = strings.Replace(response, "&react&", "", -1) 386 | 387 | // if there is an error return the error 388 | if _, err = botSession.SendMessage(channel.ID, response); err != nil { 389 | return 390 | } 391 | 392 | return 393 | } 394 | 395 | // send a reaction to a message 396 | func sendDiscordReaction(botSession *session.Session, channel *discord.Channel, message discord.Message, reactionArray []string) (err error) { 397 | // if there is no reaction to send just return 398 | if len(reactionArray) == 0 || len(reactionArray) == 1 && reactionArray[0] == "" { 399 | return 400 | } 401 | 402 | var hasReact bool 403 | 404 | for _, reaction := range reactionArray { 405 | if reaction != "" { 406 | hasReact = true 407 | break 408 | } 409 | } 410 | 411 | if !hasReact { 412 | return nil 413 | } 414 | 415 | for _, reaction := range reactionArray { 416 | Log.Debugf("sending \"%s\" as a reaction to message: %s", reaction, message.ID) 417 | // if there is an error sending a message return it 418 | if err = botSession.React(channel.ID, message.ID, discord.APIEmoji(reaction)); err != nil { 419 | return 420 | } 421 | } 422 | return 423 | } 424 | 425 | // send a message with an embed 426 | func sendDiscordEmbed(botSession *session.Session, channel discord.Channel, embed *discord.Embed) error { 427 | // if there is an error sending the embed message 428 | if _, err := botSession.SendEmbeds(channel.ID, *embed); err != nil { 429 | Log.Fatal("Embed send error") 430 | return err 431 | } 432 | 433 | return nil 434 | } 435 | 436 | // service handling 437 | // start all the bots 438 | func startDiscordsBots() { 439 | Log.Infof("Starting discord server connections\n") 440 | // range over the bots available to start 441 | for _, bot := range discordGlobal.Bots { 442 | Log.Infof("Connecting to %s\n", bot.BotName) 443 | 444 | // spin up a channel to tell the bot to stop later 445 | // stopDiscord[bot.BotName] = make(chan string) 446 | 447 | // start the bot 448 | go startDiscordBotConnection(bot) 449 | // wait on bot being able to start. 450 | <-discordLoad 451 | } 452 | 453 | Log.Debug("Discord service started\n") 454 | servStart <- "discord_online" 455 | } 456 | 457 | // when a shutdown is sent close out services properly 458 | func stopDiscordBots() { 459 | Log.Infof("stopping discord connections") 460 | // loop through bots and send shutdowns 461 | for _, bot := range discordGlobal.Bots { 462 | stopDiscordServer <- bot.BotName 463 | } 464 | 465 | for range discordGlobal.Bots { 466 | botIn := <-discordServerStopped 467 | Log.Infof("%s", botIn) 468 | } 469 | 470 | Log.Infof("discord connections stopped") 471 | // return shutdown signal on channel 472 | servStopped <- "discord_stopped" 473 | } 474 | 475 | // start connections to discord 476 | func startDiscordBotConnection(discordConfig discordBot) { 477 | Log.Debugf("starting connections for %s", discordConfig.BotName) 478 | // Initializing Discord connection 479 | 480 | // Create a new Discord session using the provided bot token. 481 | Log.Debugf("using token '%s' to auth", discordConfig.Config.Token) 482 | botSession := session.New("Bot " + discordConfig.Config.Token) 483 | 484 | // Add Gateway Intents 485 | botSession.AddIntents(gateway.IntentGuildMessages) 486 | botSession.AddIntents(gateway.IntentGuildEmojis) 487 | botSession.AddIntents(gateway.IntentGuildModeration) 488 | botSession.AddIntents(gateway.IntentDirectMessages) 489 | 490 | // Register ready as a callback for the ready event 491 | botSession.AddHandler(func(gate *gateway.ReadyEvent) { 492 | readyDiscord(botSession, discordConfig.Config.Game) 493 | }) 494 | 495 | // Register messageCreate as a callback for the messageCreate events. 496 | for range discordConfig.Servers { 497 | botSession.AddHandler(func(gate *gateway.MessageCreateEvent) { 498 | discordMessageHandler(botSession, gate, discordConfig.BotName) 499 | }) 500 | } 501 | 502 | for range discordConfig.Servers { 503 | botSession.AddHandler(func(gate *gateway.ThreadCreateEvent) {}) 504 | } 505 | 506 | for range discordConfig.Servers { 507 | botSession.AddHandler(func(gate *gateway.ThreadDeleteEvent) {}) 508 | } 509 | 510 | // Open the websocket and begin listening. 511 | if err := botSession.Open(context.Background()); err != nil { 512 | Log.Error("Failed to connect:", err) 513 | } 514 | 515 | Log.Debugf("Discord service connected for %s", discordConfig.BotName) 516 | 517 | //bot, err := dg.User("@me") 518 | botUser, err := botSession.Me() 519 | if err != nil { 520 | fmt.Println("error obtaining account details,", err) 521 | syscall.Exit(2) 522 | } 523 | 524 | // Permissions requested 525 | // Administration - Kick/Ban/Moderate Members 526 | // Text - 527 | // Send/Manage Messages w/In Threads 528 | // Create/Manage Threads 529 | // Add Reactions 530 | // User External Emojis 531 | // Embed links 532 | 533 | Log.Debug("Invite the bot to your server with https://discordapp.com/oauth2/authorize?client_id=" + botUser.ID.String() + "&scope=bot&permissions=1495185845318") 534 | 535 | discordLoad <- "" 536 | 537 | <-stopDiscordServer 538 | 539 | Log.Debugf("stop recieved on %s", discordConfig.BotName) 540 | 541 | if err := botSession.Close(); err != nil { 542 | Log.Error(err) 543 | } 544 | 545 | Log.Debugf("%s sent close", discordConfig.BotName) 546 | // return the shutdown signal 547 | discordServerStopped <- fmt.Sprintf("Closed connection for %s", discordConfig.BotName) 548 | } 549 | -------------------------------------------------------------------------------- /discord_structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // discord configs 4 | type discordBase struct { 5 | Bots []discordBot `json:"bots,omitempty"` 6 | } 7 | 8 | type discordBot struct { 9 | BotName string `json:"bot_name,omitempty"` 10 | BotID string `json:"bot_id,omitempty"` 11 | Config discordBotConfig `json:"config,omitempty"` 12 | Servers []discordServer `json:"servers,omitempty"` 13 | } 14 | 15 | type discordBotConfig struct { 16 | Token string `json:"token,omitempty"` 17 | Game string `json:"game,omitempty"` 18 | DMResp responseArray `json:"dm_response,omitempty"` 19 | } 20 | 21 | type discordServer struct { 22 | ServerID string `json:"server_id,omitempty"` 23 | Config discordServerConfig `json:"config,omitempty"` 24 | ChanGroups []channelGroup `json:"channel_groups,omitempty"` 25 | Permissions []permission `json:"permissions,omitempty"` 26 | Filters []filter `json:"filters,omitempty"` 27 | } 28 | 29 | type discordServerConfig struct { 30 | Prefix string `json:"prefix,omitempty"` 31 | Clear bool `json:"clear_commands,omitempty"` 32 | WebHooks discordWebHooks `json:"web_hooks,omitempty"` 33 | } 34 | 35 | type discordWebHooks struct { 36 | Logs string `json:"logs,omitempty"` 37 | } 38 | 39 | type discordKickOnMention struct { 40 | Roles []string `json:"roles,omitempty"` 41 | Users []string `json:"users,omitempty"` 42 | Direct responseArray `json:"dm,omitempty"` 43 | Channel responseArray `json:"channel,omitempty"` 44 | Kick bool `json:"kick,omitempty"` 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/parkervcp/parkertron 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/diamondburned/arikawa/v3 v3.3.6 7 | github.com/fatih/color v1.17.0 // indirect 8 | github.com/fsnotify/fsnotify v1.7.0 9 | github.com/goccy/go-yaml v1.11.3 10 | github.com/gorilla/schema v1.3.0 // indirect 11 | github.com/gorilla/websocket v1.5.1 // indirect 12 | github.com/h2non/filetype v1.1.3 13 | github.com/husio/irc v0.0.0-20150308150232-bcf322335678 14 | github.com/kr/pretty v0.1.0 // indirect 15 | github.com/nlopes/slack v0.6.0 16 | github.com/otiai10/gosseract/v2 v2.4.1 17 | github.com/pkg/errors v0.9.1 // indirect 18 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 19 | github.com/sirupsen/logrus v1.9.3 20 | github.com/spf13/pflag v1.0.5 21 | github.com/syfaro/haste-client v0.0.0-20150731062254-09b1fbfa3977 22 | golang.org/x/net v0.25.0 // indirect 23 | golang.org/x/time v0.5.0 // indirect 24 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 25 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 26 | mvdan.cc/xurls/v2 v2.5.0 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/diamondburned/arikawa/v3 v3.3.5 h1:Z6BwetBMzPxTBLY2Ixxic2kdJJe0JhNvVrdbJ0gRcWg= 5 | github.com/diamondburned/arikawa/v3 v3.3.5/go.mod h1:KPkkWr40xmEithhd15XD2dbkVY8A5+MCmZO0gRXk3qc= 6 | github.com/diamondburned/arikawa/v3 v3.3.6 h1:Vxyb+kuWEFseDS2+USRTWS0b5RUbV9PQ1fnVN5sJhwo= 7 | github.com/diamondburned/arikawa/v3 v3.3.6/go.mod h1:0EAniaG6PMkhuIZEDR8BxXodasfWT7wekNqlNmb+JZI= 8 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 9 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 10 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 11 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 12 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 13 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 14 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 15 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 16 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 17 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 18 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 19 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 20 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 21 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 22 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 23 | github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= 24 | github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= 25 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 28 | github.com/gorilla/schema v1.3.0 h1:rbciOzXAx3IB8stEFnfTwO3sYa6EWlQk79XdyustPDA= 29 | github.com/gorilla/schema v1.3.0/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 30 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 31 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 32 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 33 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 34 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 35 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 36 | github.com/husio/irc v0.0.0-20150308150232-bcf322335678 h1:BCL5oLOLxmJ9NDLgFtiCh8DgwoJHtd3HVWSevrPYBAs= 37 | github.com/husio/irc v0.0.0-20150308150232-bcf322335678/go.mod h1:zgR4mQAFvN5mPy8vBccsokv4Oo8zQYq1aZCrVS2Bc7g= 38 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 39 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 42 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 43 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 44 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 45 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 46 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 47 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 48 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 49 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 50 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 51 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 52 | github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= 53 | github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= 54 | github.com/otiai10/gosseract/v2 v2.4.1 h1:G8AyBpXEeSlcq8TI85LH/pM5SXk8Djy2GEXisgyblRw= 55 | github.com/otiai10/gosseract/v2 v2.4.1/go.mod h1:1gNWP4Hgr2o7yqWfs6r5bZxAatjOIdqWxJLWsTsembk= 56 | github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= 57 | github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= 58 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 59 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 60 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= 64 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= 65 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 66 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 67 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 68 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 69 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 72 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 73 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/syfaro/haste-client v0.0.0-20150731062254-09b1fbfa3977 h1:q4zsfToRKReODIv51dXoT7YSi/DYl+xZcPvebFnRbgg= 76 | github.com/syfaro/haste-client v0.0.0-20150731062254-09b1fbfa3977/go.mod h1:Z8f/FggiNbik6JGd2arZKiv3Y0BOfGiA2kpi8QJsZDI= 77 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 78 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 79 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 81 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 82 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 83 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 84 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 85 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 86 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 87 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 88 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 89 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 90 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 91 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 92 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 93 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 94 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 95 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 96 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 97 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 98 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 99 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 100 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 101 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 102 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 103 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 104 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 105 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 124 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 125 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 126 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 127 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 128 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 129 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 130 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 131 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 132 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 133 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 134 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 135 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 136 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 137 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 138 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 139 | golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 140 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 141 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 142 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 143 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 144 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 145 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 146 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 147 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 148 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 149 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 150 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 151 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 152 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 153 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 156 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 157 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 158 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 160 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 161 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 162 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 163 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 164 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 167 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 168 | mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= 169 | mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= 170 | -------------------------------------------------------------------------------- /images/parkertron_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkervcp/parkertron/35393d928b61000b71f471437ca97ec18762725b/images/parkertron_logo.png -------------------------------------------------------------------------------- /irc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | hirc "github.com/husio/irc" 9 | ) 10 | 11 | var ( 12 | stopIRC = make(map[string](chan string)) 13 | 14 | ircGlobal irc 15 | 16 | ircLoad = make(chan string) 17 | ) 18 | 19 | func ircIdentityHandler(botName string) (nickname, password string) { 20 | for _, bot := range ircGlobal.Bots { 21 | if bot.BotName == botName { 22 | nickname = bot.Config.Server.Nickname 23 | password = bot.Config.Server.Password 24 | } 25 | } 26 | return 27 | } 28 | 29 | //ircMessageHandler the IRC listener that manages inbound messaging 30 | func ircMessageHandler(conn hirc.Conn, botName string) { 31 | nickname, password := ircIdentityHandler(botName) 32 | 33 | message, err := conn.ReadMessage() 34 | if err != nil { 35 | Log.Errorf("cannot read message: %s", err) 36 | return 37 | } 38 | 39 | // make these easier to send and recieve 40 | channel := message.Params[0] 41 | author := message.Nick() 42 | messageIn := message.Trailing 43 | 44 | // Log.Debugf("started handle") 45 | Log.Debug("irc inbound " + message.String()) 46 | 47 | // keep alive messaging 48 | if message.Command == "PING" { 49 | conn.Send("PONG " + messageIn) 50 | Log.Debug("PONG Sent") 51 | return 52 | } 53 | 54 | // for authentication 55 | if message.Command == "NOTICE" { 56 | if strings.Contains(strings.ToLower(messageIn), "this nickname is registered") { 57 | conn.Send("%s IDENTIFY %s %s", author, nickname, password) 58 | } 59 | return 60 | } 61 | 62 | // message handling 63 | if message.Command == "PRIVMSG" { 64 | Log.Debug("channel: " + channel) // channel 65 | Log.Debug("author: " + author) // user 66 | Log.Debug("messageIn: " + messageIn) // actual message 67 | 68 | // get all the configs 69 | prefix := getPrefix("irc", botName, botName) 70 | channelCommands := getCommands("irc", botName, "", channel) 71 | channelKeywords := getKeywords("irc", botName, "", channel) 72 | channelParsing := getParsing("irc", botName, "", channel) 73 | 74 | if author == nickname { 75 | Log.Debug("User is the bot and being ignored.") 76 | return 77 | } 78 | 79 | // if the user nickname matches bot or blacklisted. 80 | for _, user := range getBlacklist("discord", botName, "", channel) { 81 | if user == author { 82 | Log.Debugf("user %s is blacklisted", author) 83 | return 84 | } 85 | } 86 | 87 | // if bot is DM'd 88 | if channel == nickname { 89 | Log.Debug("This was a DM") 90 | _, mention := getMentions("irc", botName, "", channel) 91 | sendIRCMessage(conn, channel, author, prefix, mention.Response) 92 | return 93 | } 94 | 95 | // 96 | // Message Handling 97 | // 98 | if messageIn != "" { 99 | Log.Debug("Message Content: " + messageIn) 100 | 101 | if !strings.HasPrefix(messageIn, prefix) { 102 | Log.Debug("sending to \"" + channel) 103 | parseKeyword(messageIn, botName, channelKeywords, channelParsing) 104 | } else { 105 | Log.Debug("sending to \"" + channel) 106 | parseCommand(strings.TrimPrefix(messageIn, prefix), botName, channelCommands) 107 | } 108 | return 109 | } 110 | // Log.Debug(message.Raw) 111 | } 112 | return 113 | } 114 | 115 | // kick irc user 116 | func kickIRCUser() { 117 | 118 | } 119 | 120 | // ban irc user 121 | func banIRCUser() { 122 | 123 | } 124 | 125 | //sendIRCMessage function to send messages separate of the listener 126 | func sendIRCMessage(conn hirc.Conn, channelName string, user string, prefix string, responseArray []string) { 127 | // send nothing if there is nothing in the array 128 | if len(responseArray) == 0 { 129 | return 130 | } 131 | 132 | // send a line per item in the array. 133 | for _, response := range responseArray { 134 | Log.Debugf("line sent: " + response) 135 | response = strings.Replace(response, "&user&", user, -1) 136 | response = strings.Replace(response, "&prefix&", prefix, -1) 137 | conn.Send("PRIVMSG " + "#" + channelName + " :" + response) 138 | time.Sleep(time.Millisecond * 300) 139 | } 140 | 141 | // log the message that was sent in debug mode. 142 | Log.Debugf("IRC Message Sent: %s", responseArray) 143 | } 144 | 145 | // service handling 146 | // start all the bots 147 | func startIRCBots() { 148 | Log.Infof("Starting IRC server connections\n") 149 | // range over the bots available to start 150 | for _, bot := range ircGlobal.Bots { 151 | Log.Infof("Connecting to %s\n", bot.BotName) 152 | 153 | // spin up a channel to tell the bot to shutdown later 154 | stopIRC[bot.BotName] = make(chan string) 155 | 156 | // start the bot 157 | go startIRCConnection(bot) 158 | // wait on bot being able to start. 159 | <-ircLoad 160 | } 161 | 162 | Log.Infof("irc service started\n") 163 | // inform main process that the bot is started 164 | servStart <- "irc_online" 165 | } 166 | 167 | // when a shutdown is sent close out services properly 168 | func stopIRCBots() { 169 | Log.Infof("stopping irc connections") 170 | // loop through bots and send shutdowns 171 | for _, bot := range ircGlobal.Bots { 172 | Log.Infof("stopping %s", bot.BotName) 173 | // send stop to bot 174 | stopIRC[bot.BotName] <- "" 175 | // wait for bot to send a stop back 176 | <-stopIRC[bot.BotName] 177 | // close channel 178 | close(stopIRC[bot.BotName]) 179 | Log.Infof("stopped %s", bot.BotName) 180 | } 181 | 182 | Log.Infof("irc connections stopped") 183 | // return shutdown signal on channel 184 | servStopped <- "irc_stopped" 185 | } 186 | 187 | // start connections to irc 188 | func startIRCConnection(ircConfig ircBot) { 189 | host := ircConfig.Config.Server.Address + ":" + strconv.Itoa(ircConfig.Config.Server.Port) 190 | Log.Debugf("Connecting on %s\n", host) 191 | 192 | // Connect to the server 193 | conn, err := hirc.Connect(host) 194 | if err != nil { 195 | Log.Errorf("cannot connect to %s: %s\n", host, err) 196 | } 197 | 198 | Log.Debugf("Connected to %s\n", host) 199 | 200 | // send user info 201 | conn.Send("USER %s %s * :"+ircConfig.Config.Server.RealName, ircConfig.Config.Server.Ident, host) 202 | conn.Send("NICK %s", ircConfig.Config.Server.Nickname) 203 | 204 | time.Sleep(time.Millisecond * 100) 205 | 206 | ircLoad <- "" 207 | 208 | for _, group := range ircConfig.ChanGroups { 209 | for _, channel := range group.ChannelIDs { 210 | Log.Debugf("joining %s", channel) 211 | if !strings.HasPrefix(channel, "#") { 212 | channel = "#" + channel 213 | } 214 | conn.Send("JOIN %s", channel) 215 | } 216 | } 217 | 218 | for { 219 | // listen for stop on channel 220 | select { 221 | case <-stopIRC[ircConfig.BotName]: 222 | Log.Debugf("closing channel for %s", ircConfig.BotName) 223 | conn.Close() 224 | stopIRC[ircConfig.BotName] <- "" 225 | Log.Debugf("%s channel closed", ircConfig.BotName) 226 | return 227 | default: 228 | ircMessageHandler(*conn, ircConfig.BotName) 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /irc_structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // irc configs 4 | type irc struct { 5 | Bots []ircBot `json:"bots,omitempty"` 6 | } 7 | 8 | type ircBot struct { 9 | BotName string `json:"bot_name,omitempty"` 10 | Config ircBotConfig `json:"config,omitempty"` 11 | ChanGroups []channelGroup `json:"channel_groups,omitempty"` 12 | } 13 | 14 | type ircBotConfig struct { 15 | Server ircServerConfig `json:"server,omitempty"` 16 | DMResp responseArray `json:"dm_response,omitempty"` 17 | Prefix string `json:"prefix,omitempty"` 18 | } 19 | 20 | type ircServerConfig struct { 21 | Address string `json:"address,omitempty"` 22 | Port int `json:"port,omitempty"` 23 | SSLEnable bool `json:"ssl,omitempty"` 24 | Ident string `json:"ident,omitempty"` 25 | Email string `json:"email,omitempty"` 26 | Password string `json:"password,omitempty"` 27 | Nickname string `json:"nickname,omitempty"` 28 | RealName string `json:"real_name,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /parkertron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "reflect" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "github.com/rifflock/lfshook" 16 | "github.com/sirupsen/logrus" 17 | flag "github.com/spf13/pflag" 18 | "github.com/syfaro/haste-client" 19 | ) 20 | 21 | var ( 22 | //startTime = time.Now 23 | 24 | // Log is a logrus logger 25 | Log *logrus.Logger 26 | 27 | //ServStat is the Service Status channel 28 | servStart = make(chan string) 29 | shutdown = make(chan string) 30 | servStopped = make(chan string) 31 | 32 | botConfig parkertron 33 | 34 | serviceStart = map[string]func(){ 35 | "discord": startDiscordsBots, 36 | "irc": startIRCBots, 37 | } 38 | 39 | serviceStop = map[string]func(){ 40 | "discord": stopDiscordBots, 41 | "irc": stopIRCBots, 42 | } 43 | 44 | // startup flag values 45 | verbose string 46 | logDir string 47 | confDir string 48 | conf string 49 | diag bool 50 | ) 51 | 52 | const ( 53 | version = "v.0.4.0" 54 | asciiArt = ` 55 | __ __ 56 | ____ ____ ______/ /_____ _____/ /__________ ____ 57 | / __ \/ __ '/ ___/ //_/ _ \/ ___/ __/ ___/ __ \/ __ \ 58 | / /_/ / /_/ / / / ,< / __/ / / /_/ / / /_/ / / / / 59 | / .___/\__,_/_/ /_/|_|\___/_/ \__/_/ \____/_/ /_/ 60 | /_/` 61 | ) 62 | 63 | type parkertron struct { 64 | Services []string `json:"services,omitempty"` 65 | Log logConf `json:"log,omitempty"` 66 | Database databaseConfig `json:"database,omitempty"` 67 | Parsing botParseConfig `json:"parsing,omitempty"` 68 | } 69 | 70 | type logConf struct { 71 | Level string `json:"level,omitempty"` 72 | Location string `json:"location,omitempty"` 73 | } 74 | 75 | type databaseConfig struct { 76 | Host string `json:"host,omitempty"` 77 | Port int `json:"port,omitempty"` 78 | User string `json:"user,omitempty"` 79 | Pass string `json:"pass,omitempty"` 80 | Database string `json:"database,omitempty"` 81 | } 82 | 83 | type botParseConfig struct { 84 | Reaction []string `json:"reaction,omitempty"` 85 | Response []string `json:"response,omitempty"` 86 | Max int `json:"max,omitempty"` 87 | AllowIP bool `json:"allow_ip,omitempty"` 88 | } 89 | 90 | func init() { 91 | flag.StringVarP(&verbose, "verbosity", "v", "info", "set the verbosity level for the bot {info,debug} (default is info)") 92 | flag.StringVarP(&logDir, "logdir", "l", "logs/", "set the log directory of the bot. (default is ./logs/)") 93 | flag.StringVarP(&confDir, "confdir", "d", "configs/", "set the config directory of the bot. (default is ./configs/)") 94 | flag.StringVarP(&conf, "conffile", "c", "parkertron.yml", "set the config name for the bot. (default is parkertron.yml)") 95 | flag.BoolVar(&diag, "diag", false, "uploads diagnotics to hastebin") 96 | flag.Parse() 97 | 98 | if !strings.HasSuffix(confDir, "/") { 99 | confDir = confDir + "/" 100 | } 101 | 102 | if newbot, err := loadInitConfig(confDir, conf, verbose); err != nil { 103 | log.Fatal(err) 104 | } else { 105 | if !flag.CommandLine.Changed(verbose) { 106 | verbose = newbot.Log.Level 107 | } 108 | 109 | if !flag.CommandLine.Changed(logDir) { 110 | logDir = newbot.Log.Location 111 | } 112 | } 113 | 114 | if diag { 115 | uploadDiag(logDir) 116 | } 117 | 118 | log.Print("starting logging") 119 | Log = newLogger(logDir, verbose) 120 | Log.Infof("logging online\n") 121 | 122 | if err := initConfig(confDir); err != nil { 123 | Log.Panic(err) 124 | } 125 | 126 | Log.Infof("%s %s\n\n", asciiArt, version) 127 | } 128 | 129 | func main() { 130 | // if there are no bots configured write default example configs 131 | if len(discordGlobal.Bots) == 0 && len(ircGlobal.Bots) == 0 { 132 | Log.Infof("No bots are configured") 133 | for _, service := range botConfig.Services { 134 | switch service { 135 | case "discord": 136 | if err := createExampleDiscordConfig(confDir + "discord/"); err != nil { 137 | Log.Fatalf("%s", err) 138 | } 139 | case "irc": 140 | if err := createExampleIRCConfig(confDir + "irc/"); err != nil { 141 | Log.Fatalf("%s", err) 142 | } 143 | default: 144 | } 145 | } 146 | Log.Infof("Example configs have been created.") 147 | Log.Info("shutting down") 148 | os.Exit(0) 149 | } 150 | 151 | for _, cr := range botConfig.Services { 152 | if service, ok := serviceStart[cr]; ok { 153 | Log.Debugf("running %s", runtime.FuncForPC(reflect.ValueOf(service).Pointer()).Name()) 154 | go service() 155 | } else { 156 | Log.Errorf("unexpected array value: %q", cr) 157 | } 158 | } 159 | 160 | for range botConfig.Services { 161 | Log.Debugf("checking for servStart") 162 | <-servStart 163 | } 164 | 165 | go catchSig() 166 | go console() 167 | 168 | Log.Infof("Bot is now running. Send 'shutdown' or 'ctrl + c' to stop the bot.\n") 169 | 170 | <-shutdown 171 | 172 | for _, cr := range botConfig.Services { 173 | if service, ok := serviceStop[cr]; ok { 174 | Log.Debugf("running %s", runtime.FuncForPC(reflect.ValueOf(service).Pointer()).Name()) 175 | go service() 176 | } else { 177 | Log.Errorf("unexpected array value: %q", cr) 178 | } 179 | } 180 | 181 | for range botConfig.Services { 182 | Log.Debugf("checking for servStopped") 183 | <-servStopped 184 | } 185 | } 186 | 187 | func console() { 188 | reader := bufio.NewReader(os.Stdin) 189 | for { 190 | line, err := reader.ReadString('\n') 191 | if err != nil { 192 | Log.Infof("cannot read from stdin: %v", err) 193 | } 194 | line = strings.TrimSpace(line) 195 | if len(line) == 0 { 196 | continue 197 | } 198 | if line == "shutdown" { 199 | Log.Infof("shutting down the bot.\n") 200 | Log.Infof("All services stopped\n") 201 | shutdown <- "" 202 | return 203 | } 204 | } 205 | } 206 | 207 | func catchSig() { 208 | sigc := make(chan os.Signal, 1) 209 | signal.Notify(sigc, 210 | os.Interrupt) 211 | <-sigc 212 | Log.Debugf("interupt caught\n") 213 | shutdown <- "" 214 | } 215 | 216 | func newLogger(logDir, level string) *logrus.Logger { 217 | if Log != nil { 218 | return Log 219 | } 220 | 221 | if _, err := os.Stat(logDir + "latest.log"); err != nil { 222 | } else { 223 | if err := os.Rename(logDir+"latest.log", logDir+time.Now().Format(time.RFC3339)+".log"); err != nil { 224 | Log.Errorf("there was an error opening the logs: %s", err) 225 | } 226 | } 227 | 228 | if _, err := os.Stat(logDir + "debug.log"); err != nil { 229 | } else { 230 | if err := os.Rename(logDir+"debug.log", logDir+"debug-"+time.Now().Format(time.RFC3339)+".log"); err != nil { 231 | Log.Errorf("there was an error opening the logs: %s", err) 232 | } 233 | } 234 | 235 | pathMap := lfshook.PathMap{ 236 | logrus.InfoLevel: logDir + "latest.log", 237 | logrus.DebugLevel: logDir + "debug.log", 238 | logrus.ErrorLevel: logDir + "latest.log", 239 | logrus.FatalLevel: logDir + "latest.log", 240 | } 241 | 242 | Log = logrus.New() 243 | 244 | switch level { 245 | case "info": 246 | Log.SetLevel(logrus.InfoLevel) 247 | case "debug": 248 | Log.SetLevel(logrus.DebugLevel) 249 | default: 250 | Log.SetLevel(logrus.InfoLevel) 251 | } 252 | 253 | Log.Hooks.Add(lfshook.NewHook( 254 | pathMap, 255 | &logrus.JSONFormatter{}, 256 | )) 257 | return Log 258 | } 259 | 260 | func uploadDiag(logDir string) { 261 | log.Printf("uploading logs to hastebin") 262 | if _, err := os.Stat(logDir + "latest.log"); err != nil { 263 | } else { 264 | uploadFile(logDir + "latest.log") 265 | } 266 | 267 | if _, err := os.Stat(logDir + "debug.log"); err != nil { 268 | } else { 269 | uploadFile(logDir + "debug.log") 270 | } 271 | 272 | os.Exit(0) 273 | } 274 | 275 | func uploadFile(name string) { 276 | hasteClient := haste.NewHaste("https://ptero.co") 277 | data, err := ioutil.ReadFile(name) 278 | if err != nil { 279 | Log.Infof("Unable to read file: %s\n", err.Error()) 280 | os.Exit(2) 281 | } 282 | 283 | resp, err := hasteClient.UploadBytes(data) 284 | if err != nil { 285 | Log.Infof("Error uploading: %s\n", err.Error()) 286 | os.Exit(3) 287 | } 288 | 289 | fmt.Println(name, resp.GetLink(hasteClient)) 290 | } 291 | -------------------------------------------------------------------------------- /parsing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | _ "image/jpeg" 6 | _ "image/png" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/h2non/filetype" 15 | "github.com/otiai10/gosseract/v2" 16 | ) 17 | 18 | func parseImage(remoteURL string) (imageText string, err error) { 19 | Log.Info("Reading from " + remoteURL) 20 | 21 | remote, err := http.Get(remoteURL) 22 | if err != nil { 23 | return 24 | } 25 | 26 | lastBin := strings.LastIndex(remoteURL, "/") 27 | fileName := strings.Split(remoteURL[lastBin+1:], "?")[0] 28 | 29 | if len(fileName) > 150 { 30 | fileName = fileName[len(fileName)-50:] 31 | } 32 | 33 | Log.Debug("Filename is " + fileName) 34 | 35 | //open a file for writing 36 | file, err := os.Create("/tmp/" + fileName) 37 | if err != nil { 38 | return 39 | } 40 | 41 | // Use io.Copy to just dump the response body to the file. This supports huge files 42 | _, err = io.Copy(file, remote.Body) 43 | if err != nil { 44 | return 45 | } 46 | 47 | err = remote.Body.Close() 48 | if err != nil { 49 | return 50 | } 51 | 52 | err = file.Close() 53 | if err != nil { 54 | return 55 | } 56 | 57 | Log.Debug("Image File Pulled and saved to /tmp/" + fileName) 58 | 59 | //load file to read 60 | buf, err := os.ReadFile("/tmp/" + fileName) 61 | if err != nil { 62 | return 63 | } 64 | 65 | // check filetype 66 | if !filetype.IsImage(buf) { 67 | Log.Debugf("file is not an image\n") 68 | return 69 | } 70 | 71 | Log.Debug("File is an image") 72 | 73 | client := gosseract.NewClient() 74 | 75 | err = client.SetImage("/tmp/" + fileName) 76 | if err != nil { 77 | return 78 | } 79 | 80 | imageData, err := getImageDimension("/tmp/" + fileName) 81 | if err != nil { 82 | return 83 | } 84 | 85 | Log.Debugf("Image width is %d", imageData.Width) 86 | Log.Debugf("Image height is %d", imageData.Height) 87 | 88 | imageText, err = client.Text() 89 | if err != nil { 90 | return 91 | } 92 | 93 | if len(imageText) >= 1 { 94 | imageText = imageText[:len(imageText)-1] 95 | } 96 | 97 | err = client.Close() 98 | if err != nil { 99 | return 100 | } 101 | 102 | err = os.Remove("/tmp/" + fileName) 103 | 104 | Log.Debug("Image Parsed") 105 | Log.Debug(imageText) 106 | 107 | return 108 | } 109 | 110 | func getImageDimension(imagePath string) (imageData image.Config, err error) { 111 | file, err := os.Open(imagePath) 112 | if err != nil { 113 | Log.Error("error opening file") 114 | return 115 | } 116 | defer file.Close() 117 | 118 | imageData, _, err = image.DecodeConfig(file) 119 | if err != nil { 120 | Log.Error("error decoding image") 121 | return 122 | } 123 | 124 | return 125 | } 126 | 127 | // paste site handling 128 | func parseBin(url, format string) (binText string, err error) { 129 | var rawURL string 130 | 131 | Log.Debugf("reading from %s", url) 132 | _, file := path.Split(url) 133 | rawURL = strings.Replace(format, "&filename&", file, 1) 134 | 135 | Log.Debug("Raw text URL is " + rawURL) 136 | 137 | resp, err := http.Get(rawURL) 138 | if err != nil { 139 | return 140 | } 141 | defer resp.Body.Close() 142 | 143 | body, err := io.ReadAll(resp.Body) 144 | 145 | binText = string(body) 146 | 147 | Log.Debug("Contents = \n" + binText) 148 | 149 | return 150 | } 151 | 152 | // parses url contents for images and paste sites. 153 | func parseURL(url string, parseConf parsing) (parsedText string) { 154 | //Catch domains and route to the proper controllers (image, binsite parsers) 155 | Log.Debugf("checking for pastes and images on %s\n", url) 156 | // If the url ends with a / remove it. Stupid chrome adds them. 157 | if strings.HasSuffix(url, "/") { 158 | url = strings.TrimSuffix(url, "/") 159 | } 160 | 161 | //check for image filetypes 162 | Log.Debug("checking if image") 163 | for _, filetype := range parseConf.Image.FileTypes { 164 | // need to remove any flags set for the url by cutting anything from the ? to the end 165 | if strings.HasSuffix(strings.Split(url, "?")[0], filetype) { 166 | Log.Debug("found image file") 167 | if imageText, err := parseImage(url); err != nil { 168 | Log.Errorf("%s\n", err) 169 | } else { 170 | Log.Debugf(imageText) 171 | parsedText = imageText 172 | return 173 | } 174 | } 175 | } 176 | 177 | // check for paste sites 178 | Log.Debug("checking if bin file") 179 | for _, paste := range parseConf.Paste.Sites { 180 | if strings.HasPrefix(url, paste.URL) { 181 | if binText, err := parseBin(url, paste.Format); err != nil { 182 | Log.Errorf("%s\n", err) 183 | } else { 184 | Log.Debugf(binText) 185 | parsedText = binText 186 | return 187 | } 188 | } 189 | } 190 | 191 | return 192 | } 193 | 194 | // __ __ 195 | // / /_____ __ ___ _____ _______/ / 196 | // / '_/ -_) // / |/|/ / _ \/ __/ _ / 197 | // /_/\_\\__/\_, /|__,__/\___/_/ \_,_/ 198 | // /___/ 199 | 200 | // returns response and reaction for keywords 201 | func parseKeyword(message, botName string, channelKeywords []keyword, parseConf parsing) (response, reaction []string) { 202 | Log.Debugf("Parsing inbound chat for %s", botName) 203 | 204 | message = strings.ToLower(message) 205 | 206 | //exact match search 207 | Log.Debug("Testing matches") 208 | for _, keyWord := range channelKeywords { 209 | if message == keyWord.Keyword && keyWord.Exact { // if the match was an exact match 210 | Log.Debugf("Response is %v", keyWord.Response) 211 | Log.Debugf("Reaction is %v", keyWord.Reaction) 212 | return keyWord.Response, keyWord.Reaction 213 | } else if strings.Contains(message, keyWord.Keyword) && !keyWord.Exact { // if the match was just a match 214 | Log.Debugf("Response is %v", keyWord.Response) 215 | Log.Debugf("Reaction is %v", keyWord.Reaction) 216 | return keyWord.Response, keyWord.Reaction 217 | } 218 | } 219 | 220 | lastIndex := -1 221 | 222 | //Match on errors 223 | Log.Debug("Testing matches") 224 | 225 | for _, keyWord := range channelKeywords { 226 | if strings.Contains(message, keyWord.Keyword) { 227 | Log.Debugf("match is %s", keyWord.Keyword) 228 | } 229 | 230 | index := strings.LastIndex(message, keyWord.Keyword) 231 | if index > lastIndex && !keyWord.Exact { 232 | lastIndex = index 233 | response = keyWord.Response 234 | reaction = keyWord.Reaction 235 | } 236 | } 237 | 238 | return 239 | } 240 | 241 | // returns response and reaction for patterns 242 | func parseRegex(message, botName string, channelPatterns []pattern, parseConf parsing) (response, reaction []string) { 243 | Log.Debugf("Parsing inbound chat for %s", botName) 244 | 245 | message = strings.ToLower(message) 246 | 247 | //regex match search 248 | Log.Debug("Testing regex patterns") 249 | 250 | for _, pat := range channelPatterns { 251 | Log.Debugf("Pattern is %s", pat.Pattern) 252 | if match, err := regexp.MatchString(pat.Pattern, message); err != nil { 253 | Log.Error(err) 254 | } else if match { 255 | // if the pattern was a match 256 | Log.Debugf("Response is %v", pat.Response) 257 | Log.Debugf("Reaction is %v", pat.Reaction) 258 | return pat.Response, pat.Reaction 259 | } 260 | } 261 | return 262 | } 263 | 264 | // __ 265 | // _______ __ _ __ _ ___ ____ ___/ / 266 | // / __/ _ \/ ' \/ ' \/ _ `/ _ \/ _ / 267 | // \__/\___/_/_/_/_/_/_/\_,_/_//_/\_,_/ 268 | // 269 | 270 | // AdminCommand commands are hard coded for now 271 | func adminCommand(message, botName string, servCommands []command, servKeywords []keyword) (response, reaction []string) { 272 | Log.Debugf("Parsing inbound admin command for %s", botName) 273 | message = strings.ToLower(message) 274 | 275 | return 276 | } 277 | 278 | // ModCommand commands are hard coded for now 279 | func modCommand(message, botName string, servCommands []command) (response, reaction []string) { 280 | Log.Debugf("Parsing inbound mod command for %s", botName) 281 | message = strings.ToLower(message) 282 | return 283 | } 284 | 285 | // Command parses commands 286 | func parseCommand(message, botName string, channelCommands []command) (response, reaction []string) { 287 | Log.Debugf("Parsing inbound command for %s", botName) 288 | message = strings.ToLower(message) 289 | 290 | for _, command := range channelCommands { 291 | if command.Command == message { 292 | response = command.Response 293 | reaction = command.Reaction 294 | } 295 | } 296 | return 297 | } 298 | 299 | // general funcs 300 | func contains(array []string, str string) bool { 301 | for _, value := range array { 302 | if value == str { 303 | return true 304 | } 305 | } 306 | return false 307 | } 308 | -------------------------------------------------------------------------------- /parsing_structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // generic structs 4 | type permission struct { 5 | Group string `json:"group,omitempty"` 6 | Users []string `json:"users,omitempty"` 7 | Roles []string `json:"roles,omitempty"` 8 | Commands []string `json:"commands,omitempty"` 9 | Blacklisted bool `json:"blacklisted,omitempty"` 10 | } 11 | 12 | type command struct { 13 | Command string `json:"command,omitempty"` 14 | Response []string `json:"response,omitempty"` 15 | Reaction []string `json:"reaction,omitempty"` 16 | } 17 | 18 | type keyword struct { 19 | Keyword string `json:"keyword,omitempty"` 20 | Reaction []string `json:"reaction,omitempty"` 21 | Response []string `json:"response,omitempty"` 22 | Exact bool `json:"exact,omitempty"` 23 | } 24 | 25 | type pattern struct { 26 | Pattern string `json:"pattern,omitempty"` 27 | Reaction []string `json:"reaction,omitempty"` 28 | Response []string `json:"response,omitempty"` 29 | } 30 | 31 | type mentions struct { 32 | Ping responseArray `json:"ping,omitempty"` 33 | Mention responseArray `json:"mention,omitempty"` 34 | } 35 | 36 | type filter struct { 37 | Term string `json:"term,omitempty"` 38 | Reason []string `json:"reason,omitempty"` 39 | } 40 | 41 | type responseArray struct { 42 | Reaction []string `json:"reaction,omitempty"` 43 | Response []string `json:"response,omitempty"` 44 | } 45 | 46 | type parsing struct { 47 | Image parsingImageConfig `json:"image,omitempty"` 48 | Paste parsingPasteConfig `json:"paste,omitempty"` 49 | } 50 | 51 | type parsingConfig struct { 52 | Name string `json:"name,omitempty"` 53 | URL string `json:"url,omitempty"` 54 | Format string `json:"format,omitempty"` 55 | } 56 | 57 | type parsingImageConfig struct { 58 | FileTypes []string `json:"filetypes,omitempty"` 59 | Sites []parsingConfig `json:"sites,omitempty"` 60 | } 61 | 62 | type parsingPasteConfig struct { 63 | Sites []parsingConfig `json:"sites,omitempty"` 64 | Ignore []parsingConfig `json:"ignore,omitmepty"` 65 | } 66 | 67 | type channelGroup struct { 68 | ChannelIDs []string `json:"channels,omitempty"` 69 | Mentions mentions `json:"mentions,omitempty"` 70 | Commands []command `json:"commands,omitempty"` 71 | Keywords []keyword `json:"keywords,omitempty"` 72 | Regex []pattern `json:"regex,omitempty"` 73 | Parsing parsing `json:"parsing,omitempty"` 74 | Permissions []permission `json:"permissions,omitempty"` 75 | KOM discordKickOnMention `json:"kick_on_mention,omitempty"` 76 | } 77 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | var ( 10 | apiToken = "" 11 | ) 12 | 13 | // this is for testing. 14 | func startSlackConnection() { 15 | var ( 16 | postAsUserName string 17 | postAsUserID string 18 | postToUserName string 19 | postToUserID string 20 | postToChannelID string 21 | ) 22 | 23 | api := slack.New(apiToken) 24 | 25 | authTest, err := api.AuthTest() 26 | if err != nil { 27 | fmt.Printf("Error getting channels: %s\n", err) 28 | return 29 | } 30 | // Post as the authenticated user. 31 | postAsUserName = authTest.User 32 | postAsUserID = authTest.UserID 33 | 34 | // Posting to DM with self causes a conversation with slackbot. 35 | postToUserName = authTest.User 36 | postToUserID = authTest.UserID 37 | 38 | Log.Info(fmt.Sprintf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)) 39 | } 40 | --------------------------------------------------------------------------------