This repository has been archived by the owner on Oct 21, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bot.go
293 lines (258 loc) · 8.48 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package bot
import (
"fmt"
"strings"
"github.com/nlopes/slack"
log "github.com/sirupsen/logrus"
"github.com/ubclaunchpad/rocket/cmd"
"github.com/ubclaunchpad/rocket/config"
"github.com/ubclaunchpad/rocket/data"
"github.com/ubclaunchpad/rocket/github"
"github.com/ubclaunchpad/rocket/model"
)
const (
// Our Slack Bot's username on the UBC Launch Pad Slack
username = "U5RU9TB38"
// Default message to send when any error occurs
errorMessage = "Oops, an error occurred :robot_face:. Bruno must have " +
"coded a bug... Sorry about that!"
// GithubAllTeamID for the `all` team that everyone should be on
GithubAllTeamID = 2467607
)
var noParams = slack.PostMessageParameters{}
// EventHandler is any function that handles a Slack event
type EventHandler func(slack.RTMEvent)
// Bot represents an instance of the Rocket Slack bot. Only one should be
// created under normal circumstances.
type Bot struct {
token string
API *slack.Client
rtm *slack.RTM
DAL *data.DAL
GitHub *github.API
Log *log.Entry
Commands map[string]*cmd.Command
handlers map[string][]EventHandler
Users map[string]slack.User
}
// New constructs and returns a new Slack bot instance. It creates a new RTM
// object to receive incoming messages, populates a cache with users, and
// sets up command handlers.
func New(cfg *config.Config, dal *data.DAL, gh *github.API, log *log.Entry) *Bot {
api := slack.New(cfg.SlackToken)
b := &Bot{
token: cfg.SlackToken,
API: api,
rtm: api.NewRTM(),
DAL: dal,
GitHub: gh,
Log: log,
Commands: map[string]*cmd.Command{},
handlers: map[string][]EventHandler{},
}
b.UpdateUsers()
// Register default Slack event handlers
b.RegisterEventHandlers(map[string]EventHandler{
"message": b.handleMessageEvent,
"team_join": b.handleUserChange,
"user_change": b.handleUserChange,
})
return b
}
// NewEmptyBot returns a bare-bones, empty bot used for testing
func NewEmptyBot() *Bot {
return &Bot{
Commands: map[string]*cmd.Command{},
handlers: map[string][]EventHandler{},
Log: log.WithField("test", "test"),
}
}
// RegisterEventHandlers registers a handlers for different events. These
// handlers will be called when an event of the corresponding type is received.
func (b *Bot) RegisterEventHandlers(handlers map[string]EventHandler) {
for evt, handler := range handlers {
if b.handlers[evt] == nil {
b.handlers[evt] = []EventHandler{handler}
} else {
b.handlers[evt] = append(b.handlers[evt], handler)
}
b.Log.Infof("registered handler for %s Slack event", evt)
}
}
// RegisterCommands registers commands that the bot should handle. Returns an
// error if multiple commands were registered with the same name.
func (b *Bot) RegisterCommands(commands []*cmd.Command) error {
for _, c := range commands {
if b.Commands[c.Name] != nil {
return fmt.Errorf("multiple commands registered with name %s", c.Name)
}
b.Commands[c.Name] = c
b.Log.Infof("registered command %s", c.Name)
}
return nil
}
// Start causes an already initialized bot instance to begin listening for
// and responding to commands sent on its Slack channel.
func (b *Bot) Start() {
go b.rtm.ManageConnection()
for evt := range b.rtm.IncomingEvents {
// Call any registered event handlers that are expecting events of this
// type.
if handlers := b.handlers[evt.Type]; handlers != nil {
for _, handler := range handlers {
handler(evt)
}
}
}
}
// UpdateUsers retrieves list of users from API, populates the bot
// instance's cache, and updates any member entries in the DB with any relevant
// info from their Slack profiles. It will also remove any users whose accounts
// have been deleted.
func (b *Bot) UpdateUsers() {
users, err := b.API.GetUsers()
if err != nil {
b.Log.WithError(err).Error("Failed to populate users")
}
b.Users = make(map[string]slack.User)
for _, u := range users {
member := &model.Member{
SlackID: u.ID,
Name: u.Profile.RealName,
IsAdmin: u.IsAdmin,
Email: u.Profile.Email,
Position: u.Profile.Title,
}
// Delete the member from GitHub and the DB if they've been deleted from Slack
if u.Deleted {
// If the user has their GitHub username set, try remove them from
// the GitHub organization
m := &model.Member{
SlackID: u.ID,
}
b.DAL.GetMemberBySlackID(m)
if m.GithubUsername != "" {
if err := b.GitHub.RemoveUserFromOrg(m.GithubUsername); err != nil {
b.Log.WithError(err).Errorf(
"failed to remove %s from ubclaunchpad org on GitHub", m.GithubUsername)
} else {
b.Log.Debugf("removed %s from ubclaunchpad org on GitHub", m.GithubUsername)
}
}
// Delete the user from the DB
if err := b.DAL.DeleteMember(member); err != nil {
b.Log.WithError(err).Errorf("failed to delete member %s", member.Name)
} else {
b.Log.Debugf("deleted member %s", member.Name)
}
continue
}
// Update the member in the DB and add them to the cache
b.Users[u.ID] = u
if err := b.DAL.UpdateMember(member); err != nil {
b.Log.WithError(err).Error("failed to update member " + member.Name)
}
b.Log.Debugf("successfully updated user %s", member.Name)
}
}
// SendErrorMessage sends a generic error message back to the sender and
// logs the specific error that occurred.
func (b *Bot) SendErrorMessage(channel string, err error, msg string) {
errorMsg := errorMessage
if len(msg) > 0 {
errorMsg = msg
}
b.API.PostMessage(channel, errorMsg, noParams)
b.Log.WithError(err).Error(msg)
}
// handleMessageEvent is a generic handler for any new message we receive.
// Determines whether the message is meant to be a command (if we need to
// take action for it), populates the command context object for the message,
// and calls the appropriate handler.
func (b *Bot) handleMessageEvent(evt slack.RTMEvent) {
msg := evt.Data.(*slack.MessageEvent).Msg
b.Log.WithFields(log.Fields{
"Text": msg.Text,
"Channel": msg.Channel,
"User": msg.User,
})
// Ignore messages from bots
if len(msg.User) == 0 {
return
}
member := model.Member{
SlackID: msg.User,
ImageURL: b.Users[msg.User].Profile.Image192,
}
// Create member if doesn't already exist (this acts like an upsert)
if err := b.DAL.CreateMember(&member); err != nil {
b.Log.WithError(err).Errorf("Error creating member with Slack ID %s", member.SlackID)
b.API.PostMessage(msg.Channel, errorMessage, noParams)
return
}
// Set member image to their slack profile image
if err := b.DAL.SetMemberImageURL(&member); err != nil {
b.Log.WithError(err).Errorf("Error setting member image URL")
b.API.PostMessage(msg.Channel, errorMessage, noParams)
return
}
// Retrieves the full member object from the database
if err := b.DAL.GetMemberBySlackID(&member); err != nil {
b.Log.WithError(err).Errorf("Error getting member by Slack ID %s", member.SlackID)
b.API.PostMessage(msg.Channel, errorMessage, noParams)
return
}
args := strings.Fields(msg.Text)
if len(args) == 0 {
return
}
// A command is defined by being prefixed by our username
// i.e. "@rocket <command> <arg1> ..."
if args[0] == cmd.ToMention(username) {
context := cmd.Context{
Message: &msg,
User: member,
}
var cmd *cmd.Command
if len(args) > 1 {
command := args[1]
cmd = b.Commands[command]
if cmd == nil {
cmd = b.Commands["help"]
}
} else {
cmd = b.Commands["help"]
}
res, params, err := cmd.Execute(context)
if err != nil {
log.WithError(err).Error("Failed to execute command")
b.SendErrorMessage(context.Message.Channel, err, err.Error())
}
b.API.PostMessage(context.Message.Channel, res, params)
}
}
// Handler for when a user changes their profile, or a user is added/deleted.
// Creates the member if they don't already exist and sets their profile image.
func (b *Bot) handleUserChange(evt slack.RTMEvent) {
var user slack.User
// This function is only called for team join or user change events, so
// check which case we are in before proceeding.
if evt.Type == "team_join" {
user = evt.Data.(*slack.TeamJoinEvent).User
} else {
user = evt.Data.(*slack.UserChangeEvent).User
}
b.Users[user.ID] = user
member := model.Member{
SlackID: user.ID,
ImageURL: user.Profile.Image192,
}
// Create user if doesn't exist
if err := b.DAL.CreateMember(&member); err != nil {
b.Log.WithError(err).Errorf("Error creating user with Slack ID %s", member.SlackID)
}
// Update image URL
if err := b.DAL.SetMemberImageURL(&member); err != nil {
b.Log.WithError(err).Errorf("Error setting image URL for Slack ID %s", member.SlackID)
}
}