package main import ( "bytes" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/textproto" "os" "strings" "time" "github.com/nlopes/slack" "github.com/unixvoid/glogger" "gopkg.in/gcfg.v1" "gopkg.in/redis.v5" ) type Config struct { Lorebot struct { Loglevel string BootstrapDelay time.Duration AuthFile string } Redis struct { Host string Password string } } var ( config = Config{} ) func main() { // read in config file readConf() // initialize the logger with the configured loglevel initLogger(config.Lorebot.Loglevel) // initialize redis connection redisClient, err := initRedisConnection() if err != nil { glogger.Debug.Printf("redis conneciton cannot be made, trying again in %d seconds", config.Lorebot.BootstrapDelay) time.Sleep(config.Lorebot.BootstrapDelay * time.Second) redisClient, err = initRedisConnection() if err != nil { glogger.Error.Println("redis connection cannot be made.") os.Exit(1) } } glogger.Debug.Println("connection to redis succeeded.") glogger.Info.Println("link to redis on", config.Redis.Host) // read in creds credentials, _ := ioutil.ReadFile(config.Lorebot.AuthFile) lines := strings.Split(string(credentials), "\n") auth := lines[0] api := slack.New(auth) //api.SetDebug(true) rtm := api.NewRTM() go rtm.ManageConnection() Loop: for { select { case msg := <-rtm.IncomingEvents: switch ev := msg.Data.(type) { case *slack.MessageEvent: if ev.Type == "message" { glogger.Debug.Printf("processing: %s", ev.Text) // see if the message begins with '.', it could be a command if len(ev.Text) == 0 { // nil message, dont print //glogger.Debug.Println("breaking on blank message") break } else { comm := fmt.Sprintf("%c", ev.Text[0]) if comm == "." { if strings.Count(ev.Text, " ") == 0 { // no spaces, its a static command staticCommandHandler(rtm, ev, redisClient) } else { // spaces, dynamic handler dynamicCommandHandler(rtm, ev, redisClient) } } } } case *slack.RTMError: fmt.Printf("Error: %s\n", ev.Error()) case *slack.InvalidAuthEvent: fmt.Printf("Invalid credentials") break Loop default: // ignore all other events //fmt.Printf("Unknown error") //fmt.Printf("%v\n", msg.Data) } } } } func readConf() { // init config file err := gcfg.ReadFileInto(&config, "config.gcfg") if err != nil { panic(fmt.Sprintf("Could not load config.gcfg, error: %s\n", err)) } } func initLogger(logLevel string) { // init logger if logLevel == "debug" { glogger.LogInit(os.Stdout, os.Stdout, os.Stdout, os.Stderr) } else if logLevel == "cluster" { glogger.LogInit(os.Stdout, os.Stdout, ioutil.Discard, os.Stderr) } else if logLevel == "info" { glogger.LogInit(os.Stdout, ioutil.Discard, ioutil.Discard, os.Stderr) } else { glogger.LogInit(ioutil.Discard, ioutil.Discard, ioutil.Discard, os.Stderr) } } func initRedisConnection() (*redis.Client, error) { // init redis connection redisClient := redis.NewClient(&redis.Options{ Addr: config.Redis.Host, Password: config.Redis.Password, DB: 0, }) _, redisErr := redisClient.Ping().Result() return redisClient, redisErr } func dynamicCommandHandler(rtm *slack.RTM, ev *slack.MessageEvent, redisClient *redis.Client) { message := ev.Text if strings.Count(message, " ") < 1 { glogger.Debug.Println("not proper syntax") } // verify syntax s := strings.SplitN(message, " ", 2) command, content := s[0], s[1] trimmedPrefix := strings.TrimPrefix(command, ".") switch trimmedPrefix { case "lore": s := strings.SplitN(string(ev.Text), " ", 2) contenthandler(rtm, ev, s[1], "lore", redisClient) case "newlore": if strings.Contains(content, " ") { newhandler(rtm, ev, content, "lore", redisClient) } else { rtm.SendMessage(rtm.NewOutgoingMessage("not proper syntax", ev.Channel)) return } case "rmlore": rmhandler(rtm, ev, content, "lore", redisClient) case "rek": s := strings.SplitN(string(ev.Text), " ", 2) rekhandler(rtm, ev, s[1], redisClient) case "gnu": s := strings.SplitN(string(ev.Text), " ", 2) gnuhandler(rtm, ev, s[1]) case "created": attrhandler(rtm, ev, "lore", content, "cdate", redisClient) case "owner": attrhandler(rtm, ev, "lore", content, "owner", redisClient) case "removedby": attrhandler(rtm, ev, "lore", content, "rmby", redisClient) case "removed": attrhandler(rtm, ev, "lore", content, "rmdate", redisClient) } } func staticCommandHandler(rtm *slack.RTM, ev *slack.MessageEvent, redisClient *redis.Client) { s := strings.SplitN(ev.Text, " ", 2) command := s[0] trimmedPrefix := strings.TrimPrefix(command, ".") switch trimmedPrefix { case "help": helpmsg(rtm, ev) case "help2": advancedhelpmsg(rtm, ev) case "rng": rtm.SendMessage(rtm.NewOutgoingMessage("command not yet supported", ev.Channel)) case "version": rtm.SendMessage(rtm.NewOutgoingMessage("command not yet supported", ev.Channel)) case "lorelist": listhandler(redisClient, rtm, ev, "lore", "added") case "removedlore": listhandler(redisClient, rtm, ev, "lore", "removed") case "commandlist": listhandler(redisClient, rtm, ev, "command", "added") case "removedcommands": listhandler(redisClient, rtm, ev, "command", "removed") case "lorestatus": lorestatus(rtm, ev, redisClient) } } func helpmsg(rtm *slack.RTM, ev *slack.MessageEvent) { help1 := "I am the lore archival bot, I support the following commands:\n" + "```" + ".lore: view lore\n" + ".newlore: add new lore\n" + ".rmlore: remove lore\n" + ".lorelist: view lore database\n" + "use '.help2' to see advanced lookup commands." + "```" rtm.SendMessage(rtm.NewOutgoingMessage(help1, ev.Channel)) } func advancedhelpmsg(rtm *slack.RTM, ev *slack.MessageEvent) { help2 := "```" + ".created view creation timestamp\n" + ".owner: who created the lore\n" + ".removed: view removal timestamp\n" + ".removedby: who removed the lore" + "```" rtm.SendMessage(rtm.NewOutgoingMessage(help2, ev.Channel)) } func contenthandler(rtm *slack.RTM, ev *slack.MessageEvent, message, queryType string, redisClient *redis.Client) { loreTerm := strings.Replace(message, ".lore", "", -1) println("searching for:", loreTerm) //rtm.SendMessage(rtm.NewOutgoingMessage(fmt.Sprintf("searching for '%s'\n", loreTerm), ev.Channel)) content, err := loreCheck(loreTerm, queryType, redisClient) if err == nil { // lore found, print it rtm.SendMessage(rtm.NewOutgoingMessage(content, ev.Channel)) } else { // lore not found, print error rtm.SendMessage(rtm.NewOutgoingMessage(fmt.Sprintf("lore '%s' not found\n", loreTerm), ev.Channel)) } } func attrhandler(rtm *slack.RTM, ev *slack.MessageEvent, queryType, queryAttr, message string, redisClient *redis.Client) { query, err := loreAttrQuery(message, queryType, queryAttr, redisClient) if err == nil { rtm.SendMessage(rtm.NewOutgoingMessage(query, ev.Channel)) } else { rtm.SendMessage(rtm.NewOutgoingMessage("attribute does not exist", ev.Channel)) } } func rekhandler(rtm *slack.RTM, ev *slack.MessageEvent, target string, redisClient *redis.Client) { rektBit, _ := redisClient.SRandMember("index:rekt").Result() rekFmt := fmt.Sprintf("%s %s", target, rektBit) rtm.SendMessage(rtm.NewOutgoingMessage(rekFmt, ev.Channel)) } func gnuhandler(rtm *slack.RTM, ev *slack.MessageEvent, target string) { gnuFmt := fmt.Sprintf("slaps the nonfree software out of %s's hands", target) rtm.SendMessage(rtm.NewOutgoingMessage(gnuFmt, ev.Channel)) } func listhandler(redisClient *redis.Client, rtm *slack.RTM, ev *slack.MessageEvent, listType, listAttr string) { // aka index:command:added newType := fmt.Sprintf("%s:%s:%s", "index", listType, listAttr) //command, err := botfunc.Query(newType, redisClient) command, err := loreQuery(newType, redisClient) if err != nil { glogger.Error.Println(err) println(err) } fCommand := strings.Join(command, "\n") // initialize buffer bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) // create header and body. filename must be set mh := make(textproto.MIMEHeader) mh.Set("Content-Type", "application/octet-stream") mh.Set("Content-Disposition", "form-data; name=\"file\"; filename=\" \"") // write body with mime header partWriter, _ := bodyWriter.CreatePart(mh) io.Copy(partWriter, bytes.NewBufferString(fCommand)) contentType := bodyWriter.FormDataContentType() bodyWriter.Close() // set content type to multipart/form-data, required for bitnuke.io h := make(textproto.MIMEHeader) h.Set("Content-Type", "multipart/form-data") bodyWriter.CreatePart(h) bodyWriter.Close() // POST to bitnuke.io resp, err := http.Post("https://bitnuke.io/upload", contentType, bodyBuf) if err != nil { println(err) } defer resp.Body.Close() resp_body, err := ioutil.ReadAll(resp.Body) if err != nil { println(err) } // format response to be link fResponse := fmt.Sprintf("https://bitnuke.io/%s", resp_body[:]) // print to channel rtm.SendMessage(rtm.NewOutgoingMessage(fResponse, ev.Channel)) } func lorestatus(rtm *slack.RTM, ev *slack.MessageEvent, redisClient *redis.Client) { command, _ := loreQuery("index:lore:added", redisClient) loreammount := len(command) status := fmt.Sprintf("the lore db has %d entries", loreammount) rtm.SendMessage(rtm.NewOutgoingMessage(status, ev.Channel)) } func newhandler(rtm *slack.RTM, ev *slack.MessageEvent, message, newType string, redisClient *redis.Client) { // dont allow certain chars //if strings.ContainsAny(message, ";:.,'\"\\/!$&^\(\)") { println(message) if strings.ContainsAny(message, ";:,.'\"!$%^*()/\\") { // reject user input, it has unwanted chars rtm.SendMessage(rtm.NewOutgoingMessage("entry could not be created, unwanted characters", ev.Channel)) return } // get nick from slack api user, err := rtm.GetUserInfo(ev.User) if err != nil { fmt.Printf("%s\n", err) return } nick := fmt.Sprintf("%s", user.Name) err = loreNewString(nick, message, newType, redisClient) if err == nil { rtm.SendMessage(rtm.NewOutgoingMessage("entry created", ev.Channel)) } else { rtm.SendMessage(rtm.NewOutgoingMessage("entry could not be created", ev.Channel)) } } func rmhandler(rtm *slack.RTM, ev *slack.MessageEvent, message, rmType string, redisClient *redis.Client) { // get nick from slack api user, err := rtm.GetUserInfo(ev.User) if err != nil { fmt.Printf("%s\n", err) return } nick := fmt.Sprintf("%s", user.Name) err = loreRmString(nick, message, rmType, redisClient) if err == nil { rtm.SendMessage(rtm.NewOutgoingMessage("entry removed", ev.Channel)) } else { rtm.SendMessage(rtm.NewOutgoingMessage("entry could not be removed", ev.Channel)) } }