diff --git a/bot/bot.go b/bot/bot.go index 9c07bda..6e2a974 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -11,15 +11,10 @@ import ( "time" "github.com/velour/catbase/config" - "github.com/velour/velour/irc" _ "github.com/mattn/go-sqlite3" ) -const actionPrefix = "\x01ACTION" - -var throttle <-chan time.Time - // Bot type provides storage for bot-wide information, configs, and database connections type Bot struct { // Each plugin must be registered in our plugins handler. To come: a map so that this @@ -32,11 +27,10 @@ type Bot struct { // Represents the bot Me User - // Conn allows us to send messages and modify our connection state - Client *irc.Client - Config *config.Config + Conn Connector + // SQL DB // TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that // the select/update/etc statements could be simplified with struct @@ -103,8 +97,8 @@ type Variable struct { } // NewBot creates a Bot for a given connection and set of handlers. -func NewBot(config *config.Config, c *irc.Client) *Bot { - sqlDB, err := sql.Open("sqlite3", config.DbFile) +func NewBot(config *config.Config, connector Connector) *Bot { + sqlDB, err := sql.Open("sqlite3", config.DB.File) if err != nil { log.Fatal(err) } @@ -124,9 +118,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot { Config: config, Plugins: make(map[string]Handler), PluginOrdering: make([]string, 0), + Conn: connector, Users: users, Me: users[0], - Client: c, DB: sqlDB, logIn: logIn, logOut: logOut, @@ -142,6 +136,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot { } go http.ListenAndServe(config.HttpAddr, nil) + connector.RegisterMessageReceived(bot.MsgReceived) + connector.RegisterEventReceived(bot.EventReceived) + return bot } @@ -197,45 +194,6 @@ func (b *Bot) AddHandler(name string, h Handler) { } } -func (b *Bot) SendMessage(channel, message string) { - if !strings.HasPrefix(message, actionPrefix) { - b.selfSaid(channel, message, false) - } - 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 := b.Config.RatePerSec - throttle = time.Tick(time.Second / time.Duration(ratePerSec)) - } - - <-throttle - - b.Client.Out <- m - } -} - -// Sends action to channel -func (b *Bot) SendAction(channel, message string) { - // Notify plugins that we've said something - b.selfSaid(channel, message, true) - - message = actionPrefix + " " + message + "\x01" - - b.SendMessage(channel, message) -} - func (b *Bot) Who(channel string) []User { out := []User{} for _, u := range b.Users { @@ -285,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 36f557f..b1322b8 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -12,18 +12,14 @@ import ( "strconv" "strings" "time" - - "github.com/velour/velour/irc" ) // Handles incomming PRIVMSG requests -func (b *Bot) MsgRecieved(client *irc.Client, inMsg irc.Msg) { - log.Println("Recieved message: ", inMsg) - if inMsg.User == "" { - return - } +func (b *Bot) MsgReceived(msg Message) { + log.Println("Received message: ", msg) - msg := b.buildMessage(client, inMsg) + // msg := b.buildMessage(client, inMsg) + // do need to look up user and fix it if strings.HasPrefix(msg.Body, "help") && msg.Command { parts := strings.Fields(strings.ToLower(msg.Body)) @@ -44,27 +40,23 @@ RET: } // Handle incoming events -func (b *Bot) EventRecieved(conn *irc.Client, inMsg irc.Msg) { - log.Println("Recieved event: ", inMsg) - if inMsg.User == "" { - return - } - msg := b.buildMessage(conn, inMsg) +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] - if p.Event(inMsg.Cmd, msg) { + if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body break } } } -// Interface used for compatibility with the Plugin interface -type Handler interface { - Message(message Message) bool - Event(kind string, message Message) bool - BotMessage(message Message) bool - Help(channel string, parts []string) - RegisterWeb() *string +func (b *Bot) SendMessage(channel, message string) { + b.Conn.SendMessage(channel, message) +} + +func (b *Bot) SendAction(channel, message string) { + b.Conn.SendAction(channel, message) } // Checks to see if the user is asking for help, returns true if so and handles the situation. @@ -96,79 +88,6 @@ func (b *Bot) checkHelp(channel string, parts []string) { } } -// Checks if message is a command and returns its curtailed version -func (b *Bot) isCmd(message string) (bool, string) { - cmdc := b.Config.CommandChar - botnick := strings.ToLower(b.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 -} - -// Builds our internal message type out of a Conn & Line from irc -func (b *Bot) buildMessage(conn *irc.Client, inMsg irc.Msg) Message { - // Check for the user - user := b.GetUser(inMsg.Origin) - - channel := inMsg.Args[0] - if channel == b.Config.Nick { - 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 = b.isCmd(message) - } - - msg := Message{ - User: user, - Channel: channel, - Body: filteredMessage, - Raw: message, - Command: iscmd, - Action: isAction, - Time: time.Now(), - Host: inMsg.Host, - } - - return msg -} - func (b *Bot) LastMessage(channel string) (Message, error) { log := <-b.logOut if len(log) == 0 { @@ -238,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 new file mode 100644 index 0000000..bd79bec --- /dev/null +++ b/bot/interfaces.go @@ -0,0 +1,19 @@ +package bot + +type Connector interface { + RegisterEventReceived(func(message Message)) + RegisterMessageReceived(func(message Message)) + + SendMessage(channel, message string) + SendAction(channel, message string) + Serve() +} + +// Interface used for compatibility with the Plugin interface +type Handler interface { + Message(message Message) bool + Event(kind string, message Message) bool + BotMessage(message Message) bool + Help(channel string, parts []string) + RegisterWeb() *string +} diff --git a/config/config.go b/config/config.go index 18b0d62..1dc9169 100644 --- a/config/config.go +++ b/config/config.go @@ -9,13 +9,22 @@ import "io/ioutil" // Config stores any system-wide startup information that cannot be easily configured via // the database type Config struct { - DbFile string - DbName string - DbServer string - Channels []string - MainChannel string - Plugins []string - Nick, Server, Pass string + DB struct { + File string + Name string + Server string + } + Channels []string + MainChannel string + Plugins []string + Type string + Irc struct { + Server, Pass string + } + Slack struct { + Token string + } + Nick string FullName string Version string CommandChar string @@ -58,6 +67,10 @@ func Readconfig(version, cfile string) *Config { } c.Version = version + if c.Type == "" { + c.Type = "irc" + } + fmt.Printf("godeepintir version %s running.\n", c.Version) return &c diff --git a/example_config.json b/example_config.json index fdd1c3d..663fbe1 100644 --- a/example_config.json +++ b/example_config.json @@ -1,12 +1,20 @@ { - "DFile": "catbase.db", - "Dbserver": "127.0.0.1", + "DB": { + "File": "catbase.db", + "Server": "127.0.0.1" + }, "Channels": ["#CatBaseTest"], "MainChannel": "#CatBaseTest", "Plugins": [], - "Server": "127.0.0.1:6666", + "Type": "slack", + "Irc": { + "Server": "ircserver:6697", + "Pass": "CatBaseTest:test" + }, + "Slack": { + "Token": "" + }, "Nick": "CatBaseTest", - "Pass": "CatBaseTest:test", "FullName": "CatBase", "CommandChar": "!", "RatePerSec": 10.0, diff --git a/irc/irc.go b/irc/irc.go new file mode 100644 index 0000000..5267c45 --- /dev/null +++ b/irc/irc.go @@ -0,0 +1,272 @@ +package irc + +import ( + "io" + "log" + "os" + "strings" + "time" + + "github.com/velour/catbase/bot" + "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 + + eventReceived func(bot.Message) + messageReceived func(bot.Message) +} + +func New(c *config.Config) *Irc { + i := Irc{} + i.config = c + + return &i +} + +func (i *Irc) RegisterEventReceived(f func(bot.Message)) { + i.eventReceived = f +} + +func (i *Irc) RegisterMessageReceived(f func(bot.Message)) { + i.messageReceived = f +} + +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) { + 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.RatePerSec + throttle = time.Tick(time.Second / time.Duration(ratePerSec)) + } + + <-throttle + + i.Client.Out <- m + } +} + +// Sends action to channel +func (i *Irc) SendAction(channel, message string) { + message = actionPrefix + " " + message + "\x01" + + i.SendMessage(channel, message) +} + +func (i *Irc) Serve() { + if i.eventReceived == nil || i.messageReceived == nil { + log.Fatal("Missing an event handler") + } + + var err error + i.Client, err = irc.DialSSL( + i.config.Irc.Server, + i.config.Nick, + i.config.FullName, + i.config.Irc.Pass, + true, + ) + if err != nil { + log.Fatal(err) + } + + for _, c := range i.config.Channels { + i.JoinChannel(c) + } + + i.quit = make(chan bool) + go i.handleConnection() + <-i.quit +} + +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: + i.eventReceived(botMsg) + + case irc.ERR_NOSUCHCHANNEL: + i.eventReceived(botMsg) + + case irc.RPL_MOTD: + i.eventReceived(botMsg) + + case irc.RPL_NAMREPLY: + i.eventReceived(botMsg) + + case irc.RPL_TOPIC: + i.eventReceived(botMsg) + + case irc.KICK: + i.eventReceived(botMsg) + + case irc.TOPIC: + i.eventReceived(botMsg) + + case irc.MODE: + i.eventReceived(botMsg) + + case irc.JOIN: + i.eventReceived(botMsg) + + case irc.PART: + i.eventReceived(botMsg) + + case irc.QUIT: + os.Exit(1) + + case irc.NOTICE: + i.eventReceived(botMsg) + + case irc.PRIVMSG: + i.messageReceived(botMsg) + + case irc.NICK: + i.eventReceived(botMsg) + + case irc.RPL_WHOREPLY: + i.eventReceived(botMsg) + + case irc.RPL_ENDOFWHO: + i.eventReceived(botMsg) + + 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) bot.Message { + // Check for the user + user := bot.User{ + Name: inMsg.Origin, + } + + channel := inMsg.Args[0] + if channel == i.config.Nick { + 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 := bot.Message{ + User: &user, + Channel: channel, + Body: filteredMessage, + Raw: message, + Command: iscmd, + Action: isAction, + Time: time.Now(), + Host: inMsg.Host, + } + + return msg +} diff --git a/main.go b/main.go index 26e2653..8f9e96d 100644 --- a/main.go +++ b/main.go @@ -4,181 +4,47 @@ package main import ( "flag" - "io" "log" - "os" - "time" "github.com/velour/catbase/bot" "github.com/velour/catbase/config" + "github.com/velour/catbase/irc" "github.com/velour/catbase/plugins" - "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 -) - -var ( - Client *irc.Client - Bot *bot.Bot - Config *config.Config + "github.com/velour/catbase/slack" ) func main() { - var err error var cfile = flag.String("config", "config.json", "Config file to load. (Defaults to config.json)") flag.Parse() // parses the logging flags. - Config = config.Readconfig(Version, *cfile) - - Client, err = irc.DialSSL( - Config.Server, - Config.Nick, - Config.FullName, - Config.Pass, - true, - ) - - if err != nil { - log.Fatal(err) - } - - Bot = bot.NewBot(Config, Client) - // Bot.AddHandler(plugins.NewTestPlugin(Bot)) - Bot.AddHandler("admin", plugins.NewAdminPlugin(Bot)) - Bot.AddHandler("first", plugins.NewFirstPlugin(Bot)) - Bot.AddHandler("downtime", plugins.NewDowntimePlugin(Bot)) - Bot.AddHandler("talker", plugins.NewTalkerPlugin(Bot)) - Bot.AddHandler("dice", plugins.NewDicePlugin(Bot)) - Bot.AddHandler("beers", plugins.NewBeersPlugin(Bot)) - Bot.AddHandler("counter", plugins.NewCounterPlugin(Bot)) - Bot.AddHandler("remember", plugins.NewRememberPlugin(Bot)) - Bot.AddHandler("skeleton", plugins.NewSkeletonPlugin(Bot)) - Bot.AddHandler("your", plugins.NewYourPlugin(Bot)) - // catches anything left, will always return true - Bot.AddHandler("factoid", plugins.NewFactoidPlugin(Bot)) - - handleConnection() - - // And a signal on disconnect - quit := make(chan bool) - - // Wait for disconnect - <-quit -} - -func handleConnection() { - t := time.NewTimer(pingTime) - - defer func() { - t.Stop() - close(Client.Out) - for err := range Client.Errors { - if err != io.EOF { - log.Println(err) - } - } - }() - - for { - select { - case msg, ok := <-Client.In: - if !ok { // disconnect - return - } - t.Stop() - t = time.NewTimer(pingTime) - handleMsg(msg) - - case <-t.C: - Client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{Client.Server}} - t = time.NewTimer(pingTime) - - case err, ok := <-Client.Errors: - if ok && err != io.EOF { - log.Println(err) - return - } - } - } -} - -// HandleMsg handles IRC messages from the server. -func handleMsg(msg irc.Msg) { - switch msg.Cmd { - case irc.ERROR: - log.Println(1, "Received error: "+msg.Raw) - - case irc.PING: - Client.Out <- irc.Msg{Cmd: irc.PONG} - - case irc.PONG: - // OK, ignore - - case irc.ERR_NOSUCHNICK: - Bot.EventRecieved(Client, msg) - - case irc.ERR_NOSUCHCHANNEL: - Bot.EventRecieved(Client, msg) - - case irc.RPL_MOTD: - Bot.EventRecieved(Client, msg) - - case irc.RPL_NAMREPLY: - Bot.EventRecieved(Client, msg) - - case irc.RPL_TOPIC: - Bot.EventRecieved(Client, msg) - - case irc.KICK: - Bot.EventRecieved(Client, msg) - - case irc.TOPIC: - Bot.EventRecieved(Client, msg) - - case irc.MODE: - Bot.EventRecieved(Client, msg) - - case irc.JOIN: - Bot.EventRecieved(Client, msg) - - case irc.PART: - Bot.EventRecieved(Client, msg) - - case irc.QUIT: - os.Exit(1) - - case irc.NOTICE: - Bot.EventRecieved(Client, msg) - - case irc.PRIVMSG: - Bot.MsgRecieved(Client, msg) - - case irc.NICK: - Bot.EventRecieved(Client, msg) - - case irc.RPL_WHOREPLY: - Bot.EventRecieved(Client, msg) - - case irc.RPL_ENDOFWHO: - Bot.EventRecieved(Client, msg) + c := config.Readconfig(Version, *cfile) + var client bot.Connector + switch c.Type { + case "irc": + client = irc.New(c) + case "slack": + client = slack.New(c) default: - cmd := irc.CmdNames[msg.Cmd] - log.Println("(" + cmd + ") " + msg.Raw) + log.Fatalf("Unknown connection type: %s", c.Type) } + + b := bot.NewBot(c, client) + + // b.AddHandler(plugins.NewTestPlugin(b)) + b.AddHandler("admin", plugins.NewAdminPlugin(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)) + b.AddHandler("beers", plugins.NewBeersPlugin(b)) + b.AddHandler("counter", plugins.NewCounterPlugin(b)) + b.AddHandler("remember", plugins.NewRememberPlugin(b)) + b.AddHandler("skeleton", plugins.NewSkeletonPlugin(b)) + b.AddHandler("your", plugins.NewYourPlugin(b)) + // catches anything left, will always return true + b.AddHandler("factoid", plugins.NewFactoidPlugin(b)) + + client.Serve() } 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 new file mode 100644 index 0000000..cf6b6c8 --- /dev/null +++ b/slack/slack.go @@ -0,0 +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] +}