From dc1239783e2020e6149a43706f3b85fdbda16255 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Thu, 10 Mar 2016 21:11:52 -0500 Subject: [PATCH] Initial slack worky worky --- bot/bot.go | 34 +++++++- bot/handlers.go | 10 +-- bot/interfaces.go | 4 +- config/config.go | 3 + irc/irc.go | 76 +++++----------- main.go | 5 +- plugins/admin.go | 4 +- slack/slack.go | 215 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 286 insertions(+), 65 deletions(-) diff --git a/bot/bot.go b/bot/bot.go index 01905e4..6e2a974 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -136,8 +136,8 @@ func NewBot(config *config.Config, connector Connector) *Bot { } go http.ListenAndServe(config.HttpAddr, nil) - connector.RegisterMessageRecieved(bot.MsgRecieved) - connector.RegisterEventRecieved(bot.EventRecieved) + connector.RegisterMessageReceived(bot.MsgReceived) + connector.RegisterEventReceived(bot.EventReceived) return bot } @@ -243,3 +243,33 @@ func (b *Bot) serveRoot(w http.ResponseWriter, r *http.Request) { } t.Execute(w, context) } + +// Checks if message is a command and returns its curtailed version +func IsCmd(c *config.Config, message string) (bool, string) { + cmdc := c.CommandChar + botnick := strings.ToLower(c.Nick) + iscmd := false + lowerMessage := strings.ToLower(message) + + if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 { + iscmd = true + message = message[len(cmdc):] + // } else if match, _ := regexp.MatchString(rex, lowerMessage); match { + } else if strings.HasPrefix(lowerMessage, botnick) && + len(lowerMessage) > len(botnick) && + (lowerMessage[len(botnick)] == ',' || lowerMessage[len(botnick)] == ':') { + + iscmd = true + message = message[len(botnick):] + + // trim off the customary addressing punctuation + if message[0] == ':' || message[0] == ',' { + message = message[1:] + } + } + + // trim off any whitespace left on the message + message = strings.TrimSpace(message) + + return iscmd, message +} diff --git a/bot/handlers.go b/bot/handlers.go index bf0165f..b1322b8 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -15,8 +15,8 @@ import ( ) // Handles incomming PRIVMSG requests -func (b *Bot) MsgRecieved(msg Message) { - log.Println("Recieved message: ", msg) +func (b *Bot) MsgReceived(msg Message) { + log.Println("Received message: ", msg) // msg := b.buildMessage(client, inMsg) // do need to look up user and fix it @@ -40,8 +40,8 @@ RET: } // Handle incoming events -func (b *Bot) EventRecieved(msg Message) { - log.Println("Recieved event: ", msg) +func (b *Bot) EventReceived(msg Message) { + log.Println("Received event: ", msg) //msg := b.buildMessage(conn, inMsg) for _, name := range b.PluginOrdering { p := b.Plugins[name] @@ -157,7 +157,7 @@ func (b *Bot) Filter(message Message, input string) string { func (b *Bot) getVar(varName string) (string, error) { var text string - err := b.DB.QueryRow("select v.value from variables as va inner join values as v on va.id = va.id = v.varId order by random() limit 1").Scan(&text) + err := b.DB.QueryRow(`select v.value from variables as va inner join "values" as v on va.id = va.id = v.varId order by random() limit 1`).Scan(&text) switch { case err == sql.ErrNoRows: return "", fmt.Errorf("No factoid found") diff --git a/bot/interfaces.go b/bot/interfaces.go index b38fe16..bd79bec 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -1,8 +1,8 @@ package bot type Connector interface { - RegisterEventRecieved(func(message Message)) - RegisterMessageRecieved(func(message Message)) + RegisterEventReceived(func(message Message)) + RegisterMessageReceived(func(message Message)) SendMessage(channel, message string) SendAction(channel, message string) diff --git a/config/config.go b/config/config.go index a946823..1dc9169 100644 --- a/config/config.go +++ b/config/config.go @@ -21,6 +21,9 @@ type Config struct { Irc struct { Server, Pass string } + Slack struct { + Token string + } Nick string FullName string Version string diff --git a/irc/irc.go b/irc/irc.go index 63614e4..5267c45 100644 --- a/irc/irc.go +++ b/irc/irc.go @@ -37,8 +37,8 @@ type Irc struct { config *config.Config quit chan bool - eventRecieved func(bot.Message) - messageRecieved func(bot.Message) + eventReceived func(bot.Message) + messageReceived func(bot.Message) } func New(c *config.Config) *Irc { @@ -48,12 +48,12 @@ func New(c *config.Config) *Irc { return &i } -func (i *Irc) RegisterEventRecieved(f func(bot.Message)) { - i.eventRecieved = f +func (i *Irc) RegisterEventReceived(f func(bot.Message)) { + i.eventReceived = f } -func (i *Irc) RegisterMessageRecieved(f func(bot.Message)) { - i.messageRecieved = f +func (i *Irc) RegisterMessageReceived(f func(bot.Message)) { + i.messageReceived = f } func (i *Irc) JoinChannel(channel string) { @@ -95,7 +95,7 @@ func (i *Irc) SendAction(channel, message string) { } func (i *Irc) Serve() { - if i.eventRecieved == nil || i.messageRecieved == nil { + if i.eventReceived == nil || i.messageReceived == nil { log.Fatal("Missing an event handler") } @@ -173,52 +173,52 @@ func (i *Irc) handleMsg(msg irc.Msg) { // OK, ignore case irc.ERR_NOSUCHNICK: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.ERR_NOSUCHCHANNEL: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.RPL_MOTD: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.RPL_NAMREPLY: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.RPL_TOPIC: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.KICK: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.TOPIC: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.MODE: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.JOIN: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.PART: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.QUIT: os.Exit(1) case irc.NOTICE: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.PRIVMSG: - i.messageRecieved(botMsg) + i.messageReceived(botMsg) case irc.NICK: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.RPL_WHOREPLY: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) case irc.RPL_ENDOFWHO: - i.eventRecieved(botMsg) + i.eventReceived(botMsg) default: cmd := irc.CmdNames[msg.Cmd] @@ -254,7 +254,7 @@ func (i *Irc) buildMessage(inMsg irc.Msg) bot.Message { iscmd := false filteredMessage := message if !isAction { - iscmd, filteredMessage = i.isCmd(message) + iscmd, filteredMessage = bot.IsCmd(i.config, message) } msg := bot.Message{ @@ -270,33 +270,3 @@ func (i *Irc) buildMessage(inMsg irc.Msg) bot.Message { return msg } - -// Checks if message is a command and returns its curtailed version -func (i *Irc) isCmd(message string) (bool, string) { - cmdc := i.config.CommandChar - botnick := strings.ToLower(i.config.Nick) - iscmd := false - lowerMessage := strings.ToLower(message) - - if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 { - iscmd = true - message = message[len(cmdc):] - // } else if match, _ := regexp.MatchString(rex, lowerMessage); match { - } else if strings.HasPrefix(lowerMessage, botnick) && - len(lowerMessage) > len(botnick) && - (lowerMessage[len(botnick)] == ',' || lowerMessage[len(botnick)] == ':') { - - iscmd = true - message = message[len(botnick):] - - // trim off the customary addressing punctuation - if message[0] == ':' || message[0] == ',' { - message = message[1:] - } - } - - // trim off any whitespace left on the message - message = strings.TrimSpace(message) - - return iscmd, message -} diff --git a/main.go b/main.go index 032b508..8f9e96d 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/velour/catbase/config" "github.com/velour/catbase/irc" "github.com/velour/catbase/plugins" + "github.com/velour/catbase/slack" ) func main() { @@ -23,6 +24,8 @@ func main() { switch c.Type { case "irc": client = irc.New(c) + case "slack": + client = slack.New(c) default: log.Fatalf("Unknown connection type: %s", c.Type) } @@ -31,7 +34,7 @@ func main() { // b.AddHandler(plugins.NewTestPlugin(b)) b.AddHandler("admin", plugins.NewAdminPlugin(b)) - b.AddHandler("first", plugins.NewFirstPlugin(b)) + // b.AddHandler("first", plugins.NewFirstPlugin(b)) b.AddHandler("downtime", plugins.NewDowntimePlugin(b)) b.AddHandler("talker", plugins.NewTalkerPlugin(b)) b.AddHandler("dice", plugins.NewDicePlugin(b)) diff --git a/plugins/admin.go b/plugins/admin.go index ca24b47..9472949 100644 --- a/plugins/admin.go +++ b/plugins/admin.go @@ -64,10 +64,10 @@ func (p *AdminPlugin) handleVariables(message bot.Message) bool { var count int64 var varId int64 - err := p.DB.QueryRow(`select count(*), varId from variables vs inner join values v on vs.id = v.varId where vs.name = ? and v.value = ?`, variable, value).Scan(&count) + err := p.DB.QueryRow(`select count(*), varId from variables vs inner join "values" v on vs.id = v.varId where vs.name = ? and v.value = ?`, variable, value).Scan(&count) switch { case err == sql.ErrNoRows: - _, err := p.DB.Exec(`insert into values (varId, value) values (?, ?)`, varId, value) + _, err := p.DB.Exec(`insert into "values" (varId, value) values (?, ?)`, varId, value) if err != nil { log.Println(err) } diff --git a/slack/slack.go b/slack/slack.go index 9766782..cf6b6c8 100644 --- a/slack/slack.go +++ b/slack/slack.go @@ -1,2 +1,217 @@ // Package to connect to slack service package slack + +import ( + "encoding/json" + "fmt" + "html" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "sync/atomic" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/config" + "golang.org/x/net/websocket" +) + +type Slack struct { + config *config.Config + + url string + id string + ws *websocket.Conn + + users map[string]string + + eventReceived func(bot.Message) + messageReceived func(bot.Message) +} + +var idCounter uint64 + +type slackUserInfoResp struct { + Ok bool `json:"ok"` + User struct { + Id string `json:"id"` + Name string `json:"name"` + } `json:"user"` +} + +type slackMessage struct { + Id uint64 `json:"id"` + Type string `json:"type"` + SubType string `json:"subtype"` + Channel string `json:"channel"` + Text string `json:"text"` + User string `json:"user"` + Error struct { + Code uint64 `json:"code"` + Msg string `json:"msg"` + } `json:"error"` +} + +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 { + return &Slack{ + config: c, + users: make(map[string]string), + } +} + +func (s *Slack) RegisterEventReceived(f func(bot.Message)) { + s.eventReceived = f +} + +func (s *Slack) RegisterMessageReceived(f func(bot.Message)) { + s.messageReceived = f +} + +func (s *Slack) SendMessageType(channel, messageType, subType, message string) error { + m := slackMessage{ + Id: atomic.AddUint64(&idCounter, 1), + Type: messageType, + SubType: subType, + Channel: channel, + Text: message, + } + err := websocket.JSON.Send(s.ws, m) + if err != nil { + log.Printf("Error sending Slack message: %s", err) + } + return err +} + +func (s *Slack) SendMessage(channel, message string) { + s.SendMessageType(channel, "message", "", message) +} + +func (s *Slack) SendAction(channel, message string) { + s.SendMessageType(channel, "message", "me_message", "_"+message+"_") +} + +func (s *Slack) receiveMessage() (slackMessage, error) { + m := slackMessage{} + err := websocket.JSON.Receive(s.ws, &m) + return m, err +} + +func (s *Slack) Serve() { + s.connect() + for { + msg, err := s.receiveMessage() + if err != nil { + log.Printf("Slack API error: ", err) + } + switch msg.Type { + case "message": + s.messageReceived(s.buildMessage(msg)) + 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": + // squeltch this stuff + default: + log.Printf("Unhandled Slack message type: '%s'", msg.Type) + } + } +} + +// Convert a slackMessage to a bot.Message +func (s *Slack) buildMessage(msg slackMessage) bot.Message { + log.Println("DEBUG: msg: %%#v", msg) + text := html.UnescapeString(msg.Text) + + isCmd, text := bot.IsCmd(s.config, text) + + isAction := strings.HasPrefix(text, "/me ") + if isAction { + text = text[3:] + } + + user := s.getUser(msg.User) + + return bot.Message{ + User: &bot.User{ + Name: user, + }, + Body: text, + Raw: msg.Text, + Channel: msg.Channel, + Command: isCmd, + Action: isAction, + Host: string(msg.Id), + } +} + +func (s *Slack) connect() { + token := s.config.Slack.Token + url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token) + resp, err := http.Get(url) + 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 + + s.ws, err = websocket.Dial(rtm.Url, "", s.url) + if err != nil { + log.Fatal(err) + } +} + +// Get username for Slack user ID +func (s *Slack) getUser(id string) string { + if name, ok := s.users[id]; ok { + return name + } + + log.Printf("User %s not already found, requesting info", id) + u := s.url + "users.info" + resp, err := http.PostForm(u, + url.Values{"token": {s.config.Slack.Token}, "user": {id}}) + if err != nil || resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d %s", + resp.StatusCode, err) + return "UNKNOWN" + } + var userInfo slackUserInfoResp + err = json.NewDecoder(resp.Body).Decode(&userInfo) + if err != nil { + log.Println("Error decoding response: ", err) + return "UNKNOWN" + } + s.users[id] = userInfo.User.Name + return s.users[id] +}