diff --git a/bot/handlers.go b/bot/handlers.go index 2108afc..0a4781e 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -17,7 +17,7 @@ import ( "github.com/velour/catbase/bot/msg" ) -func (b *bot) Receive(kind Kind, msg msg.Message, args ...interface{}) { +func (b *bot) Receive(kind Kind, msg msg.Message, args ...interface{}) bool { log.Println("Received event: ", msg) // msg := b.buildMessage(client, inMsg) @@ -36,7 +36,7 @@ func (b *bot) Receive(kind Kind, msg msg.Message, args ...interface{}) { RET: b.logIn <- msg - return + return true } func (b *bot) runCallback(plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool { diff --git a/bot/interfaces.go b/bot/interfaces.go index 74eed27..eb318ca 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -49,7 +49,7 @@ type Bot interface { // First arg should be one of bot.Message/Reply/Action/etc Send(Kind, ...interface{}) (string, error) // First arg should be one of bot.Message/Reply/Action/etc - Receive(Kind, msg.Message, ...interface{}) + Receive(Kind, msg.Message, ...interface{}) bool // Register a callback Register(Plugin, Kind, Callback) @@ -63,7 +63,7 @@ type Bot interface { // Connector represents a server connection to a chat service type Connector interface { - RegisterEvent(func(Kind, msg.Message, ...interface{})) + RegisterEvent(Callback) Send(Kind, ...interface{}) (string, error) diff --git a/bot/mock.go b/bot/mock.go index ea8986e..ec48201 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -46,12 +46,12 @@ func (mb *MockBot) Send(kind Kind, args ...interface{}) (string, error) { } return "ERR", fmt.Errorf("Mesasge type unhandled") } -func (mb *MockBot) AddPlugin(f Plugin) {} -func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {} -func (mb *MockBot) Receive(kind Kind, msg msg.Message, args ...interface{}) {} -func (mb *MockBot) Filter(msg msg.Message, s string) string { return s } -func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil } -func (mb *MockBot) CheckAdmin(nick string) bool { return false } +func (mb *MockBot) AddPlugin(f Plugin) {} +func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {} +func (mb *MockBot) Receive(kind Kind, msg msg.Message, args ...interface{}) bool { return false } +func (mb *MockBot) Filter(msg msg.Message, s string) string { return s } +func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil } +func (mb *MockBot) CheckAdmin(nick string) bool { return false } func (mb *MockBot) react(channel, reaction string, message msg.Message) (string, error) { mb.Reactions = append(mb.Reactions, reaction) diff --git a/connectors/irc/irc.go b/connectors/irc/irc.go new file mode 100644 index 0000000..a10deea --- /dev/null +++ b/connectors/irc/irc.go @@ -0,0 +1,295 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +package irc + +import ( + "fmt" + "io" + "log" + "os" + "strings" + "time" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" + "github.com/velour/catbase/config" + "github.com/velour/velour/irc" +) + +const ( + // DefaultPort is the port used to connect to + // the server if one is not specified. + defaultPort = "6667" + + // InitialTimeout is the initial amount of time + // to delay before reconnecting. Each failed + // reconnection doubles the timout until + // a connection is made successfully. + initialTimeout = 2 * time.Second + + // PingTime is the amount of inactive time + // to wait before sending a ping to the server. + pingTime = 120 * time.Second + + actionPrefix = "\x01ACTION" +) + +var throttle <-chan time.Time + +type Irc struct { + Client *irc.Client + config *config.Config + quit chan bool + + event bot.Callback +} + +func New(c *config.Config) *Irc { + i := Irc{} + i.config = c + + return &i +} + +func (i *Irc) RegisterEvent(f bot.Callback) { + i.event = f +} + +func (i *Irc) Send(kind bot.Kind, args ...interface{}) (string, error) { + switch kind { + case bot.Reply: + case bot.Message: + return i.sendMessage(args[0].(string), args[1].(string)) + case bot.Action: + return i.sendAction(args[0].(string), args[1].(string)) + default: + } + return "", nil +} + +func (i *Irc) JoinChannel(channel string) { + log.Printf("Joining channel: %s", channel) + i.Client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{channel}} +} + +func (i *Irc) sendMessage(channel, message string) (string, error) { + for len(message) > 0 { + m := irc.Msg{ + Cmd: "PRIVMSG", + Args: []string{channel, message}, + } + _, err := m.RawString() + if err != nil { + mtl := err.(irc.MsgTooLong) + m.Args[1] = message[:mtl.NTrunc] + message = message[mtl.NTrunc:] + } else { + message = "" + } + + if throttle == nil { + ratePerSec := i.config.GetInt("RatePerSec", 5) + throttle = time.Tick(time.Second / time.Duration(ratePerSec)) + } + + <-throttle + + i.Client.Out <- m + } + return "NO_IRC_IDENTIFIERS", nil +} + +// Sends action to channel +func (i *Irc) sendAction(channel, message string) (string, error) { + message = actionPrefix + " " + message + "\x01" + + return i.sendMessage(channel, message) +} + +func (i *Irc) GetEmojiList() map[string]string { + //we're not going to do anything because it's IRC + return make(map[string]string) +} + +func (i *Irc) Serve() error { + if i.event == nil { + return fmt.Errorf("Missing an event handler") + } + + var err error + i.Client, err = irc.DialSSL( + i.config.Get("Irc.Server", "localhost"), + i.config.Get("Nick", "bot"), + i.config.Get("FullName", "bot"), + i.config.Get("Irc.Pass", ""), + true, + ) + if err != nil { + return fmt.Errorf("%s", err) + } + + for _, c := range i.config.GetArray("channels", []string{}) { + i.JoinChannel(c) + } + + i.quit = make(chan bool) + go i.handleConnection() + <-i.quit + return nil +} + +func (i *Irc) handleConnection() { + t := time.NewTimer(pingTime) + + defer func() { + t.Stop() + close(i.Client.Out) + for err := range i.Client.Errors { + if err != io.EOF { + log.Println(err) + } + } + }() + + for { + select { + case msg, ok := <-i.Client.In: + if !ok { // disconnect + i.quit <- true + return + } + t.Stop() + t = time.NewTimer(pingTime) + i.handleMsg(msg) + + case <-t.C: + i.Client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{i.Client.Server}} + t = time.NewTimer(pingTime) + + case err, ok := <-i.Client.Errors: + if ok && err != io.EOF { + log.Println(err) + i.quit <- true + return + } + } + } +} + +// HandleMsg handles IRC messages from the server. +func (i *Irc) handleMsg(msg irc.Msg) { + botMsg := i.buildMessage(msg) + + switch msg.Cmd { + case irc.ERROR: + log.Println(1, "Received error: "+msg.Raw) + + case irc.PING: + i.Client.Out <- irc.Msg{Cmd: irc.PONG} + + case irc.PONG: + // OK, ignore + + case irc.ERR_NOSUCHNICK: + fallthrough + + case irc.ERR_NOSUCHCHANNEL: + fallthrough + + case irc.RPL_MOTD: + fallthrough + + case irc.RPL_NAMREPLY: + fallthrough + + case irc.RPL_TOPIC: + fallthrough + + case irc.KICK: + fallthrough + + case irc.TOPIC: + fallthrough + + case irc.MODE: + fallthrough + + case irc.JOIN: + fallthrough + + case irc.PART: + fallthrough + + case irc.NOTICE: + fallthrough + + case irc.NICK: + fallthrough + + case irc.RPL_WHOREPLY: + fallthrough + + case irc.RPL_ENDOFWHO: + i.event(bot.Event, botMsg) + + case irc.PRIVMSG: + i.event(bot.Message, botMsg) + + case irc.QUIT: + os.Exit(1) + + default: + cmd := irc.CmdNames[msg.Cmd] + log.Println("(" + cmd + ") " + msg.Raw) + } +} + +// Builds our internal message type out of a Conn & Line from irc +func (i *Irc) buildMessage(inMsg irc.Msg) msg.Message { + // Check for the user + u := user.User{ + Name: inMsg.Origin, + } + + channel := inMsg.Args[0] + if channel == i.config.Get("Nick", "bot") { + channel = inMsg.Args[0] + } + + isAction := false + var message string + if len(inMsg.Args) > 1 { + message = inMsg.Args[1] + + isAction = strings.HasPrefix(message, actionPrefix) + if isAction { + message = strings.TrimRight(message[len(actionPrefix):], "\x01") + message = strings.TrimSpace(message) + } + + } + + iscmd := false + filteredMessage := message + if !isAction { + iscmd, filteredMessage = bot.IsCmd(i.config, message) + } + + msg := msg.Message{ + User: &u, + Channel: channel, + Body: filteredMessage, + Raw: message, + Command: iscmd, + Action: isAction, + Time: time.Now(), + Host: inMsg.Host, + } + + return msg +} + +func (i Irc) Who(channel string) []string { + return []string{} +} diff --git a/connectors/slack/fix_text.go b/connectors/slack/fix_text.go new file mode 100644 index 0000000..28a8c1e --- /dev/null +++ b/connectors/slack/fix_text.go @@ -0,0 +1,96 @@ +package slack + +import ( + "unicode/utf8" +) + +// fixText strips all of the Slack-specific annotations from message text, +// replacing it with the equivalent display form. +// Currently it: +// • Replaces user mentions like <@U124356> with @ followed by the user's nick. +// This uses the lookupUser function, which must map U1243456 to the nick. +// • Replaces user mentions like with the user's nick. +// • Strips < and > surrounding links. +// +// This was directly bogarted from velour/chat with emoji conversion removed. +func fixText(findUser func(id string) (string, bool), text string) string { + var output []rune + for len(text) > 0 { + r, i := utf8.DecodeRuneInString(text) + text = text[i:] + switch { + case r == '<': + var tag []rune + for { + r, i := utf8.DecodeRuneInString(text) + text = text[i:] + switch { + case r == '>': + if t, ok := fixTag(findUser, tag); ok { + output = append(output, t...) + break + } + fallthrough + case len(text) == 0: + output = append(output, '<') + output = append(output, tag...) + output = append(output, r) + default: + tag = append(tag, r) + continue + } + break + } + default: + output = append(output, r) + } + } + return string(output) +} + +func fixTag(findUser func(string) (string, bool), tag []rune) ([]rune, bool) { + switch { + case hasPrefix(tag, "@U"): + if i := indexRune(tag, '|'); i >= 0 { + return tag[i+1:], true + } + if findUser != nil { + if u, ok := findUser(string(tag[1:])); ok { + return []rune(u), true + } + } + return tag, true + + case hasPrefix(tag, "#C"): + if i := indexRune(tag, '|'); i >= 0 { + return append([]rune{'#'}, tag[i+1:]...), true + } + + case hasPrefix(tag, "http"): + if i := indexRune(tag, '|'); i >= 0 { + tag = tag[:i] + } + return tag, true + } + + return nil, false +} + +func hasPrefix(text []rune, prefix string) bool { + for _, r := range prefix { + if len(text) == 0 || text[0] != r { + return false + } + text = text[1:] + } + return true +} + +func indexRune(text []rune, find rune) int { + for i, r := range text { + if r == find { + return i + } + } + return -1 +} diff --git a/connectors/slack/slack.go b/connectors/slack/slack.go new file mode 100644 index 0000000..6fe4691 --- /dev/null +++ b/connectors/slack/slack.go @@ -0,0 +1,736 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +// Package slack connects to slack service +package slack + +import ( + "encoding/json" + "errors" + "fmt" + "html" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + // "sync/atomic" + "context" + "time" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" + "github.com/velour/catbase/config" + "github.com/velour/chat/websocket" +) + +type Slack struct { + config *config.Config + + url string + id string + token string + ws *websocket.Conn + + lastRecieved time.Time + + users map[string]string + + myBotID string + + emoji map[string]string + + event bot.Callback +} + +var idCounter uint64 + +type slackUserInfoResp struct { + Ok bool `json:"ok"` + User struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"user"` +} + +type slackChannelListItem struct { + ID string `json:"id"` + Name string `json:"name"` + IsChannel bool `json:"is_channel"` + Created int `json:"created"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + IsGeneral bool `json:"is_general"` + NameNormalized string `json:"name_normalized"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsMember bool `json:"is_member"` + Members []string `json:"members"` + Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"topic"` + Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"purpose"` + PreviousNames []interface{} `json:"previous_names"` + NumMembers int `json:"num_members"` +} + +type slackChannelListResp struct { + Ok bool `json:"ok"` + Channels []slackChannelListItem `json:"channels"` +} + +type slackChannelInfoResp struct { + Ok bool `json:"ok"` + Channel struct { + ID string `json:"id"` + Name string `json:"name"` + IsChannel bool `json:"is_channel"` + Created int `json:"created"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + IsGeneral bool `json:"is_general"` + NameNormalized string `json:"name_normalized"` + IsReadOnly bool `json:"is_read_only"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsMember bool `json:"is_member"` + LastRead string `json:"last_read"` + Latest struct { + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + Ts string `json:"ts"` + } `json:"latest"` + UnreadCount int `json:"unread_count"` + UnreadCountDisplay int `json:"unread_count_display"` + Members []string `json:"members"` + Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int64 `json:"last_set"` + } `json:"topic"` + Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"purpose"` + PreviousNames []string `json:"previous_names"` + } `json:"channel"` +} + +type slackMessage struct { + ID uint64 `json:"id"` + Type string `json:"type"` + SubType string `json:"subtype"` + Hidden bool `json:"hidden"` + Channel string `json:"channel"` + Text string `json:"text"` + User string `json:"user"` + Username string `json:"username"` + BotID string `json:"bot_id"` + Ts string `json:"ts"` + ThreadTs string `json:"thread_ts"` + Error struct { + Code uint64 `json:"code"` + Msg string `json:"msg"` + } `json:"error"` +} + +type slackReaction struct { + Reaction string `json:"name"` + Channel string `json:"channel"` + Timestamp float64 `json:"timestamp"` +} + +type rtmStart struct { + Ok bool `json:"ok"` + Error string `json:"error"` + URL string `json:"url"` + Self struct { + ID string `json:"id"` + } `json:"self"` +} + +func New(c *config.Config) *Slack { + token := c.Get("slack.token", "NONE") + if token == "NONE" { + log.Fatalf("No slack token found. Set SLACKTOKEN env.") + } + return &Slack{ + config: c, + token: c.Get("slack.token", ""), + lastRecieved: time.Now(), + users: make(map[string]string), + emoji: make(map[string]string), + } +} + +func (s *Slack) Send(kind bot.Kind, args ...interface{}) (string, error) { + switch kind { + case bot.Message: + return s.sendMessage(args[0].(string), args[1].(string)) + case bot.Action: + return s.sendAction(args[0].(string), args[1].(string)) + case bot.Edit: + return s.edit(args[0].(string), args[1].(string), args[2].(string)) + case bot.Reply: + switch args[2].(type) { + case msg.Message: + return s.replyToMessage(args[0].(string), args[1].(string), args[2].(msg.Message)) + case string: + return s.replyToMessageIdentifier(args[0].(string), args[1].(string), args[2].(string)) + default: + return "", fmt.Errorf("Invalid types given to Reply") + } + case bot.Reaction: + return s.react(args[0].(string), args[1].(string), args[2].(msg.Message)) + default: + } + return "", fmt.Errorf("No handler for message type %d", kind) +} + +func checkReturnStatus(response *http.Response) error { + type Response struct { + OK bool `json:"ok"` + } + + body, err := ioutil.ReadAll(response.Body) + response.Body.Close() + if err != nil { + err := fmt.Errorf("Error reading Slack API body: %s", err) + return err + } + + var resp Response + err = json.Unmarshal(body, &resp) + if err != nil { + err := fmt.Errorf("Error parsing message response: %s", err) + return err + } + return nil +} + +func (s *Slack) RegisterEvent(f bot.Callback) { + s.event = f +} + +func (s *Slack) sendMessageType(channel, message string, meMessage bool) (string, error) { + postUrl := "https://slack.com/api/chat.postMessage" + if meMessage { + postUrl = "https://slack.com/api/chat.meMessage" + } + + nick := s.config.Get("Nick", "bot") + icon := s.config.Get("IconURL", "https://placekitten.com/128/128") + + resp, err := http.PostForm(postUrl, + url.Values{"token": {s.token}, + "username": {nick}, + "icon_url": {icon}, + "channel": {channel}, + "text": {message}, + }) + + if err != nil { + log.Printf("Error sending Slack message: %s", err) + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Fatalf("Error reading Slack API body: %s", err) + } + + log.Println(string(body)) + + type MessageResponse struct { + OK bool `json:"ok"` + Timestamp string `json:"ts"` + Message struct { + BotID string `json:"bot_id"` + } `json:"message"` + } + + var mr MessageResponse + err = json.Unmarshal(body, &mr) + if err != nil { + log.Fatalf("Error parsing message response: %s", err) + } + + if !mr.OK { + return "", errors.New("failure response received") + } + + s.myBotID = mr.Message.BotID + + return mr.Timestamp, err +} + +func (s *Slack) sendMessage(channel, message string) (string, error) { + log.Printf("Sending message to %s: %s", channel, message) + identifier, err := s.sendMessageType(channel, message, false) + return identifier, err +} + +func (s *Slack) sendAction(channel, message string) (string, error) { + log.Printf("Sending action to %s: %s", channel, message) + identifier, err := s.sendMessageType(channel, "_"+message+"_", true) + return identifier, err +} + +func (s *Slack) replyToMessageIdentifier(channel, message, identifier string) (string, error) { + nick := s.config.Get("Nick", "bot") + icon := s.config.Get("IconURL", "https://placekitten.com/128/128") + + resp, err := http.PostForm("https://slack.com/api/chat.postMessage", + url.Values{"token": {s.token}, + "username": {nick}, + "icon_url": {icon}, + "channel": {channel}, + "text": {message}, + "thread_ts": {identifier}, + }) + + if err != nil { + err := fmt.Errorf("Error sending Slack reply: %s", err) + return "", err + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + err := fmt.Errorf("Error reading Slack API body: %s", err) + return "", err + } + + log.Println(string(body)) + + type MessageResponse struct { + OK bool `json:"ok"` + Timestamp string `json:"ts"` + } + + var mr MessageResponse + err = json.Unmarshal(body, &mr) + if err != nil { + err := fmt.Errorf("Error parsing message response: %s", err) + return "", err + } + + if !mr.OK { + return "", fmt.Errorf("Got !OK from slack message response") + } + + return mr.Timestamp, err +} + +func (s *Slack) replyToMessage(channel, message string, replyTo msg.Message) (string, error) { + return s.replyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"]) +} + +func (s *Slack) react(channel, reaction string, message msg.Message) (string, error) { + log.Printf("Reacting in %s: %s", channel, reaction) + resp, err := http.PostForm("https://slack.com/api/reactions.add", + url.Values{"token": {s.token}, + "name": {reaction}, + "channel": {channel}, + "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) + if err != nil { + err := fmt.Errorf("reaction failed: %s", err) + return "", err + } + return "", checkReturnStatus(resp) +} + +func (s *Slack) edit(channel, newMessage, identifier string) (string, error) { + log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage) + resp, err := http.PostForm("https://slack.com/api/chat.update", + url.Values{"token": {s.token}, + "channel": {channel}, + "text": {newMessage}, + "ts": {identifier}}) + if err != nil { + err := fmt.Errorf("edit failed: %s", err) + return "", err + } + return "", checkReturnStatus(resp) +} + +func (s *Slack) GetEmojiList() map[string]string { + return s.emoji +} + +func (s *Slack) populateEmojiList() { + resp, err := http.PostForm("https://slack.com/api/emoji.list", + url.Values{"token": {s.token}}) + if err != nil { + log.Printf("Error retrieving emoji list from Slack: %s", err) + return + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Fatalf("Error reading Slack API body: %s", err) + } + + type EmojiListResponse struct { + OK bool `json:"ok"` + Emoji map[string]string `json:"emoji"` + } + + var list EmojiListResponse + err = json.Unmarshal(body, &list) + if err != nil { + log.Fatalf("Error parsing emoji list: %s", err) + } + s.emoji = list.Emoji +} + +func (s *Slack) ping(ctx context.Context) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + ping := map[string]interface{}{"type": "ping", "time": time.Now().UnixNano()} + if err := s.ws.Send(context.TODO(), ping); err != nil { + panic(err) + } + } + } +} + +func (s *Slack) receiveMessage() (slackMessage, error) { + m := slackMessage{} + err := s.ws.Recv(context.TODO(), &m) + if err != nil { + log.Println("Error decoding WS message") + panic(fmt.Errorf("%v\n%v", m, err)) + } + return m, nil +} + +// I think it's horseshit that I have to do this +func slackTStoTime(t string) time.Time { + ts := strings.Split(t, ".") + sec, _ := strconv.ParseInt(ts[0], 10, 64) + nsec, _ := strconv.ParseInt(ts[1], 10, 64) + return time.Unix(sec, nsec) +} + +func (s *Slack) Serve() error { + s.connect() + s.populateEmojiList() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go s.ping(ctx) + + for { + msg, err := s.receiveMessage() + if err != nil && err == io.EOF { + log.Fatalf("Slack API EOF") + } else if err != nil { + return fmt.Errorf("Slack API error: %s", err) + } + switch msg.Type { + case "message": + isItMe := msg.BotID != "" && msg.BotID == s.myBotID + if !isItMe && !msg.Hidden && msg.ThreadTs == "" { + m := s.buildMessage(msg) + if m.Time.Before(s.lastRecieved) { + log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time) + } else { + s.lastRecieved = m.Time + s.event(bot.Message, m) + } + } else if msg.ThreadTs != "" { + //we're throwing away some information here by not parsing the correct reply object type, but that's okay + s.event(bot.Reply, s.buildLightReplyMessage(msg), msg.ThreadTs) + } else { + log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) + } + case "error": + log.Printf("Slack error, code: %d, message: %s", msg.Error.Code, msg.Error.Msg) + case "": // what even is this? + case "hello": + case "presence_change": + case "user_typing": + case "reconnect_url": + case "desktop_notification": + case "pong": + // squeltch this stuff + continue + default: + log.Printf("Unhandled Slack message type: '%s'", msg.Type) + } + } +} + +var urlDetector = regexp.MustCompile(`<(.+)://([^|^>]+).*>`) + +// Convert a slackMessage to a msg.Message +func (s *Slack) buildMessage(m slackMessage) msg.Message { + text := html.UnescapeString(m.Text) + + text = fixText(s.getUser, text) + + isCmd, text := bot.IsCmd(s.config, text) + + isAction := m.SubType == "me_message" + + u, _ := s.getUser(m.User) + if m.Username != "" { + u = m.Username + } + + tstamp := slackTStoTime(m.Ts) + + return msg.Message{ + User: &user.User{ + ID: m.User, + Name: u, + }, + Body: text, + Raw: m.Text, + Channel: m.Channel, + Command: isCmd, + Action: isAction, + Host: string(m.ID), + Time: tstamp, + AdditionalData: map[string]string{ + "RAW_SLACK_TIMESTAMP": m.Ts, + }, + } +} + +func (s *Slack) buildLightReplyMessage(m slackMessage) msg.Message { + text := html.UnescapeString(m.Text) + + text = fixText(s.getUser, text) + + isCmd, text := bot.IsCmd(s.config, text) + + isAction := m.SubType == "me_message" + + u, _ := s.getUser(m.User) + if m.Username != "" { + u = m.Username + } + + tstamp := slackTStoTime(m.Ts) + + return msg.Message{ + User: &user.User{ + ID: m.User, + Name: u, + }, + Body: text, + Raw: m.Text, + Channel: m.Channel, + Command: isCmd, + Action: isAction, + Host: string(m.ID), + Time: tstamp, + AdditionalData: map[string]string{ + "RAW_SLACK_TIMESTAMP": m.Ts, + }, + } +} + +// markAllChannelsRead gets a list of all channels and marks each as read +func (s *Slack) markAllChannelsRead() { + chs := s.getAllChannels() + log.Printf("Got list of channels to mark read: %+v", chs) + for _, ch := range chs { + s.markChannelAsRead(ch.ID) + } + log.Printf("Finished marking channels read") +} + +// getAllChannels returns info for all channels joined +func (s *Slack) getAllChannels() []slackChannelListItem { + u := s.url + "channels.list" + resp, err := http.PostForm(u, + url.Values{"token": {s.token}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return nil + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return nil + } + defer resp.Body.Close() + var chanInfo slackChannelListResp + err = json.NewDecoder(resp.Body).Decode(&chanInfo) + if err != nil || !chanInfo.Ok { + log.Println("Error decoding response: ", err) + return nil + } + return chanInfo.Channels +} + +// markAsRead marks a channel read +func (s *Slack) markChannelAsRead(slackChanId string) error { + u := s.url + "channels.info" + resp, err := http.PostForm(u, + url.Values{"token": {s.token}, "channel": {slackChanId}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return err + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return err + } + defer resp.Body.Close() + var chanInfo slackChannelInfoResp + err = json.NewDecoder(resp.Body).Decode(&chanInfo) + log.Printf("%+v, %+v", err, chanInfo) + if err != nil || !chanInfo.Ok { + log.Println("Error decoding response: ", err) + return err + } + + u = s.url + "channels.mark" + resp, err = http.PostForm(u, + url.Values{"token": {s.token}, "channel": {slackChanId}, "ts": {chanInfo.Channel.Latest.Ts}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return err + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return err + } + defer resp.Body.Close() + var markInfo map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&markInfo) + log.Printf("%+v, %+v", err, markInfo) + if err != nil { + log.Println("Error decoding response: ", err) + return err + } + + log.Printf("Marked %s as read", slackChanId) + return nil +} + +func (s *Slack) connect() { + token := s.token + u := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token) + resp, err := http.Get(u) + if err != nil { + return + } + if resp.StatusCode != 200 { + log.Fatalf("Slack API failed. Code: %d", resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Fatalf("Error reading Slack API body: %s", err) + } + var rtm rtmStart + err = json.Unmarshal(body, &rtm) + if err != nil { + return + } + + if !rtm.Ok { + log.Fatalf("Slack error: %s", rtm.Error) + } + + s.url = "https://slack.com/api/" + s.id = rtm.Self.ID + + // This is hitting the rate limit, and it may not be needed + //s.markAllChannelsRead() + + rtmURL, _ := url.Parse(rtm.URL) + s.ws, err = websocket.Dial(context.TODO(), rtmURL) + if err != nil { + log.Fatal(err) + } +} + +// Get username for Slack user ID +func (s *Slack) getUser(id string) (string, bool) { + if name, ok := s.users[id]; ok { + return name, true + } + + log.Printf("User %s not already found, requesting info", id) + u := s.url + "users.info" + resp, err := http.PostForm(u, + url.Values{"token": {s.token}, "user": {id}}) + if err != nil || resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d %s", + resp.StatusCode, err) + return "UNKNOWN", false + } + defer resp.Body.Close() + var userInfo slackUserInfoResp + err = json.NewDecoder(resp.Body).Decode(&userInfo) + if err != nil { + log.Println("Error decoding response: ", err) + return "UNKNOWN", false + } + s.users[id] = userInfo.User.Name + return s.users[id], true +} + +// Who gets usernames out of a channel +func (s *Slack) Who(id string) []string { + log.Println("Who is queried for ", id) + u := s.url + "channels.info" + resp, err := http.PostForm(u, + url.Values{"token": {s.token}, "channel": {id}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return []string{} + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return []string{} + } + defer resp.Body.Close() + var chanInfo slackChannelInfoResp + err = json.NewDecoder(resp.Body).Decode(&chanInfo) + if err != nil || !chanInfo.Ok { + log.Println("Error decoding response: ", err) + return []string{} + } + + log.Printf("%#v", chanInfo.Channel) + + handles := []string{} + for _, member := range chanInfo.Channel.Members { + u, _ := s.getUser(member) + handles = append(handles, u) + } + log.Printf("Returning %d handles", len(handles)) + return handles +} diff --git a/connectors/slackapp/fix_text.go b/connectors/slackapp/fix_text.go new file mode 100644 index 0000000..24735ad --- /dev/null +++ b/connectors/slackapp/fix_text.go @@ -0,0 +1,96 @@ +package slackapp + +import ( + "unicode/utf8" +) + +// fixText strips all of the Slack-specific annotations from message text, +// replacing it with the equivalent display form. +// Currently it: +// • Replaces user mentions like <@U124356> with @ followed by the user's nick. +// This uses the lookupUser function, which must map U1243456 to the nick. +// • Replaces user mentions like with the user's nick. +// • Strips < and > surrounding links. +// +// This was directly bogarted from velour/chat with emoji conversion removed. +func fixText(findUser func(id string) (string, bool), text string) string { + var output []rune + for len(text) > 0 { + r, i := utf8.DecodeRuneInString(text) + text = text[i:] + switch { + case r == '<': + var tag []rune + for { + r, i := utf8.DecodeRuneInString(text) + text = text[i:] + switch { + case r == '>': + if t, ok := fixTag(findUser, tag); ok { + output = append(output, t...) + break + } + fallthrough + case len(text) == 0: + output = append(output, '<') + output = append(output, tag...) + output = append(output, r) + default: + tag = append(tag, r) + continue + } + break + } + default: + output = append(output, r) + } + } + return string(output) +} + +func fixTag(findUser func(string) (string, bool), tag []rune) ([]rune, bool) { + switch { + case hasPrefix(tag, "@U"): + if i := indexRune(tag, '|'); i >= 0 { + return tag[i+1:], true + } + if findUser != nil { + if u, ok := findUser(string(tag[1:])); ok { + return []rune(u), true + } + } + return tag, true + + case hasPrefix(tag, "#C"): + if i := indexRune(tag, '|'); i >= 0 { + return append([]rune{'#'}, tag[i+1:]...), true + } + + case hasPrefix(tag, "http"): + if i := indexRune(tag, '|'); i >= 0 { + tag = tag[:i] + } + return tag, true + } + + return nil, false +} + +func hasPrefix(text []rune, prefix string) bool { + for _, r := range prefix { + if len(text) == 0 || text[0] != r { + return false + } + text = text[1:] + } + return true +} + +func indexRune(text []rune, find rune) int { + for i, r := range text { + if r == find { + return i + } + } + return -1 +} diff --git a/connectors/slackapp/slackApp.go b/connectors/slackapp/slackApp.go new file mode 100644 index 0000000..24e0493 --- /dev/null +++ b/connectors/slackapp/slackApp.go @@ -0,0 +1,447 @@ +package slackapp + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "html" + "io/ioutil" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "github.com/nlopes/slack" + "github.com/nlopes/slack/slackevents" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" + "github.com/velour/catbase/config" +) + +type SlackApp struct { + bot bot.Bot + config *config.Config + api *slack.Client + + token string + verification string + id string + + lastRecieved time.Time + + myBotID string + users map[string]string + emoji map[string]string + + event bot.Callback +} + +func New(c *config.Config) *SlackApp { + token := c.Get("slack.token", "NONE") + if token == "NONE" { + log.Fatalf("No slack token found. Set SLACKTOKEN env.") + } + + dbg := slack.OptionDebug(true) + api := slack.New(token) + dbg(api) + + return &SlackApp{ + api: api, + config: c, + token: token, + verification: c.Get("slack.verification", "NONE"), + lastRecieved: time.Now(), + users: make(map[string]string), + emoji: make(map[string]string), + } +} + +func (s *SlackApp) RegisterEvent(f bot.Callback) { + s.event = f +} + +func (s *SlackApp) Serve() error { + s.populateEmojiList() + http.HandleFunc("/evt", func(w http.ResponseWriter, r *http.Request) { + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + body := buf.String() + eventsAPIEvent, e := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionVerifyToken(&slackevents.TokenComparator{VerificationToken: s.verification})) + if e != nil { + log.Println(e) + w.WriteHeader(http.StatusInternalServerError) + } + + if eventsAPIEvent.Type == slackevents.URLVerification { + var r *slackevents.ChallengeResponse + err := json.Unmarshal([]byte(body), &r) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "text") + w.Write([]byte(r.Challenge)) + } else if eventsAPIEvent.Type == slackevents.CallbackEvent { + innerEvent := eventsAPIEvent.InnerEvent + switch ev := innerEvent.Data.(type) { + case *slackevents.AppMentionEvent: + // This is a bit of a problem. AppMentionEvent also needs to + // End up in msgReceived + //s.msgReceivd(ev) + case *slackevents.MessageEvent: + s.msgReceivd(ev) + } + } + }) + log.Println("[INFO] Server listening") + log.Fatal(http.ListenAndServe("0.0.0.0:1337", nil)) + return nil +} + +func (s *SlackApp) msgReceivd(msg *slackevents.MessageEvent) { + isItMe := msg.BotID != "" && msg.BotID == s.myBotID + if !isItMe && msg.ThreadTimeStamp == "" { + m := s.buildMessage(msg) + if m.Time.Before(s.lastRecieved) { + log.Printf("Ignoring message: lastRecieved: %v msg: %v", s.lastRecieved, m.Time) + } else { + s.lastRecieved = m.Time + s.event(bot.Message, m) + } + } else if msg.ThreadTimeStamp != "" { + //we're throwing away some information here by not parsing the correct reply object type, but that's okay + s.event(bot.Reply, s.buildLightReplyMessage(msg), msg.ThreadTimeStamp) + } else { + log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.Text) + } +} + +func (s *SlackApp) Send(kind bot.Kind, args ...interface{}) (string, error) { + // TODO: All of these local calls to slack should get routed through the library + switch kind { + case bot.Message: + return s.sendMessage(args[0].(string), args[1].(string)) + case bot.Action: + return s.sendAction(args[0].(string), args[1].(string)) + case bot.Edit: + return s.edit(args[0].(string), args[1].(string), args[2].(string)) + case bot.Reply: + switch args[2].(type) { + case msg.Message: + return s.replyToMessage(args[0].(string), args[1].(string), args[2].(msg.Message)) + case string: + return s.replyToMessageIdentifier(args[0].(string), args[1].(string), args[2].(string)) + default: + return "", fmt.Errorf("Invalid types given to Reply") + } + case bot.Reaction: + return s.react(args[0].(string), args[1].(string), args[2].(msg.Message)) + default: + } + return "", fmt.Errorf("No handler for message type %d", kind) +} + +func (s *SlackApp) sendMessageType(channel, message string, meMessage bool) (string, error) { + postUrl := "https://slack.com/api/chat.postMessage" + if meMessage { + postUrl = "https://slack.com/api/chat.meMessage" + } + + nick := s.config.Get("Nick", "bot") + icon := s.config.Get("IconURL", "https://placekitten.com/128/128") + + resp, err := http.PostForm(postUrl, + url.Values{"token": {s.token}, + "username": {nick}, + "icon_url": {icon}, + "channel": {channel}, + "text": {message}, + }) + + if err != nil { + log.Printf("Error sending Slack message: %s", err) + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Fatalf("Error reading Slack API body: %s", err) + } + + log.Println(string(body)) + + type MessageResponse struct { + OK bool `json:"ok"` + Timestamp string `json:"ts"` + Message struct { + BotID string `json:"bot_id"` + } `json:"message"` + } + + var mr MessageResponse + err = json.Unmarshal(body, &mr) + if err != nil { + log.Fatalf("Error parsing message response: %s", err) + } + + if !mr.OK { + return "", errors.New("failure response received") + } + + s.myBotID = mr.Message.BotID + + return mr.Timestamp, err +} + +func (s *SlackApp) sendMessage(channel, message string) (string, error) { + log.Printf("Sending message to %s: %s", channel, message) + identifier, err := s.sendMessageType(channel, message, false) + return identifier, err +} + +func (s *SlackApp) sendAction(channel, message string) (string, error) { + log.Printf("Sending action to %s: %s", channel, message) + identifier, err := s.sendMessageType(channel, "_"+message+"_", true) + return identifier, err +} + +func (s *SlackApp) replyToMessageIdentifier(channel, message, identifier string) (string, error) { + nick := s.config.Get("Nick", "bot") + icon := s.config.Get("IconURL", "https://placekitten.com/128/128") + + resp, err := http.PostForm("https://slack.com/api/chat.postMessage", + url.Values{"token": {s.token}, + "username": {nick}, + "icon_url": {icon}, + "channel": {channel}, + "text": {message}, + "thread_ts": {identifier}, + }) + + if err != nil { + err := fmt.Errorf("Error sending Slack reply: %s", err) + return "", err + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + err := fmt.Errorf("Error reading Slack API body: %s", err) + return "", err + } + + log.Println(string(body)) + + type MessageResponse struct { + OK bool `json:"ok"` + Timestamp string `json:"ts"` + } + + var mr MessageResponse + err = json.Unmarshal(body, &mr) + if err != nil { + err := fmt.Errorf("Error parsing message response: %s", err) + return "", err + } + + if !mr.OK { + return "", fmt.Errorf("Got !OK from slack message response") + } + + return mr.Timestamp, err +} + +func (s *SlackApp) replyToMessage(channel, message string, replyTo msg.Message) (string, error) { + return s.replyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"]) +} + +func (s *SlackApp) react(channel, reaction string, message msg.Message) (string, error) { + log.Printf("Reacting in %s: %s", channel, reaction) + resp, err := http.PostForm("https://slack.com/api/reactions.add", + url.Values{"token": {s.token}, + "name": {reaction}, + "channel": {channel}, + "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) + if err != nil { + err := fmt.Errorf("reaction failed: %s", err) + return "", err + } + return "", checkReturnStatus(resp) +} + +func (s *SlackApp) edit(channel, newMessage, identifier string) (string, error) { + log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage) + resp, err := http.PostForm("https://slack.com/api/chat.update", + url.Values{"token": {s.token}, + "channel": {channel}, + "text": {newMessage}, + "ts": {identifier}}) + if err != nil { + err := fmt.Errorf("edit failed: %s", err) + return "", err + } + return "", checkReturnStatus(resp) +} + +func (s *SlackApp) GetEmojiList() map[string]string { + return s.emoji +} + +func (s *SlackApp) populateEmojiList() { + resp, err := http.PostForm("https://slack.com/api/emoji.list", + url.Values{"token": {s.token}}) + if err != nil { + log.Printf("Error retrieving emoji list from Slack: %s", err) + return + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Fatalf("Error reading Slack API body: %s", err) + } + + type EmojiListResponse struct { + OK bool `json:"ok"` + Emoji map[string]string `json:"emoji"` + } + + var list EmojiListResponse + err = json.Unmarshal(body, &list) + if err != nil { + log.Fatalf("Error parsing emoji list: %s", err) + } + s.emoji = list.Emoji +} + +// I think it's horseshit that I have to do this +func slackTStoTime(t string) time.Time { + ts := strings.Split(t, ".") + sec, _ := strconv.ParseInt(ts[0], 10, 64) + nsec, _ := strconv.ParseInt(ts[1], 10, 64) + return time.Unix(sec, nsec) +} + +func checkReturnStatus(response *http.Response) error { + type Response struct { + OK bool `json:"ok"` + } + + body, err := ioutil.ReadAll(response.Body) + response.Body.Close() + if err != nil { + err := fmt.Errorf("Error reading Slack API body: %s", err) + return err + } + + var resp Response + err = json.Unmarshal(body, &resp) + if err != nil { + err := fmt.Errorf("Error parsing message response: %s", err) + return err + } + return nil +} + +var urlDetector = regexp.MustCompile(`<(.+)://([^|^>]+).*>`) + +// Convert a slackMessage to a msg.Message +func (s *SlackApp) buildMessage(m *slackevents.MessageEvent) msg.Message { + text := html.UnescapeString(m.Text) + + text = fixText(s.getUser, text) + + isCmd, text := bot.IsCmd(s.config, text) + + isAction := m.SubType == "me_message" + + u, _ := s.getUser(m.User) + if m.Username != "" { + u = m.Username + } + + tstamp := slackTStoTime(m.TimeStamp) + + return msg.Message{ + User: &user.User{ + ID: m.User, + Name: u, + }, + Body: text, + Raw: m.Text, + Channel: m.Channel, + Command: isCmd, + Action: isAction, + Time: tstamp, + AdditionalData: map[string]string{ + "RAW_SLACK_TIMESTAMP": m.TimeStamp, + }, + } +} + +func (s *SlackApp) buildLightReplyMessage(m *slackevents.MessageEvent) msg.Message { + text := html.UnescapeString(m.Text) + + text = fixText(s.getUser, text) + + isCmd, text := bot.IsCmd(s.config, text) + + isAction := m.SubType == "me_message" + + u, _ := s.getUser(m.User) + if m.Username != "" { + u = m.Username + } + + tstamp := slackTStoTime(m.TimeStamp) + + return msg.Message{ + User: &user.User{ + ID: m.User, + Name: u, + }, + Body: text, + Raw: m.Text, + Channel: m.Channel, + Command: isCmd, + Action: isAction, + Time: tstamp, + AdditionalData: map[string]string{ + "RAW_SLACK_TIMESTAMP": m.TimeStamp, + }, + } +} + +// Get username for Slack user ID +func (s *SlackApp) getUser(id string) (string, bool) { + if name, ok := s.users[id]; ok { + return name, true + } + + log.Printf("User %s not already found, requesting info", id) + u, err := s.api.GetUserInfo(id) + if err != nil { + return "UNKNOWN", false + } + s.users[id] = u.Name + return s.users[id], true +} + +// Who gets usernames out of a channel +func (s *SlackApp) Who(id string) []string { + log.Println("Who is queried for ", id) + // Not super sure this is the correct call + members, err := s.api.GetUserGroupMembers(id) + if err != nil { + log.Println(err) + return []string{} + } + return members +} diff --git a/go.mod b/go.mod index 45d8122..d05e371 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mmcdole/gofeed v1.0.0-beta2 github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect + github.com/nlopes/slack v0.5.0 + github.com/pkg/errors v0.8.1 // indirect github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 2b566da..cf9d3cc 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,10 @@ github.com/mmcdole/gofeed v1.0.0-beta2 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9Bx github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= +github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= +github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4= diff --git a/main.go b/main.go index 6ab529d..bb0dae5 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,9 @@ import ( "github.com/velour/catbase/bot" "github.com/velour/catbase/config" - "github.com/velour/catbase/irc" + "github.com/velour/catbase/connectors/irc" + "github.com/velour/catbase/connectors/slack" + "github.com/velour/catbase/connectors/slackapp" "github.com/velour/catbase/plugins/admin" "github.com/velour/catbase/plugins/babbler" "github.com/velour/catbase/plugins/beers" @@ -35,7 +37,6 @@ import ( "github.com/velour/catbase/plugins/twitch" "github.com/velour/catbase/plugins/your" "github.com/velour/catbase/plugins/zork" - "github.com/velour/catbase/slack" ) var ( @@ -67,11 +68,13 @@ func main() { var client bot.Connector - switch c.Get("type", "slack") { + switch c.Get("type", "slackapp") { case "irc": client = irc.New(c) case "slack": client = slack.New(c) + case "slackapp": + client = slackapp.New(c) default: log.Fatalf("Unknown connection type: %s", c.Get("type", "UNSET")) } diff --git a/plugins/twitch/twitch.go b/plugins/twitch/twitch.go index b77a2be..c748cad 100644 --- a/plugins/twitch/twitch.go +++ b/plugins/twitch/twitch.go @@ -135,6 +135,10 @@ func (p *TwitchPlugin) help(kind bot.Kind, message msg.Message, args ...interfac func (p *TwitchPlugin) twitchLoop(channel string) { frequency := p.config.GetInt("Twitch.Freq", 60) + if p.config.Get("twitch.clientid", "") == "" || p.config.Get("twitch.authorization", "") == "" { + log.Println("Disabling twitch autochecking.") + return + } log.Println("Checking every ", frequency, " seconds")