diff --git a/.gitignore b/.gitignore index 1287944..36bb5e6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,40 @@ vendor *.code-workspace *config.lua modd.conf + + +# Created by https://www.gitignore.io/api/macos +# Edit at https://www.gitignore.io/?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.gitignore.io/api/macos + +util/*/files +util/*/files diff --git a/bot/bot.go b/bot/bot.go index c5b237d..c12d1bb 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -116,9 +116,6 @@ func (b *bot) AddPlugin(h Plugin) { name := reflect.TypeOf(h).String() b.plugins[name] = h b.pluginOrdering = append(b.pluginOrdering, name) - if entry := h.RegisterWeb(); entry != nil { - b.httpEndPoints[name] = *entry - } } func (b *bot) Who(channel string) []user.User { @@ -260,3 +257,7 @@ func (b *bot) Register(p Plugin, kind Kind, cb Callback) { } b.callbacks[t][kind] = append(b.callbacks[t][kind], cb) } + +func (b *bot) RegisterWeb(root, name string) { + b.httpEndPoints[name] = root +} 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..53d5aa3 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) @@ -59,11 +59,12 @@ type Bot interface { CheckAdmin(string) bool GetEmojiList() map[string]string RegisterFilter(string, func(string) string) + RegisterWeb(string, string) } // 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) @@ -74,7 +75,6 @@ type Connector interface { } // Plugin interface used for compatibility with the Plugin interface -// Probably can disappear once RegisterWeb gets inverted +// Uhh it turned empty, but we're still using it to ID plugins type Plugin interface { - RegisterWeb() *string } diff --git a/bot/mock.go b/bot/mock.go index ea8986e..f9f2917 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -5,6 +5,7 @@ package bot import ( "fmt" "log" + "net/http" "strconv" "strings" @@ -46,12 +47,13 @@ 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) RegisterWeb(_, _ string) {} +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) @@ -99,5 +101,7 @@ func NewMockBot() *MockBot { Messages: make([]string, 0), Actions: make([]string, 0), } + // If any plugin registered a route, we need to reset those before any new test + http.DefaultServeMux = new(http.ServeMux) return &b } 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..8a23a53 --- /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, error), 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, error), 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, err := findUser(string(tag[1:])); err == nil { + 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..2d309f7 --- /dev/null +++ b/connectors/slackapp/slackApp.go @@ -0,0 +1,333 @@ +package slackapp + +import ( + "bytes" + "encoding/json" + "fmt" + "html" + "log" + "net/http" + "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 + + botToken string + userToken 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, + botToken: token, + userToken: c.Get("slack.usertoken", "NONE"), + verification: c.Get("slack.verification", "NONE"), + myBotID: c.Get("slack.botid", ""), + 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.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.buildMessage(msg), msg.ThreadTimeStamp) + } else { + log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.Text) + } +} + +func (s *SlackApp) 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 (s *SlackApp) sendMessageType(channel, message string, meMessage bool) (string, error) { + ts, err := "", fmt.Errorf("") + nick := s.config.Get("Nick", "bot") + + if meMessage { + _, ts, err = s.api.PostMessage(channel, + slack.MsgOptionUsername(nick), + slack.MsgOptionText(message, false), + slack.MsgOptionMeMessage()) + } else { + _, ts, err = s.api.PostMessage(channel, + slack.MsgOptionUsername(nick), + slack.MsgOptionText(message, false)) + } + + if err != nil { + log.Printf("Error sending message: %+v", err) + return "", err + } + + return ts, nil +} + +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") + _, ts, err := s.api.PostMessage(channel, + slack.MsgOptionUsername(nick), + slack.MsgOptionText(message, false), + slack.MsgOptionMeMessage(), + slack.MsgOptionTS(identifier)) + return ts, 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) + ref := slack.ItemRef{ + Channel: channel, + Timestamp: message.AdditionalData["RAW_SLACK_TIMESTAMP"], + } + err := s.api.AddReaction(reaction, ref) + return "", err +} + +func (s *SlackApp) edit(channel, newMessage, identifier string) (string, error) { + log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage) + nick := s.config.Get("Nick", "bot") + _, ts, err := s.api.PostMessage(channel, + slack.MsgOptionUsername(nick), + slack.MsgOptionText(newMessage, false), + slack.MsgOptionMeMessage(), + slack.MsgOptionUpdate(identifier)) + return ts, err +} + +func (s *SlackApp) GetEmojiList() map[string]string { + return s.emoji +} + +func (s *SlackApp) populateEmojiList() { + if s.userToken == "NONE" { + log.Println("Cannot get emoji list without slack.usertoken") + return + } + dbg := slack.OptionDebug(true) + api := slack.New(s.userToken) + dbg(api) + + em, err := api.GetEmoji() + if err != nil { + log.Printf("Error retrieving emoji list from Slack: %s", err) + return + } + + s.emoji = em +} + +// 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) +} + +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, + }, + } +} + +// Get username for Slack user ID +func (s *SlackApp) getUser(id string) (string, error) { + if name, ok := s.users[id]; ok { + return name, nil + } + + log.Printf("User %s not already found, requesting info", id) + u, err := s.api.GetUserInfo(id) + if err != nil { + return "UNKNOWN", err + } + s.users[id] = u.Name + return s.users[id], nil +} + +// Who gets usernames out of a channel +func (s *SlackApp) Who(id string) []string { + if s.userToken == "NONE" { + log.Println("Cannot get emoji list without slack.usertoken") + return []string{s.config.Get("nick", "bot")} + } + dbg := slack.OptionDebug(true) + api := slack.New(s.userToken) + dbg(api) + + log.Println("Who is queried for ", id) + // Not super sure this is the correct call + params := &slack.GetUsersInConversationParameters{ + ChannelID: id, + Limit: 50, + } + members, _, err := api.GetUsersInConversation(params) + if err != nil { + log.Println(err) + return []string{s.config.Get("nick", "bot")} + } + + ret := []string{} + for _, m := range members { + u, err := s.getUser(m) + if err != nil { + log.Printf("Couldn't get user %s: %s", m, err) + continue + } + ret = append(ret, u) + } + return ret +} 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/admin/admin.go b/plugins/admin/admin.go index 5de34f0..98b439f 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -149,8 +149,3 @@ func (p *AdminPlugin) help(kind bot.Kind, m msg.Message, args ...interface{}) bo p.Bot.Send(bot.Message, m.Channel, "This does super secret things that you're not allowed to know about.") return true } - -// Register any web URLs desired -func (p *AdminPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/babbler/babbler.go b/plugins/babbler/babbler.go index efc0c63..f95782f 100644 --- a/plugins/babbler/babbler.go +++ b/plugins/babbler/babbler.go @@ -162,10 +162,6 @@ func (p *BabblerPlugin) help(kind bot.Kind, msg msg.Message, args ...interface{} return true } -func (p *BabblerPlugin) RegisterWeb() *string { - return nil -} - func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) { res, err := p.db.Exec(`insert into babblers (babbler) values (?);`, name) if err == nil { diff --git a/plugins/babbler/babbler_test.go b/plugins/babbler/babbler_test.go index 1631257..a569e0d 100644 --- a/plugins/babbler/babbler_test.go +++ b/plugins/babbler/babbler_test.go @@ -312,10 +312,3 @@ func TestHelp(t *testing.T) { bp.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - mb := bot.NewMockBot() - bp := newBabblerPlugin(mb) - assert.NotNil(t, bp) - assert.Nil(t, bp.RegisterWeb()) -} diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go index 7736936..3ad3dee 100644 --- a/plugins/beers/beers.go +++ b/plugins/beers/beers.go @@ -358,7 +358,6 @@ func (p *BeersPlugin) checkUntappd(channel string) { log.Fatal(err) } userMap[u.untappdUser] = u - log.Printf("Found untappd user: %#v", u) if u.chanNick == "" { log.Fatal("Empty chanNick for no good reason.") } @@ -373,7 +372,6 @@ func (p *BeersPlugin) checkUntappd(channel string) { checkin := chks[i-1] if checkin.Checkin_id <= userMap[checkin.User.User_name].lastCheckin { - log.Printf("User %s already check in >%d", checkin.User.User_name, checkin.Checkin_id) continue } @@ -436,8 +434,3 @@ func (p *BeersPlugin) untappdLoop(channel string) { p.checkUntappd(channel) } } - -// Register any web URLs desired -func (p BeersPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/beers/beers_test.go b/plugins/beers/beers_test.go index bb34008..4fa07b1 100644 --- a/plugins/beers/beers_test.go +++ b/plugins/beers/beers_test.go @@ -124,8 +124,3 @@ func TestHelp(t *testing.T) { b.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - b, _ := makeBeersPlugin(t) - assert.Nil(t, b.RegisterWeb()) -} diff --git a/plugins/couldashouldawoulda/csw.go b/plugins/couldashouldawoulda/csw.go index 9d2d7a3..0052a5e 100644 --- a/plugins/couldashouldawoulda/csw.go +++ b/plugins/couldashouldawoulda/csw.go @@ -71,7 +71,3 @@ func (p *CSWPlugin) message(kind bot.Kind, message msg.Message, args ...interfac return false } - -func (p *CSWPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 6d0e0ed..b23c4ae 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -455,11 +455,6 @@ func (p *CounterPlugin) help(kind bot.Kind, message msg.Message, args ...interfa return true } -// Register any web URLs desired -func (p *CounterPlugin) RegisterWeb() *string { - return nil -} - func (p *CounterPlugin) checkMatch(message msg.Message) bool { nick := message.User.Name channel := message.Channel diff --git a/plugins/counter/counter_test.go b/plugins/counter/counter_test.go index 1cbec0c..417c44c 100644 --- a/plugins/counter/counter_test.go +++ b/plugins/counter/counter_test.go @@ -253,9 +253,3 @@ func TestHelp(t *testing.T) { c.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - _, c := setup(t) - assert.NotNil(t, c) - assert.Nil(t, c.RegisterWeb()) -} diff --git a/plugins/db/db.go b/plugins/db/db.go index 4bf8387..026bf76 100644 --- a/plugins/db/db.go +++ b/plugins/db/db.go @@ -18,7 +18,9 @@ type DBPlugin struct { } func New(b bot.Bot) *DBPlugin { - return &DBPlugin{b, b.Config()} + p := &DBPlugin{b, b.Config()} + p.registerWeb() + return p } func (p *DBPlugin) Message(message msg.Message) bool { return false } @@ -27,10 +29,8 @@ func (p *DBPlugin) ReplyMessage(msg.Message, string) bool { return false } func (p *DBPlugin) BotMessage(message msg.Message) bool { return false } func (p *DBPlugin) Help(channel string, parts []string) {} -func (p *DBPlugin) RegisterWeb() *string { +func (p *DBPlugin) registerWeb() { http.HandleFunc("/db/catbase.db", p.serveQuery) - tmp := "/db/catbase.db" - return &tmp } func (p *DBPlugin) serveQuery(w http.ResponseWriter, r *http.Request) { diff --git a/plugins/dice/dice.go b/plugins/dice/dice.go index 721a652..dafc715 100644 --- a/plugins/dice/dice.go +++ b/plugins/dice/dice.go @@ -74,8 +74,3 @@ func (p *DicePlugin) help(kind bot.Kind, message msg.Message, args ...interface{ p.Bot.Send(bot.Message, message.Channel, "Roll dice using notation XdY. Try \"3d20\".") return true } - -// Register any web URLs desired -func (p *DicePlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/dice/dice_test.go b/plugins/dice/dice_test.go index bcb7a26..b7f2961 100644 --- a/plugins/dice/dice_test.go +++ b/plugins/dice/dice_test.go @@ -89,10 +89,3 @@ func TestHelp(t *testing.T) { c.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - mb := bot.NewMockBot() - c := New(mb) - assert.NotNil(t, c) - assert.Nil(t, c.RegisterWeb()) -} diff --git a/plugins/downtime/downtime.go b/plugins/downtime/downtime.go deleted file mode 100644 index 335886e..0000000 --- a/plugins/downtime/downtime.go +++ /dev/null @@ -1,233 +0,0 @@ -// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. - -package downtime - -import ( - "database/sql" - - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot" - "github.com/velour/catbase/bot/msg" -) - -import ( - "fmt" - "log" - "sort" - "strings" - "time" -) - -// This is a downtime plugin to monitor how much our users suck - -type DowntimePlugin struct { - Bot bot.Bot - db *sqlx.DB -} - -type idleEntry struct { - id sql.NullInt64 - nick string - lastSeen time.Time -} - -func (entry idleEntry) saveIdleEntry(db *sqlx.DB) error { - var err error - if entry.id.Valid { - log.Println("Updating downtime for: ", entry) - _, err = db.Exec(`update downtime set - nick=?, lastSeen=? - where id=?;`, entry.nick, entry.lastSeen.Unix(), entry.id.Int64) - } else { - log.Println("Inserting downtime for: ", entry) - _, err = db.Exec(`insert into downtime (nick, lastSeen) - values (?, ?)`, entry.nick, entry.lastSeen.Unix()) - } - return err -} - -func getIdleEntryByNick(db *sqlx.DB, nick string) (idleEntry, error) { - var id sql.NullInt64 - var lastSeen sql.NullInt64 - err := db.QueryRow(`select id, max(lastSeen) from downtime - where nick = ?`, nick).Scan(&id, &lastSeen) - if err != nil { - log.Println("Error selecting downtime: ", err) - return idleEntry{}, err - } - if !id.Valid { - return idleEntry{ - nick: nick, - lastSeen: time.Now(), - }, nil - } - return idleEntry{ - id: id, - nick: nick, - lastSeen: time.Unix(lastSeen.Int64, 0), - }, nil -} - -func getAllIdleEntries(db *sqlx.DB) (idleEntries, error) { - rows, err := db.Query(`select id, nick, max(lastSeen) from downtime - group by nick`) - if err != nil { - return nil, err - } - entries := idleEntries{} - for rows.Next() { - var e idleEntry - err := rows.Scan(&e.id, &e.nick, &e.lastSeen) - if err != nil { - return nil, err - } - entries = append(entries, &e) - } - return entries, nil -} - -type idleEntries []*idleEntry - -func (ie idleEntries) Len() int { - return len(ie) -} - -func (ie idleEntries) Less(i, j int) bool { - return ie[i].lastSeen.Before(ie[j].lastSeen) -} - -func (ie idleEntries) Swap(i, j int) { - ie[i], ie[j] = ie[j], ie[i] -} - -// NewDowntimePlugin creates a new DowntimePlugin with the Plugin interface -func New(bot bot.Bot) *DowntimePlugin { - p := DowntimePlugin{ - Bot: bot, - db: bot.DB(), - } - - _, err := p.db.Exec(`create table if not exists downtime ( - id integer primary key, - nick string, - lastSeen integer - );`) - if err != nil { - log.Fatal("Error creating downtime table: ", err) - } - - return &p -} - -// Message responds to the bot hook on recieving messages. -// This function returns true if the plugin responds in a meaningful way to the users message. -// Otherwise, the function returns false and the bot continues execution of other plugins. -func (p *DowntimePlugin) Message(message msg.Message) bool { - // If it's a command and the payload is idle , give it. Log everything. - - parts := strings.Fields(strings.ToLower(message.Body)) - channel := message.Channel - ret := false - - if len(parts) == 0 { - return false - } - - if parts[0] == "idle" && len(parts) == 2 { - nick := parts[1] - // parts[1] must be the userid, or we don't know them - entry, err := getIdleEntryByNick(p.db, nick) - if err != nil { - log.Println("Error getting idle entry: ", err) - } - if !entry.id.Valid { - // couldn't find em - p.Bot.Send(bot.Message, channel, fmt.Sprintf("Sorry, I don't know %s.", nick)) - } else { - p.Bot.Send(bot.Message, channel, fmt.Sprintf("%s has been idle for: %s", - nick, time.Now().Sub(entry.lastSeen))) - } - ret = true - } else if parts[0] == "idle" && len(parts) == 1 { - // Find all idle times, report them. - entries, err := getAllIdleEntries(p.db) - if err != nil { - log.Println("Error retrieving idle entries: ", err) - } - sort.Sort(entries) - tops := "The top entries are: " - for _, e := range entries { - - // filter out ZNC entries and ourself - if strings.HasPrefix(e.nick, "*") || strings.ToLower(p.Bot.Config().Get("Nick", "bot")) == e.nick { - p.remove(e.nick) - } else { - tops = fmt.Sprintf("%s%s: %s ", tops, e.nick, time.Now().Sub(e.lastSeen)) - } - } - p.Bot.Send(bot.Message, channel, tops) - ret = true - - } - - p.record(strings.ToLower(message.User.Name)) - - return ret -} - -func (p *DowntimePlugin) record(user string) { - entry, err := getIdleEntryByNick(p.db, user) - if err != nil { - log.Println("Error recording downtime: ", err) - } - entry.lastSeen = time.Now() - entry.saveIdleEntry(p.db) - log.Println("Inserted downtime for:", user) -} - -func (p *DowntimePlugin) remove(user string) error { - _, err := p.db.Exec(`delete from downtime where nick = ?`, user) - if err != nil { - log.Println("Error removing downtime for user: ", user, err) - return err - } - log.Println("Removed downtime for:", user) - return nil -} - -// Help responds to help requests. Every plugin must implement a help function. -func (p *DowntimePlugin) Help(channel string, parts []string) { - p.Bot.Send(bot.Message, channel, "Ask me how long one of your friends has been idele with, \"idle \"") -} - -// Empty event handler because this plugin does not do anything on event recv -func (p *DowntimePlugin) Event(kind string, message msg.Message) bool { - log.Println(kind, "\t", message) - if kind != "PART" && message.User.Name != p.Bot.Config().Get("Nick", "bot") { - // user joined, let's nail them for it - if kind == "NICK" { - p.record(strings.ToLower(message.Channel)) - p.remove(strings.ToLower(message.User.Name)) - } else { - p.record(strings.ToLower(message.User.Name)) - } - } else if kind == "PART" || kind == "QUIT" { - p.remove(strings.ToLower(message.User.Name)) - } else { - log.Println("Unknown event: ", kind, message.User, message) - p.record(strings.ToLower(message.User.Name)) - } - return false -} - -// Handler for bot's own messages -func (p *DowntimePlugin) BotMessage(message msg.Message) bool { - return false -} - -// Register any web URLs desired -func (p *DowntimePlugin) RegisterWeb() *string { - return nil -} - -func (p *DowntimePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/emojifyme/emojifyme.go b/plugins/emojifyme/emojifyme.go index 03b7fbb..3419e4c 100644 --- a/plugins/emojifyme/emojifyme.go +++ b/plugins/emojifyme/emojifyme.go @@ -99,10 +99,6 @@ func (p *EmojifyMePlugin) message(kind bot.Kind, message msg.Message, args ...in return false } -func (p *EmojifyMePlugin) RegisterWeb() *string { - return nil -} - func stringsContain(haystack []string, needle string) bool { for _, s := range haystack { if s == needle { diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go index b8b40c2..d7479a9 100644 --- a/plugins/fact/factoid.go +++ b/plugins/fact/factoid.go @@ -317,6 +317,8 @@ func New(botInst bot.Bot) *Factoid { botInst.Register(p, bot.Message, p.message) botInst.Register(p, bot.Help, p.help) + p.registerWeb() + return p } @@ -737,11 +739,10 @@ func (p *Factoid) factTimer(channel string) { } // Register any web URLs desired -func (p *Factoid) RegisterWeb() *string { +func (p *Factoid) registerWeb() { http.HandleFunc("/factoid/req", p.serveQuery) http.HandleFunc("/factoid", p.serveQuery) - tmp := "/factoid" - return &tmp + p.Bot.RegisterWeb("/factoid", "Factoid") } func linkify(text string) template.HTML { diff --git a/plugins/fact/remember.go b/plugins/fact/remember.go index 87485f1..8aa32da 100644 --- a/plugins/fact/remember.go +++ b/plugins/fact/remember.go @@ -153,11 +153,6 @@ func (p *RememberPlugin) randQuote() string { return f.Tidbit } -// Register any web URLs desired -func (p RememberPlugin) RegisterWeb() *string { - return nil -} - func (p *RememberPlugin) recordMsg(message msg.Message) { log.Printf("Logging message: %s: %s", message.User.Name, message.Body) p.Log[message.Channel] = append(p.Log[message.Channel], message) diff --git a/plugins/first/first.go b/plugins/first/first.go index 9771dbf..88fcb47 100644 --- a/plugins/first/first.go +++ b/plugins/first/first.go @@ -215,8 +215,3 @@ func (p *FirstPlugin) help(kind bot.Kind, message msg.Message, args ...interface p.Bot.Send(bot.Message, message.Channel, "Sorry, First does not do a goddamn thing.") return true } - -// Register any web URLs desired -func (p *FirstPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/inventory/inventory.go b/plugins/inventory/inventory.go index 50fb6f3..c13ac9e 100644 --- a/plugins/inventory/inventory.go +++ b/plugins/inventory/inventory.go @@ -224,8 +224,3 @@ func checkerr(e error) { log.Println(e) } } - -func (p *InventoryPlugin) RegisterWeb() *string { - // nothing to register - return nil -} diff --git a/plugins/leftpad/leftpad.go b/plugins/leftpad/leftpad.go index e005b46..23847f2 100644 --- a/plugins/leftpad/leftpad.go +++ b/plugins/leftpad/leftpad.go @@ -62,8 +62,3 @@ func (p *LeftpadPlugin) message(kind bot.Kind, message msg.Message, args ...inte return false } - -func (p *LeftpadPlugin) RegisterWeb() *string { - // nothing to register - return nil -} diff --git a/plugins/leftpad/leftpad_test.go b/plugins/leftpad/leftpad_test.go index 1d97488..32b9cdc 100644 --- a/plugins/leftpad/leftpad_test.go +++ b/plugins/leftpad/leftpad_test.go @@ -85,8 +85,3 @@ func TestNotPadding(t *testing.T) { p.message(makeMessage("!lololol")) assert.Len(t, mb.Messages, 0) } - -func TestRegisterWeb(t *testing.T) { - p, _ := makePlugin(t) - assert.Nil(t, p.RegisterWeb()) -} diff --git a/plugins/nerdepedia/nerdepedia.go b/plugins/nerdepedia/nerdepedia.go index eb040a7..6c0ba3c 100644 --- a/plugins/nerdepedia/nerdepedia.go +++ b/plugins/nerdepedia/nerdepedia.go @@ -94,8 +94,3 @@ func (p *NerdepediaPlugin) help(kind bot.Kind, message msg.Message, args ...inte p.bot.Send(bot.Message, message.Channel, "nerd stuff") return true } - -// Register any web URLs desired -func (p *NerdepediaPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/picker/picker.go b/plugins/picker/picker.go index 3a1c320..b7290d4 100644 --- a/plugins/picker/picker.go +++ b/plugins/picker/picker.go @@ -115,10 +115,3 @@ func (p *PickerPlugin) help(kind bot.Kind, message msg.Message, args ...interfac p.Bot.Send(bot.Message, message.Channel, "Choose from a list of options. Try \"pick {a,b,c}\".") return true } - -// Register any web URLs desired -func (p *PickerPlugin) RegisterWeb() *string { - return nil -} - -func (p *PickerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/plugins.go b/plugins/plugins.go index 364629f..b6a4fd5 100644 --- a/plugins/plugins.go +++ b/plugins/plugins.go @@ -1,15 +1,3 @@ // © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package plugins - -import "github.com/velour/catbase/bot/msg" - -// Plugin interface defines the methods needed to accept a plugin -type Plugin interface { - Message(message msg.Message) bool - Event(kind string, message msg.Message) bool - BotMessage(message msg.Message) bool - LoadData() - Help() - RegisterWeb() -} diff --git a/plugins/reaction/reaction.go b/plugins/reaction/reaction.go index c964022..3dcd078 100644 --- a/plugins/reaction/reaction.go +++ b/plugins/reaction/reaction.go @@ -63,7 +63,3 @@ func (p *ReactionPlugin) message(kind bot.Kind, message msg.Message, args ...int return false } - -func (p *ReactionPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go index 93d4dec..26078a8 100644 --- a/plugins/reminder/reminder.go +++ b/plugins/reminder/reminder.go @@ -200,10 +200,6 @@ func (p *ReminderPlugin) help(kind bot.Kind, message msg.Message, args ...interf return true } -func (p *ReminderPlugin) RegisterWeb() *string { - return nil -} - func (p *ReminderPlugin) getNextReminder() *Reminder { p.mutex.Lock() defer p.mutex.Unlock() diff --git a/plugins/reminder/reminder_test.go b/plugins/reminder/reminder_test.go index c4a4e9e..3618f16 100644 --- a/plugins/reminder/reminder_test.go +++ b/plugins/reminder/reminder_test.go @@ -226,9 +226,3 @@ func TestHelp(t *testing.T) { c.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - c, _ := setup(t) - assert.NotNil(t, c) - assert.Nil(t, c.RegisterWeb()) -} diff --git a/plugins/rpgORdie/rpgORdie.go b/plugins/rpgORdie/rpgORdie.go index 8b5a40e..1035a06 100644 --- a/plugins/rpgORdie/rpgORdie.go +++ b/plugins/rpgORdie/rpgORdie.go @@ -124,10 +124,6 @@ func (p *RPGPlugin) help(kind bot.Kind, message msg.Message, args ...interface{} return true } -func (p *RPGPlugin) RegisterWeb() *string { - return nil -} - func (p *RPGPlugin) replyMessage(kind bot.Kind, message msg.Message, args ...interface{}) bool { identifier := args[0].(string) if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Get("Nick", "bot")) { diff --git a/plugins/rss/rss.go b/plugins/rss/rss.go index b25d749..811f37f 100644 --- a/plugins/rss/rss.go +++ b/plugins/rss/rss.go @@ -102,8 +102,3 @@ func (p *RSSPlugin) help(kind bot.Kind, message msg.Message, args ...interface{} p.Bot.Send(bot.Message, message.Channel, "try '!rss http://rss.cnn.com/rss/edition.rss'") return true } - -// Register any web URLs desired -func (p *RSSPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/sisyphus/sisyphus.go b/plugins/sisyphus/sisyphus.go index 5a0a8b8..31c3f02 100644 --- a/plugins/sisyphus/sisyphus.go +++ b/plugins/sisyphus/sisyphus.go @@ -187,10 +187,6 @@ func (p *SisyphusPlugin) help(kind bot.Kind, message msg.Message, args ...interf return true } -func (p *SisyphusPlugin) RegisterWeb() *string { - return nil -} - func (p *SisyphusPlugin) replyMessage(kind bot.Kind, message msg.Message, args ...interface{}) bool { identifier := args[0].(string) if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Get("Nick", "bot")) { diff --git a/plugins/talker/talker.go b/plugins/talker/talker.go index 46ba61a..788161c 100644 --- a/plugins/talker/talker.go +++ b/plugins/talker/talker.go @@ -87,8 +87,3 @@ func (p *TalkerPlugin) help(kind bot.Kind, message msg.Message, args ...interfac p.Bot.Send(bot.Message, message.Channel, "Hi, this is talker. I like to talk about FredFelps!") return true } - -// Register any web URLs desired -func (p *TalkerPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/talker/talker_test.go b/plugins/talker/talker_test.go index 2408f45..fe65779 100644 --- a/plugins/talker/talker_test.go +++ b/plugins/talker/talker_test.go @@ -81,10 +81,3 @@ func TestHelp(t *testing.T) { c.help(bot.Help, msg.Message{Channel: "channel"}, []string{}) assert.Len(t, mb.Messages, 1) } - -func TestRegisterWeb(t *testing.T) { - mb := bot.NewMockBot() - c := New(mb) - assert.NotNil(t, c) - assert.Nil(t, c.RegisterWeb()) -} diff --git a/plugins/tell/tell.go b/plugins/tell/tell.go index c852141..aba8410 100644 --- a/plugins/tell/tell.go +++ b/plugins/tell/tell.go @@ -41,5 +41,3 @@ func (t *TellPlugin) message(kind bot.Kind, message msg.Message, args ...interfa } return false } - -func (t *TellPlugin) RegisterWeb() *string { return nil } diff --git a/plugins/twitch/twitch.go b/plugins/twitch/twitch.go index b77a2be..a088d40 100644 --- a/plugins/twitch/twitch.go +++ b/plugins/twitch/twitch.go @@ -71,13 +71,13 @@ func New(b bot.Bot) *TwitchPlugin { } b.Register(p, bot.Message, p.message) + p.registerWeb() + return p } -func (p *TwitchPlugin) RegisterWeb() *string { +func (p *TwitchPlugin) registerWeb() { http.HandleFunc("/isstreaming/", p.serveStreaming) - tmp := "/isstreaming" - return &tmp } func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) { @@ -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") diff --git a/plugins/your/your.go b/plugins/your/your.go index a319039..1d26e9d 100644 --- a/plugins/your/your.go +++ b/plugins/your/your.go @@ -57,8 +57,3 @@ func (p *YourPlugin) help(kind bot.Kind, message msg.Message, args ...interface{ p.bot.Send(bot.Message, message.Channel, "Your corrects people's grammar.") return true } - -// Register any web URLs desired -func (p *YourPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/zork/zork.go b/plugins/zork/zork.go index b5ac682..d2fda1b 100644 --- a/plugins/zork/zork.go +++ b/plugins/zork/zork.go @@ -120,5 +120,3 @@ func (p *ZorkPlugin) help(kind bot.Kind, message msg.Message, args ...interface{ p.bot.Send(bot.Message, message.Channel, "Play zork using 'zork '.") return true } - -func (p *ZorkPlugin) RegisterWeb() *string { return nil }