Merge pull request #91 from velour/rpgORdie

RPG or DIEEEEEEEEEEEE
This commit is contained in:
Scott Kiesel 2017-11-09 06:14:00 -05:00 committed by GitHub
commit 695c749727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 511 additions and 41 deletions

View File

@ -93,6 +93,7 @@ func New(config *config.Config, connector Connector) Bot {
connector.RegisterMessageReceived(bot.MsgReceived) connector.RegisterMessageReceived(bot.MsgReceived)
connector.RegisterEventReceived(bot.EventReceived) connector.RegisterEventReceived(bot.EventReceived)
connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived)
return bot return bot
} }
@ -145,7 +146,7 @@ func (b *bot) migrateDB() {
// Adds a constructed handler to the bots handlers list // Adds a constructed handler to the bots handlers list
func (b *bot) AddHandler(name string, h Handler) { func (b *bot) AddHandler(name string, h Handler) {
b.plugins[strings.ToLower(name)] = h b.plugins[name] = h
b.pluginOrdering = append(b.pluginOrdering, name) b.pluginOrdering = append(b.pluginOrdering, name)
if entry := h.RegisterWeb(); entry != nil { if entry := h.RegisterWeb(); entry != nil {
b.httpEndPoints[name] = *entry b.httpEndPoints[name] = *entry

View File

@ -22,7 +22,6 @@ func (b *bot) MsgReceived(msg msg.Message) {
// msg := b.buildMessage(client, inMsg) // msg := b.buildMessage(client, inMsg)
// do need to look up user and fix it // do need to look up user and fix it
if strings.HasPrefix(msg.Body, "help ") && msg.Command { if strings.HasPrefix(msg.Body, "help ") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body)) parts := strings.Fields(strings.ToLower(msg.Body))
b.checkHelp(msg.Channel, parts) b.checkHelp(msg.Channel, parts)
@ -53,16 +52,40 @@ func (b *bot) EventReceived(msg msg.Message) {
} }
} }
func (b *bot) SendMessage(channel, message string) { // Handle incoming replys
b.conn.SendMessage(channel, message) 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) { func (b *bot) SendMessage(channel, message string) string {
b.conn.SendAction(channel, message) return b.conn.SendMessage(channel, message)
} }
func (b *bot) React(channel, reaction string, message msg.Message) { func (b *bot) SendAction(channel, message string) string {
b.conn.React(channel, reaction, message) 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 { func (b *bot) GetEmojiList() map[string]string {

View File

@ -15,10 +15,14 @@ type Bot interface {
DB() *sqlx.DB DB() *sqlx.DB
Who(string) []user.User Who(string) []user.User
AddHandler(string, Handler) AddHandler(string, Handler)
SendMessage(string, string) SendMessage(string, string) string
SendAction(string, string) SendAction(string, string) string
React(string, string, msg.Message) 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) MsgReceived(msg.Message)
ReplyMsgReceived(msg.Message, string)
EventReceived(msg.Message) EventReceived(msg.Message)
Filter(msg.Message, string) string Filter(msg.Message, string) string
LastMessage(string) (msg.Message, error) LastMessage(string) (msg.Message, error)
@ -30,10 +34,14 @@ type Bot interface {
type Connector interface { type Connector interface {
RegisterEventReceived(func(message msg.Message)) RegisterEventReceived(func(message msg.Message))
RegisterMessageReceived(func(message msg.Message)) RegisterMessageReceived(func(message msg.Message))
RegisterReplyMessageReceived(func(msg.Message, string))
SendMessage(channel, message string) SendMessage(channel, message string) string
SendAction(channel, message string) SendAction(channel, message string) string
React(string, string, msg.Message) 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 GetEmojiList() map[string]string
Serve() error Serve() error
@ -44,6 +52,7 @@ type Connector interface {
type Handler interface { type Handler interface {
Message(message msg.Message) bool Message(message msg.Message) bool
Event(kind string, message msg.Message) bool Event(kind string, message msg.Message) bool
ReplyMessage(msg.Message, string) bool
BotMessage(message msg.Message) bool BotMessage(message msg.Message) bool
Help(channel string, parts []string) Help(channel string, parts []string)
RegisterWeb() *string RegisterWeb() *string

View File

@ -3,7 +3,10 @@
package bot package bot
import ( import (
"fmt"
"log" "log"
"strconv"
"strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -25,13 +28,22 @@ type MockBot struct {
func (mb *MockBot) Config() *config.Config { return &mb.Cfg } func (mb *MockBot) Config() *config.Config { return &mb.Cfg }
func (mb *MockBot) DBVersion() int64 { return 1 } func (mb *MockBot) DBVersion() int64 { return 1 }
func (mb *MockBot) DB() *sqlx.DB { return mb.db } 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) Who(string) []user.User { return []user.User{} }
func (mb *MockBot) AddHandler(name string, f Handler) {} 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) 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) 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) MsgReceived(msg msg.Message) {}
func (mb *MockBot) EventReceived(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) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil }
func (mb *MockBot) CheckAdmin(nick string) bool { return false } func (mb *MockBot) CheckAdmin(nick string) bool { return false }
func (mb *MockBot) React(channel, reaction string, message msg.Message) {} func (mb *MockBot) React(channel, reaction string, message msg.Message) bool { return false }
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) 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 { func NewMockBot() *MockBot {
db, err := sqlx.Open("sqlite3_custom", ":memory:") db, err := sqlx.Open("sqlite3_custom", ":memory:")

View File

@ -44,6 +44,7 @@ type Irc struct {
eventReceived func(msg.Message) eventReceived func(msg.Message)
messageReceived func(msg.Message) messageReceived func(msg.Message)
replyMessageReceived func(msg.Message, string)
} }
func New(c *config.Config) *Irc { func New(c *config.Config) *Irc {
@ -61,12 +62,16 @@ func (i *Irc) RegisterMessageReceived(f func(msg.Message)) {
i.messageReceived = f i.messageReceived = f
} }
func (i *Irc) RegisterReplyMessageReceived(f func(msg.Message, string)) {
i.replyMessageReceived = f
}
func (i *Irc) JoinChannel(channel string) { func (i *Irc) JoinChannel(channel string) {
log.Printf("Joining channel: %s", channel) log.Printf("Joining channel: %s", channel)
i.Client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{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 { for len(message) > 0 {
m := irc.Msg{ m := irc.Msg{
Cmd: "PRIVMSG", Cmd: "PRIVMSG",
@ -90,17 +95,33 @@ func (i *Irc) SendMessage(channel, message string) {
i.Client.Out <- m i.Client.Out <- m
} }
return "NO_IRC_IDENTIFIERS"
} }
// Sends action to channel // Sends action to channel
func (i *Irc) SendAction(channel, message string) { func (i *Irc) SendAction(channel, message string) string {
message = actionPrefix + " " + message + "\x01" message = actionPrefix + " " + message + "\x01"
i.SendMessage(channel, message) 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 //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 { func (i *Irc) GetEmojiList() map[string]string {

View File

@ -21,6 +21,7 @@ import (
"github.com/velour/catbase/plugins/leftpad" "github.com/velour/catbase/plugins/leftpad"
"github.com/velour/catbase/plugins/reaction" "github.com/velour/catbase/plugins/reaction"
"github.com/velour/catbase/plugins/reminder" "github.com/velour/catbase/plugins/reminder"
"github.com/velour/catbase/plugins/rpgORdie"
"github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/rss"
"github.com/velour/catbase/plugins/stats" "github.com/velour/catbase/plugins/stats"
"github.com/velour/catbase/plugins/talker" "github.com/velour/catbase/plugins/talker"
@ -69,6 +70,7 @@ func main() {
b.AddHandler("emojifyme", emojifyme.New(b)) b.AddHandler("emojifyme", emojifyme.New(b))
b.AddHandler("twitch", twitch.New(b)) b.AddHandler("twitch", twitch.New(b))
b.AddHandler("inventory", inventory.New(b)) b.AddHandler("inventory", inventory.New(b))
b.AddHandler("rpgORdie", rpgORdie.New(b))
// catches anything left, will always return true // catches anything left, will always return true
b.AddHandler("factoid", fact.New(b)) b.AddHandler("factoid", fact.New(b))

View File

@ -117,3 +117,5 @@ func (p *AdminPlugin) BotMessage(message msg.Message) bool {
func (p *AdminPlugin) RegisterWeb() *string { func (p *AdminPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *AdminPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -935,3 +935,5 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
return strings.Join(words, " "), nil return strings.Join(words, " "), nil
} }
func (p *BabblerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -461,3 +461,5 @@ func (p *BeersPlugin) BotMessage(message msg.Message) bool {
func (p *BeersPlugin) RegisterWeb() *string { func (p *BeersPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *BeersPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -364,3 +364,5 @@ func (p *CounterPlugin) BotMessage(message msg.Message) bool {
func (p *CounterPlugin) RegisterWeb() *string { func (p *CounterPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *CounterPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -100,3 +100,5 @@ func (p *DicePlugin) BotMessage(message msg.Message) bool {
func (p *DicePlugin) RegisterWeb() *string { func (p *DicePlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *DicePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -231,3 +231,5 @@ func (p *DowntimePlugin) BotMessage(message msg.Message) bool {
func (p *DowntimePlugin) RegisterWeb() *string { func (p *DowntimePlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *DowntimePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -112,3 +112,5 @@ func (p *EmojifyMePlugin) BotMessage(message msg.Message) bool {
func (p *EmojifyMePlugin) RegisterWeb() *string { func (p *EmojifyMePlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *EmojifyMePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -764,3 +764,5 @@ func (p *Factoid) serveQuery(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
} }
func (p *Factoid) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -170,3 +170,5 @@ func (p *RememberPlugin) recordMsg(message msg.Message) {
log.Printf("Logging message: %s: %s", message.User.Name, message.Body) log.Printf("Logging message: %s: %s", message.User.Name, message.Body)
p.Log[message.Channel] = append(p.Log[message.Channel], message) p.Log[message.Channel] = append(p.Log[message.Channel], message)
} }
func (p *RememberPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -228,3 +228,5 @@ func (p *FirstPlugin) BotMessage(message msg.Message) bool {
func (p *FirstPlugin) RegisterWeb() *string { func (p *FirstPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *FirstPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -236,3 +236,5 @@ func (p *InventoryPlugin) RegisterWeb() *string {
// nothing to register // nothing to register
return nil return nil
} }
func (p *InventoryPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -76,3 +76,5 @@ func (p *LeftpadPlugin) RegisterWeb() *string {
// nothing to register // nothing to register
return nil return nil
} }
func (p *LeftpadPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -80,3 +80,5 @@ func (p *ReactionPlugin) BotMessage(message msg.Message) bool {
func (p *ReactionPlugin) RegisterWeb() *string { func (p *ReactionPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *ReactionPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -322,3 +322,5 @@ func reminderer(p *ReminderPlugin) {
p.queueUpNextReminder() p.queueUpNextReminder()
} }
} }
func (p *ReminderPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -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
}

View File

@ -0,0 +1,3 @@
package rpgORdie
import ()

View File

@ -117,3 +117,5 @@ func (p *RSSPlugin) BotMessage(message msg.Message) bool {
func (p *RSSPlugin) RegisterWeb() *string { func (p *RSSPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *RSSPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -275,3 +275,5 @@ func (p *StatsPlugin) mkSightingStat(message msg.Message) stats {
func (p *StatsPlugin) mkChannelStat(message msg.Message) stats { func (p *StatsPlugin) mkChannelStat(message msg.Message) stats {
return stats{stat{mkDay(), "channel", message.Channel, 1}} return stats{stat{mkDay(), "channel", message.Channel, 1}}
} }
func (p *StatsPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -119,3 +119,5 @@ func (p *TalkerPlugin) BotMessage(message msg.Message) bool {
func (p *TalkerPlugin) RegisterWeb() *string { func (p *TalkerPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *TalkerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -238,3 +238,5 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
twitcher.game = game twitcher.game = game
} }
} }
func (p *TwitchPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -66,3 +66,5 @@ func (p *YourPlugin) BotMessage(message msg.Message) bool {
func (p *YourPlugin) RegisterWeb() *string { func (p *YourPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *YourPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -122,3 +122,5 @@ func (p *ZorkPlugin) Help(ch string, _ []string) {
} }
func (p *ZorkPlugin) RegisterWeb() *string { return nil } func (p *ZorkPlugin) RegisterWeb() *string { return nil }
func (p *ZorkPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -5,6 +5,7 @@ package slack
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"html" "html"
"io" "io"
@ -15,7 +16,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" // "sync/atomic"
"time" "time"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
@ -40,6 +41,7 @@ type Slack struct {
eventReceived func(msg.Message) eventReceived func(msg.Message)
messageReceived func(msg.Message) messageReceived func(msg.Message)
replyMessageReceived func(msg.Message, string)
} }
var idCounter uint64 var idCounter uint64
@ -133,6 +135,7 @@ type slackMessage struct {
User string `json:"user"` User string `json:"user"`
Username string `json:"username"` Username string `json:"username"`
Ts string `json:"ts"` Ts string `json:"ts"`
ThreadTs string `json:"thread_ts"`
Error struct { Error struct {
Code uint64 `json:"code"` Code uint64 `json:"code"`
Msg string `json:"msg"` 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)) { func (s *Slack) RegisterEventReceived(f func(msg.Message)) {
s.eventReceived = f s.eventReceived = f
} }
@ -171,32 +195,112 @@ func (s *Slack) RegisterMessageReceived(f func(msg.Message)) {
s.messageReceived = f s.messageReceived = f
} }
func (s *Slack) SendMessageType(channel, messageType, subType, message string) error { func (s *Slack) RegisterReplyMessageReceived(f func(msg.Message, string)) {
m := slackMessage{ s.replyMessageReceived = f
ID: atomic.AddUint64(&idCounter, 1), }
Type: messageType,
SubType: subType, func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string, error) {
Channel: channel, postUrl := "https://slack.com/api/chat.postMessage"
Text: message, 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 { if err != nil {
log.Printf("Error sending Slack message: %s", err) 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) 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) 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) log.Printf("Reacting in %s: %s", channel, reaction)
resp, err := http.PostForm("https://slack.com/api/reactions.add", resp, err := http.PostForm("https://slack.com/api/reactions.add",
url.Values{"token": {s.config.Slack.Token}, url.Values{"token": {s.config.Slack.Token},
@ -204,9 +308,24 @@ func (s *Slack) React(channel, reaction string, message msg.Message) {
"channel": {channel}, "channel": {channel},
"timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}})
if err != nil { 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 { func (s *Slack) GetEmojiList() map[string]string {
@ -273,14 +392,17 @@ func (s *Slack) Serve() error {
} }
switch msg.Type { switch msg.Type {
case "message": case "message":
if !msg.Hidden { if !msg.Hidden && msg.ThreadTs == "" {
m := s.buildMessage(msg) m := s.buildMessage(msg)
if m.Time.Before(s.lastRecieved) { if m.Time.Before(s.lastRecieved) {
log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time) log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time)
} else { } else {
s.lastRecieved = m.Time 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 { } else {
log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) 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 // markAllChannelsRead gets a list of all channels and marks each as read
func (s *Slack) markAllChannelsRead() { func (s *Slack) markAllChannelsRead() {
chs := s.getAllChannels() chs := s.getAllChannels()