/
bot.go
157 lines (147 loc) · 3.96 KB
/
bot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package slack
import (
"net/http"
"net/url"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/websocket"
)
const (
// Version is the semantic version of this library.
Version = "0.2.0"
)
// Bot encapsulates all the data needed to interact with Slack.
type Bot struct {
Token string
Name string
ID string
Handlers map[string]([]BotAction)
Subhandlers map[string](map[string]([]BotAction))
Users map[string]*User
Channels map[string]string
reconnectURL string
}
// NewBot constructs a new bot with the passed-in Slack API token.
func NewBot(token string) *Bot {
return &Bot{
Token: token,
Name: "",
ID: "",
Handlers: make(map[string]([]BotAction)),
Subhandlers: make(map[string](map[string]([]BotAction))),
Users: make(map[string]*User),
Channels: make(map[string]string),
reconnectURL: "",
}
}
// StoreReconnectURL takes a "url" from an event and stores it. This is done so
// that when Slack migrates a team to a new host, the bot can use the reconnect
// URL to reattach to the team.
func StoreReconnectURL(bot *Bot, event map[string]interface{}) (*Message, Status) {
bot.reconnectURL = event["url"].(string)
return nil, Continue
}
// Start initiates the bot's interaction with Slack. It obtains a websockect
// URL, connects to it, and then starts the main loop.
func (bot *Bot) Start() error {
payload, err := bot.Call("rtm.start", url.Values{})
if err != nil {
return err
}
success, ok := payload["ok"].(bool)
if !(ok && success) {
return &Error{"could not connect to RTM API"}
}
websocketURL, _ := payload["url"].(string)
self := payload["self"].(map[string]interface{})
channels := payload["channels"].([]interface{})
for _, channelMap := range channels {
channel := channelMap.(map[string]interface{})
channelID := channel["id"].(string)
channelName := channel["name"].(string)
bot.Channels[channelName] = channelID
bot.Channels[channelID] = channelName
}
users := payload["users"].([]interface{})
for _, userMap := range users {
user := UserFromJSON(userMap.(map[string]interface{}))
bot.Users[user.Nick] = user
bot.Users[user.ID] = user
}
bot.Name = self["name"].(string)
bot.ID = self["id"].(string)
log.WithFields(log.Fields{
"id": bot.ID,
"name": bot.Name,
}).Info("bot authenticated")
bot.OnEvent("reconnect_url", StoreReconnectURL)
for {
reconnect, err := bot.connect(websocketURL)
if reconnect && bot.reconnectURL != "" {
websocketURL = bot.reconnectURL
} else {
return err
}
}
return nil
}
func (bot *Bot) connect(websocketURL string) (bool, error) {
dialer := websocket.Dialer{}
conn, _, err := dialer.Dial(websocketURL, http.Header{})
if err != nil {
return false, err
}
return bot.loop(conn), nil
}
func (bot *Bot) loop(conn *websocket.Conn) bool {
defer conn.Close()
for {
messageType, bytes, err := conn.ReadMessage()
if err != nil {
// ReadMessage returns an error if the connection is closed
return false
}
if messageType == websocket.BinaryMessage {
continue // ignore binary messages
}
event, err := unpackJSON(bytes)
if err != nil {
log.WithFields(log.Fields{
"raw bytes": bytes,
"error": err,
}).Warn("message could not be unpacked")
continue
}
log.WithFields(log.Fields{
"event": event,
}).Info("received event")
eventType, ok := event["type"]
if ok && eventType.(string) == "team_migration_started" {
return true
}
wrappers := bot.handle(event)
closeConnection := sendResponses(wrappers, conn)
if closeConnection {
return false
}
}
}
func sendResponses(wrappers []messageWrapper, conn *websocket.Conn) bool {
abort := false
for _, wrapper := range wrappers {
message := wrapper.message
switch wrapper.status {
case Continue:
if message != nil {
conn.WriteJSON(message.toMap())
}
case Shutdown:
if message != nil {
conn.WriteJSON(message.toMap())
}
abort = true
case ShutdownNow:
return true
}
}
return abort
}