diff --git a/bot/bot.go b/bot/bot.go index 873d361..777229c 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -93,6 +93,7 @@ func New(config *config.Config, connector Connector) Bot { connector.RegisterMessageReceived(bot.MsgReceived) connector.RegisterEventReceived(bot.EventReceived) + connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived) return bot } @@ -145,7 +146,7 @@ func (b *bot) migrateDB() { // Adds a constructed handler to the bots handlers list func (b *bot) AddHandler(name string, h Handler) { - b.plugins[strings.ToLower(name)] = h + b.plugins[name] = h b.pluginOrdering = append(b.pluginOrdering, name) if entry := h.RegisterWeb(); entry != nil { b.httpEndPoints[name] = *entry diff --git a/bot/handlers.go b/bot/handlers.go index e28dd65..651e0ae 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -22,7 +22,6 @@ func (b *bot) MsgReceived(msg msg.Message) { // 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)) b.checkHelp(msg.Channel, parts) @@ -53,16 +52,40 @@ func (b *bot) EventReceived(msg msg.Message) { } } -func (b *bot) SendMessage(channel, message string) { - b.conn.SendMessage(channel, message) +// Handle incoming replys +func (b *bot) ReplyMsgReceived(msg msg.Message, identifier string) { + log.Println("Received message: ", msg) + + for _, name := range b.pluginOrdering { + p := b.plugins[name] + if p.ReplyMessage(msg, identifier) { + break + } + } } -func (b *bot) SendAction(channel, message string) { - b.conn.SendAction(channel, message) +func (b *bot) SendMessage(channel, message string) string { + return b.conn.SendMessage(channel, message) } -func (b *bot) React(channel, reaction string, message msg.Message) { - b.conn.React(channel, reaction, message) +func (b *bot) SendAction(channel, message string) string { + return b.conn.SendAction(channel, message) +} + +func (b *bot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { + return b.conn.ReplyToMessageIdentifier(channel, message, identifier) +} + +func (b *bot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { + return b.conn.ReplyToMessage(channel, message, replyTo) +} + +func (b *bot) React(channel, reaction string, message msg.Message) bool { + return b.conn.React(channel, reaction, message) +} + +func (b *bot) Edit(channel, newMessage, identifier string) bool { + return b.conn.Edit(channel, newMessage, identifier) } func (b *bot) GetEmojiList() map[string]string { diff --git a/bot/interfaces.go b/bot/interfaces.go index 23e1185..2780d29 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -15,10 +15,14 @@ type Bot interface { DB() *sqlx.DB Who(string) []user.User AddHandler(string, Handler) - SendMessage(string, string) - SendAction(string, string) - React(string, string, msg.Message) + SendMessage(string, string) string + SendAction(string, string) string + ReplyToMessageIdentifier(string, string, string) (string, bool) + ReplyToMessage(string, string, msg.Message) (string, bool) + React(string, string, msg.Message) bool + Edit(string, string, string) bool MsgReceived(msg.Message) + ReplyMsgReceived(msg.Message, string) EventReceived(msg.Message) Filter(msg.Message, string) string LastMessage(string) (msg.Message, error) @@ -30,10 +34,14 @@ type Bot interface { type Connector interface { RegisterEventReceived(func(message msg.Message)) RegisterMessageReceived(func(message msg.Message)) + RegisterReplyMessageReceived(func(msg.Message, string)) - SendMessage(channel, message string) - SendAction(channel, message string) - React(string, string, msg.Message) + SendMessage(channel, message string) string + SendAction(channel, message string) string + ReplyToMessageIdentifier(string, string, string) (string, bool) + ReplyToMessage(string, string, msg.Message) (string, bool) + React(string, string, msg.Message) bool + Edit(string, string, string) bool GetEmojiList() map[string]string Serve() error @@ -44,6 +52,7 @@ type Connector interface { type Handler interface { Message(message msg.Message) bool Event(kind string, message msg.Message) bool + ReplyMessage(msg.Message, string) bool BotMessage(message msg.Message) bool Help(channel string, parts []string) RegisterWeb() *string diff --git a/bot/mock.go b/bot/mock.go index 6b809f0..7d6c6e6 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -3,7 +3,10 @@ package bot import ( + "fmt" "log" + "strconv" + "strings" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/mock" @@ -25,13 +28,22 @@ type MockBot struct { func (mb *MockBot) Config() *config.Config { return &mb.Cfg } func (mb *MockBot) DBVersion() int64 { return 1 } func (mb *MockBot) DB() *sqlx.DB { return mb.db } +func (mb *MockBot) Conn() Connector { return nil } func (mb *MockBot) Who(string) []user.User { return []user.User{} } func (mb *MockBot) AddHandler(name string, f Handler) {} -func (mb *MockBot) SendMessage(ch string, msg string) { +func (mb *MockBot) SendMessage(ch string, msg string) string { mb.Messages = append(mb.Messages, msg) + return fmt.Sprintf("m-%d", len(mb.Actions)-1) } -func (mb *MockBot) SendAction(ch string, msg string) { +func (mb *MockBot) SendAction(ch string, msg string) string { mb.Actions = append(mb.Actions, msg) + return fmt.Sprintf("a-%d", len(mb.Actions)-1) +} +func (mb *MockBot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { + return "", false +} +func (mb *MockBot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { + return "", false } func (mb *MockBot) MsgReceived(msg msg.Message) {} func (mb *MockBot) EventReceived(msg msg.Message) {} @@ -39,9 +51,43 @@ func (mb *MockBot) Filter(msg msg.Message, s string) string { return "" } 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) {} -func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } -func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} +func (mb *MockBot) React(channel, reaction string, message msg.Message) bool { return false } + +func (mb *MockBot) Edit(channel, newMessage, identifier string) bool { + isMessage := identifier[0] == 'm' + if !isMessage && identifier[0] != 'a' { + log.Printf("failed to parse identifier: %s", identifier) + return false + } + + index, err := strconv.Atoi(strings.Split(identifier, "-")[1]) + if err != nil { + log.Printf("failed to parse identifier: %s", identifier) + return false + } + + if isMessage { + if index < len(mb.Messages) { + mb.Messages[index] = newMessage + } else { + return false + } + } else { + if index < len(mb.Actions) { + mb.Actions[index] = newMessage + } else { + return false + } + } + return true +} + +func (mb *MockBot) ReplyMsgReceived(msg.Message, string) { + +} + +func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } +func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} func NewMockBot() *MockBot { db, err := sqlx.Open("sqlite3_custom", ":memory:") diff --git a/irc/irc.go b/irc/irc.go index 5ff2b07..4c34529 100644 --- a/irc/irc.go +++ b/irc/irc.go @@ -44,6 +44,7 @@ type Irc struct { eventReceived func(msg.Message) messageReceived func(msg.Message) + replyMessageReceived func(msg.Message, string) } func New(c *config.Config) *Irc { @@ -61,12 +62,16 @@ func (i *Irc) RegisterMessageReceived(f func(msg.Message)) { i.messageReceived = f } +func (i *Irc) RegisterReplyMessageReceived(f func(msg.Message, string)) { + i.replyMessageReceived = 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) { +func (i *Irc) SendMessage(channel, message string) string { for len(message) > 0 { m := irc.Msg{ Cmd: "PRIVMSG", @@ -90,17 +95,33 @@ func (i *Irc) SendMessage(channel, message string) { i.Client.Out <- m } + return "NO_IRC_IDENTIFIERS" } // Sends action to channel -func (i *Irc) SendAction(channel, message string) { +func (i *Irc) SendAction(channel, message string) string { message = actionPrefix + " " + message + "\x01" i.SendMessage(channel, message) + return "NO_IRC_IDENTIFIERS" } -func (i *Irc) React(channel, reaction string, message msg.Message) { +func (i *Irc) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { + return "NO_IRC_IDENTIFIERS", false +} + +func (i *Irc) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { + return "NO_IRC_IDENTIFIERS", false +} + +func (i *Irc) React(channel, reaction string, message msg.Message) bool { //we're not goign to do anything because it's IRC + return false +} + +func (i *Irc) Edit(channel, newMessage, identifier string) bool { + //we're not goign to do anything because it's IRC + return false } func (i *Irc) GetEmojiList() map[string]string { diff --git a/main.go b/main.go index f576aca..069a27f 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "github.com/velour/catbase/plugins/leftpad" "github.com/velour/catbase/plugins/reaction" "github.com/velour/catbase/plugins/reminder" + "github.com/velour/catbase/plugins/rpgORdie" "github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/stats" "github.com/velour/catbase/plugins/talker" @@ -69,6 +70,7 @@ func main() { b.AddHandler("emojifyme", emojifyme.New(b)) b.AddHandler("twitch", twitch.New(b)) b.AddHandler("inventory", inventory.New(b)) + b.AddHandler("rpgORdie", rpgORdie.New(b)) // catches anything left, will always return true b.AddHandler("factoid", fact.New(b)) diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go index 85988dc..1113bdd 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -117,3 +117,5 @@ func (p *AdminPlugin) BotMessage(message msg.Message) bool { func (p *AdminPlugin) RegisterWeb() *string { return nil } + +func (p *AdminPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/babbler/babbler.go b/plugins/babbler/babbler.go index e5961c9..0ae0b58 100644 --- a/plugins/babbler/babbler.go +++ b/plugins/babbler/babbler.go @@ -935,3 +935,5 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri return strings.Join(words, " "), nil } + +func (p *BabblerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go index 1bc6521..6d1b83e 100644 --- a/plugins/beers/beers.go +++ b/plugins/beers/beers.go @@ -461,3 +461,5 @@ func (p *BeersPlugin) BotMessage(message msg.Message) bool { func (p *BeersPlugin) RegisterWeb() *string { return nil } + +func (p *BeersPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index ebe51b0..ed70349 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -364,3 +364,5 @@ func (p *CounterPlugin) BotMessage(message msg.Message) bool { func (p *CounterPlugin) RegisterWeb() *string { return nil } + +func (p *CounterPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/dice/dice.go b/plugins/dice/dice.go index 07484ce..3ee6dc3 100644 --- a/plugins/dice/dice.go +++ b/plugins/dice/dice.go @@ -100,3 +100,5 @@ func (p *DicePlugin) BotMessage(message msg.Message) bool { func (p *DicePlugin) RegisterWeb() *string { return nil } + +func (p *DicePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/downtime/downtime.go b/plugins/downtime/downtime.go index e553ed2..ef9df9e 100644 --- a/plugins/downtime/downtime.go +++ b/plugins/downtime/downtime.go @@ -231,3 +231,5 @@ func (p *DowntimePlugin) BotMessage(message msg.Message) bool { 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 65b892f..e8a6119 100644 --- a/plugins/emojifyme/emojifyme.go +++ b/plugins/emojifyme/emojifyme.go @@ -112,3 +112,5 @@ func (p *EmojifyMePlugin) BotMessage(message msg.Message) bool { func (p *EmojifyMePlugin) RegisterWeb() *string { return nil } + +func (p *EmojifyMePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go index be364cf..17a4e4a 100644 --- a/plugins/fact/factoid.go +++ b/plugins/fact/factoid.go @@ -764,3 +764,5 @@ func (p *Factoid) serveQuery(w http.ResponseWriter, r *http.Request) { log.Println(err) } } + +func (p *Factoid) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/fact/remember.go b/plugins/fact/remember.go index e4fc6bd..1038eb7 100644 --- a/plugins/fact/remember.go +++ b/plugins/fact/remember.go @@ -170,3 +170,5 @@ 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) } + +func (p *RememberPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/first/first.go b/plugins/first/first.go index 9d63c6f..3614576 100644 --- a/plugins/first/first.go +++ b/plugins/first/first.go @@ -228,3 +228,5 @@ func (p *FirstPlugin) BotMessage(message msg.Message) bool { func (p *FirstPlugin) RegisterWeb() *string { return nil } + +func (p *FirstPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/inventory/inventory.go b/plugins/inventory/inventory.go index abbaa35..7efb807 100644 --- a/plugins/inventory/inventory.go +++ b/plugins/inventory/inventory.go @@ -236,3 +236,5 @@ func (p *InventoryPlugin) RegisterWeb() *string { // nothing to register return nil } + +func (p *InventoryPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/leftpad/leftpad.go b/plugins/leftpad/leftpad.go index 6beb410..11098d9 100644 --- a/plugins/leftpad/leftpad.go +++ b/plugins/leftpad/leftpad.go @@ -76,3 +76,5 @@ func (p *LeftpadPlugin) RegisterWeb() *string { // nothing to register return nil } + +func (p *LeftpadPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/reaction/reaction.go b/plugins/reaction/reaction.go index a52c17f..7b204b4 100644 --- a/plugins/reaction/reaction.go +++ b/plugins/reaction/reaction.go @@ -80,3 +80,5 @@ func (p *ReactionPlugin) BotMessage(message msg.Message) bool { func (p *ReactionPlugin) RegisterWeb() *string { return nil } + +func (p *ReactionPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go index 1699da2..f55cb3d 100644 --- a/plugins/reminder/reminder.go +++ b/plugins/reminder/reminder.go @@ -322,3 +322,5 @@ func reminderer(p *ReminderPlugin) { p.queueUpNextReminder() } } + +func (p *ReminderPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/rpgORdie/rpgORdie.go b/plugins/rpgORdie/rpgORdie.go new file mode 100644 index 0000000..7320046 --- /dev/null +++ b/plugins/rpgORdie/rpgORdie.go @@ -0,0 +1,169 @@ +package rpgORdie + +import ( + "fmt" + "strings" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" +) + +const ( + DUDE = ":lion_face:" + BOULDER = ":full_moon:" + HOLE = ":new_moon:" + EMPTY = ":white_large_square:" + + OK = iota + INVALID = iota + WIN = iota +) + +type RPGPlugin struct { + Bot bot.Bot + listenFor map[string]*board +} + +type board struct { + state [][]string + x, y int +} + +func NewRandomBoard() *board { + boardSize := 5 + b := board{ + state: make([][]string, boardSize), + x: boardSize - 1, + y: boardSize - 1, + } + for i := 0; i < boardSize; i++ { + b.state[i] = make([]string, boardSize) + for j := 0; j < boardSize; j++ { + b.state[i][j] = ":white_large_square:" + } + } + + b.state[boardSize-1][boardSize-1] = DUDE + b.state[boardSize/2][boardSize/2] = BOULDER + b.state[0][0] = HOLE + + return &b +} + +func (b *board) toMessageString() string { + lines := make([]string, len(b.state)) + for i := 0; i < len(b.state); i++ { + lines[i] = strings.Join(b.state[i], "") + } + return strings.Join(lines, "\n") +} + +func (b *board) checkAndMove(dx, dy int) int { + newX := b.x + dx + newY := b.y + dy + + if newX < 0 || newY < 0 || newX >= len(b.state) || newY >= len(b.state) { + return INVALID + } + + if b.state[newY][newX] == HOLE { + return INVALID + } + + win := false + if b.state[newY][newX] == BOULDER { + newBoulderX := newX + dx + newBoulderY := newY + dy + + if newBoulderX < 0 || newBoulderY < 0 || newBoulderX >= len(b.state) || newBoulderY >= len(b.state) { + return INVALID + } + + if b.state[newBoulderY][newBoulderX] != HOLE { + b.state[newBoulderY][newBoulderX] = BOULDER + } else { + win = true + } + } + + b.state[newY][newX] = DUDE + b.state[b.y][b.x] = EMPTY + b.x = newX + b.y = newY + + if win { + return WIN + } + return OK +} + +func New(b bot.Bot) *RPGPlugin { + return &RPGPlugin{ + Bot: b, + listenFor: map[string]*board{}, + } +} + +func (p *RPGPlugin) Message(message msg.Message) bool { + if strings.ToLower(message.Body) == "start rpg" { + b := NewRandomBoard() + ts := p.Bot.SendMessage(message.Channel, b.toMessageString()) + p.listenFor[ts] = b + p.Bot.ReplyToMessageIdentifier(message.Channel, "Over here.", ts) + return true + } + return false +} + +func (p *RPGPlugin) LoadData() { + +} + +func (p *RPGPlugin) Help(channel string, parts []string) { + p.Bot.SendMessage(channel, "Go find a walkthrough or something.") +} + +func (p *RPGPlugin) Event(kind string, message msg.Message) bool { + return false +} + +func (p *RPGPlugin) BotMessage(message msg.Message) bool { + return false +} + +func (p *RPGPlugin) RegisterWeb() *string { + return nil +} + +func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool { + if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) { + if b, ok := p.listenFor[identifier]; ok { + + var res int + + if message.Body == "left" { + res = b.checkAndMove(-1, 0) + } else if message.Body == "right" { + res = b.checkAndMove(1, 0) + } else if message.Body == "up" { + res = b.checkAndMove(0, -1) + } else if message.Body == "down" { + res = b.checkAndMove(0, 1) + } else { + return false + } + + switch res { + case OK: + p.Bot.Edit(message.Channel, b.toMessageString(), identifier) + case WIN: + p.Bot.Edit(message.Channel, b.toMessageString(), identifier) + p.Bot.ReplyToMessageIdentifier(message.Channel, "congratulations, you beat the easiest level imaginable.", identifier) + case INVALID: + p.Bot.ReplyToMessageIdentifier(message.Channel, fmt.Sprintf("you can't move %s", message.Body), identifier) + } + return true + } + } + return false +} diff --git a/plugins/rpgORdie/rpgORdie_test.go b/plugins/rpgORdie/rpgORdie_test.go new file mode 100644 index 0000000..ddcd924 --- /dev/null +++ b/plugins/rpgORdie/rpgORdie_test.go @@ -0,0 +1,3 @@ +package rpgORdie + +import () diff --git a/plugins/rss/rss.go b/plugins/rss/rss.go index bc7bee5..099cdf9 100644 --- a/plugins/rss/rss.go +++ b/plugins/rss/rss.go @@ -117,3 +117,5 @@ func (p *RSSPlugin) BotMessage(message msg.Message) bool { func (p *RSSPlugin) RegisterWeb() *string { return nil } + +func (p *RSSPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go index 7b7039b..81450b2 100644 --- a/plugins/stats/stats.go +++ b/plugins/stats/stats.go @@ -275,3 +275,5 @@ func (p *StatsPlugin) mkSightingStat(message msg.Message) stats { func (p *StatsPlugin) mkChannelStat(message msg.Message) stats { return stats{stat{mkDay(), "channel", message.Channel, 1}} } + +func (p *StatsPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/talker/talker.go b/plugins/talker/talker.go index 1e01b90..17e9d1b 100644 --- a/plugins/talker/talker.go +++ b/plugins/talker/talker.go @@ -119,3 +119,5 @@ func (p *TalkerPlugin) BotMessage(message msg.Message) bool { func (p *TalkerPlugin) RegisterWeb() *string { return nil } + +func (p *TalkerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/twitch/twitch.go b/plugins/twitch/twitch.go index 4a5c1a0..941b55a 100644 --- a/plugins/twitch/twitch.go +++ b/plugins/twitch/twitch.go @@ -238,3 +238,5 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri twitcher.game = game } } + +func (p *TwitchPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/your/your.go b/plugins/your/your.go index 9100d37..9e24082 100644 --- a/plugins/your/your.go +++ b/plugins/your/your.go @@ -66,3 +66,5 @@ func (p *YourPlugin) BotMessage(message msg.Message) bool { func (p *YourPlugin) RegisterWeb() *string { return nil } + +func (p *YourPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/plugins/zork/zork.go b/plugins/zork/zork.go index 7bd97bd..cabc6d2 100644 --- a/plugins/zork/zork.go +++ b/plugins/zork/zork.go @@ -122,3 +122,5 @@ func (p *ZorkPlugin) Help(ch string, _ []string) { } func (p *ZorkPlugin) RegisterWeb() *string { return nil } + +func (p *ZorkPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false } diff --git a/slack/slack.go b/slack/slack.go index 5f885bb..ade25ef 100644 --- a/slack/slack.go +++ b/slack/slack.go @@ -5,6 +5,7 @@ package slack import ( "encoding/json" + "errors" "fmt" "html" "io" @@ -15,7 +16,7 @@ import ( "regexp" "strconv" "strings" - "sync/atomic" + // "sync/atomic" "time" "github.com/velour/catbase/bot" @@ -40,6 +41,7 @@ type Slack struct { eventReceived func(msg.Message) messageReceived func(msg.Message) + replyMessageReceived func(msg.Message, string) } var idCounter uint64 @@ -133,6 +135,7 @@ type slackMessage struct { User string `json:"user"` Username string `json:"username"` Ts string `json:"ts"` + ThreadTs string `json:"thread_ts"` Error struct { Code uint64 `json:"code"` Msg string `json:"msg"` @@ -163,6 +166,27 @@ func New(c *config.Config) *Slack { } } +func checkReturnStatus(response *http.Response) bool { + type Response struct { + OK bool `json:"ok"` + } + + body, err := ioutil.ReadAll(response.Body) + response.Body.Close() + if err != nil { + log.Printf("Error reading Slack API body: %s", err) + return false + } + + var resp Response + err = json.Unmarshal(body, &resp) + if err != nil { + log.Printf("Error parsing message response: %s", err) + return false + } + return resp.OK +} + func (s *Slack) RegisterEventReceived(f func(msg.Message)) { s.eventReceived = f } @@ -171,32 +195,112 @@ func (s *Slack) RegisterMessageReceived(f func(msg.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, +func (s *Slack) RegisterReplyMessageReceived(f func(msg.Message, string)) { + s.replyMessageReceived = 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" } - err := websocket.JSON.Send(s.ws, m) + + resp, err := http.PostForm(postUrl, + url.Values{"token": {s.config.Slack.Token}, + "channel": {channel}, + "text": {message}, + "as_user": {"true"}, + }) + if err != nil { log.Printf("Error sending Slack message: %s", err) } - return 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"` + } + + 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") + } + + return mr.Timestamp, err } -func (s *Slack) SendMessage(channel, message string) { +func (s *Slack) SendMessage(channel, message string) string { log.Printf("Sending message to %s: %s", channel, message) - s.SendMessageType(channel, "message", "", message) + identifier, _ := s.SendMessageType(channel, message, false) + return identifier } -func (s *Slack) SendAction(channel, message string) { +func (s *Slack) SendAction(channel, message string) string { log.Printf("Sending action to %s: %s", channel, message) - s.SendMessageType(channel, "message", "me_message", "_"+message+"_") + identifier, _ := s.SendMessageType(channel, "_"+message+"_", true) + return identifier } -func (s *Slack) React(channel, reaction string, message msg.Message) { +func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { + resp, err := http.PostForm("https://slack.com/api/chat.postMessage", + url.Values{"token": {s.config.Slack.Token}, + "channel": {channel}, + "text": {message}, + "as_user": {"true"}, + "thread_ts": {identifier}, + }) + + if err != nil { + log.Printf("Error sending Slack reply: %s", err) + return "", false + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Printf("Error reading Slack API body: %s", err) + return "", false + } + + 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 { + log.Printf("Error parsing message response: %s", err) + return "", false + } + + if !mr.OK { + return "", false + } + + return mr.Timestamp, err == nil +} + +func (s *Slack) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { + return s.ReplyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"]) +} + +func (s *Slack) React(channel, reaction string, message msg.Message) bool { log.Printf("Reacting in %s: %s", channel, reaction) resp, err := http.PostForm("https://slack.com/api/reactions.add", url.Values{"token": {s.config.Slack.Token}, @@ -204,9 +308,24 @@ func (s *Slack) React(channel, reaction string, message msg.Message) { "channel": {channel}, "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) if err != nil { - log.Printf("Error sending Slack reaction: %s", err) + log.Println("reaction failed: %s", err) + return false } - log.Print(resp) + return checkReturnStatus(resp) +} + +func (s *Slack) Edit(channel, newMessage, identifier string) bool { + log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage) + resp, err := http.PostForm("https://slack.com/api/chat.update", + url.Values{"token": {s.config.Slack.Token}, + "channel": {channel}, + "text": {newMessage}, + "ts": {identifier}}) + if err != nil { + log.Println("edit failed: %s", err) + return false + } + return checkReturnStatus(resp) } func (s *Slack) GetEmojiList() map[string]string { @@ -273,14 +392,17 @@ func (s *Slack) Serve() error { } switch msg.Type { case "message": - if !msg.Hidden { + if !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.messageReceived(s.buildMessage(msg)) + s.messageReceived(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.replyMessageReceived(s.buildLightReplyMessage(msg), msg.ThreadTs) } else { log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) } @@ -337,6 +459,40 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message { } } +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()