@hubot-friends/hubot-slack 0.0.0-development

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.github/CODE_OF_CONDUCT.md +11 -0
  2. package/.github/contributing.md +60 -0
  3. package/.github/issue_template.md +48 -0
  4. package/.github/maintainers_guide.md +91 -0
  5. package/.github/pull_request_template.md +8 -0
  6. package/.github/workflows/ci-build.yml +74 -0
  7. package/LICENSE +22 -0
  8. package/README.md +29 -0
  9. package/docs/Gemfile +2 -0
  10. package/docs/README.md +15 -0
  11. package/docs/_config.yml +29 -0
  12. package/docs/_includes/analytics.html +7 -0
  13. package/docs/_includes/head.html +33 -0
  14. package/docs/_includes/page_header.html +20 -0
  15. package/docs/_includes/side_nav.html +22 -0
  16. package/docs/_includes/tag_manager.html +13 -0
  17. package/docs/_layouts/changelog.html +15 -0
  18. package/docs/_layouts/default.html +47 -0
  19. package/docs/_layouts/page.html +10 -0
  20. package/docs/_pages/FAQ.md +63 -0
  21. package/docs/_pages/about.md +16 -0
  22. package/docs/_pages/advanced_usage.md +102 -0
  23. package/docs/_pages/auth.md +44 -0
  24. package/docs/_pages/basic_usage.md +302 -0
  25. package/docs/_pages/changelog.html +17 -0
  26. package/docs/_pages/upgrading.md +49 -0
  27. package/docs/_posts/2016-07-15-v4.0.0.md +14 -0
  28. package/docs/_posts/2016-07-19-v4.0.1.md +5 -0
  29. package/docs/_posts/2016-08-03-v4.0.2.md +4 -0
  30. package/docs/_posts/2016-09-12-v4.0.3.md +8 -0
  31. package/docs/_posts/2016-09-12-v4.0.4.md +4 -0
  32. package/docs/_posts/2016-09-14-v4.0.5.md +4 -0
  33. package/docs/_posts/2016-09-21-v4.1.0.md +4 -0
  34. package/docs/_posts/2016-10-12-v4.2.0.md +4 -0
  35. package/docs/_posts/2016-10-12-v4.2.1.md +4 -0
  36. package/docs/_posts/2016-11-05-v4.2.2.md +8 -0
  37. package/docs/_posts/2017-01-05-v4.3.0.md +5 -0
  38. package/docs/_posts/2017-01-09-v4.3.1.md +5 -0
  39. package/docs/_posts/2017-02-15-v4.3.2.md +5 -0
  40. package/docs/_posts/2017-02-17-v4.3.3.md +4 -0
  41. package/docs/_posts/2017-03-29-v4.3.4.md +5 -0
  42. package/docs/_posts/2017-08-24-v4.4.0.md +7 -0
  43. package/docs/_posts/2018-06-08-v4.5.0.md +13 -0
  44. package/docs/_posts/2018-06-14-v4.5.1.md +4 -0
  45. package/docs/_posts/2018-07-03-v4.5.2.md +5 -0
  46. package/docs/_posts/2018-07-17-v4.5.3.md +12 -0
  47. package/docs/_posts/2018-08-10-v4.5.4.md +7 -0
  48. package/docs/_posts/2018-10-01-v4.5.5.md +7 -0
  49. package/docs/_posts/2018-12-21-v4.6.0.md +6 -0
  50. package/docs/_posts/2019-04-29-v4.7.0.md +6 -0
  51. package/docs/_posts/2019-04-30-v4.7.1.md +5 -0
  52. package/docs/_posts/2020-04-03-v4.7.2.md +6 -0
  53. package/docs/_posts/2020-05-19-v4.8.0.md +10 -0
  54. package/docs/_posts/2020-10-19-v4.8.1.md +7 -0
  55. package/docs/_posts/2021-01-26-v4.9.0.md +8 -0
  56. package/docs/_posts/2022-01-12-v4.10.0.md +8 -0
  57. package/docs/index.md +93 -0
  58. package/docs/styles/docs.css +37 -0
  59. package/package.json +50 -0
  60. package/slack.js +20 -0
  61. package/src/SlackAdapter.mjs +302 -0
  62. package/src/SlackAdapter.test.mjs +342 -0
  63. package/src/bot.js +526 -0
  64. package/src/client.js +445 -0
  65. package/src/extensions.js +82 -0
  66. package/src/mention.js +15 -0
  67. package/src/message.js +307 -0
  68. package/src/testing.mjs +24 -0
  69. package/test/bot.js +769 -0
  70. package/test/client.js +446 -0
  71. package/test/message.js +329 -0
  72. package/test/stubs.js +388 -0
package/src/bot.js ADDED
@@ -0,0 +1,526 @@
1
+ const {Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage, User} = require.main.require("hubot/es2015.js");
2
+ const {SlackTextMessage, ReactionMessage, FileSharedMessage, MeMessage} = require("./message");
3
+
4
+ const SocketModeClient = require('@slack/socket-mode').SocketModeClient;
5
+ const WebClient = require('@slack/web-api').WebClient;
6
+
7
+ const pkg = require("../package.json");
8
+
9
+ class SlackClient {
10
+ static CONVERSATION_CACHE_TTL_MS =
11
+ process.env.HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS
12
+ ? parseInt(process.env.HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS, 10)
13
+ : (5 * 60 * 1000);
14
+ constructor(options, robot) {
15
+ this.robot = robot;
16
+ this.socket = new SocketModeClient({ appToken: options.appToken, ...options.socketModeOptions });
17
+ this.web = new WebClient(options.botToken, { maxRequestConcurrency: 1, logLevel: 'error'});
18
+ this.apiPageSize = 100;
19
+ if (!isNaN(options.apiPageSize)) {
20
+ this.apiPageSize = parseInt(options.apiPageSize, 10);
21
+ }
22
+
23
+ this.robot.logger.debug(`SocketModeClient initialized with options: ${JSON.stringify(options.socketModeOptions)}`);
24
+
25
+ // Map to convert bot user IDs (BXXXXXXXX) to user representations for events from custom
26
+ // integrations and apps without a bot user
27
+ this.botUserIdMap = {
28
+ "B01": { id: "B01", user_id: "USLACKBOT" }
29
+ };
30
+
31
+ // Map to convert conversation IDs to conversation representations
32
+ this.channelData = {};
33
+
34
+ // Event handling
35
+ // NOTE: add channel join and leave events
36
+ this.socket.on('authenticated', this.eventWrapper, this);
37
+ this.socket.on("message", this.eventWrapper, this);
38
+ this.socket.on("reaction_added", this.eventWrapper, this);
39
+ this.socket.on("reaction_removed", this.eventWrapper, this);
40
+ this.socket.on("member_joined_channel", this.eventWrapper, this);
41
+ this.socket.on("member_left_channel", this.eventWrapper, this);
42
+ this.socket.on("file_shared", this.eventWrapper, this);
43
+ this.socket.on("user_change", this.updateUserInBrain, this);
44
+ this.eventHandler = undefined;
45
+ }
46
+
47
+ async connect() {
48
+ this.robot.logger.debug(`Calling SocketModeClient#start()`);
49
+ const response = await this.socket.start();
50
+ return response;
51
+ }
52
+ onEvent(callback) {
53
+ if (this.eventHandler !== callback) { return this.eventHandler = callback; }
54
+ }
55
+
56
+ disconnect() {
57
+ this.socket.disconnect();
58
+ return this.socket.removeAllListeners();
59
+ }
60
+
61
+ setTopic(conversationId, topic) {
62
+ this.robot.logger.debug(`SlackClient#setTopic() with topic ${topic}`);
63
+ return this.web.conversations.info(conversationId)
64
+ .then(res => {
65
+ const conversation = res.channel;
66
+ if (!conversation.is_im && !conversation.is_mpim) {
67
+ return this.web.conversations.setTopic(conversationId, topic);
68
+ } else {
69
+ return this.robot.logger.debug(`Conversation ${conversationId} is a DM or MPDM. ` +
70
+ "These conversation types do not have topics."
71
+ );
72
+ }
73
+ }).catch(error => {
74
+ return this.robot.logger.error(`Error setting topic in conversation ${conversationId}: ${error.message}`);
75
+ });
76
+ }
77
+ send(envelope, message) {
78
+ const room = envelope.room || envelope.id;
79
+ if (room == null) {
80
+ this.robot.logger.error("Cannot send message without a valid room. Envelopes should contain a room property set to a Slack conversation ID.");
81
+ return;
82
+ }
83
+ this.robot.logger.debug(`SlackClient#send() room: ${room}, message: ${message}`);
84
+ if (typeof message !== "string") {
85
+ return this.web.chat.postMessage({ channel: room, text: message.text }).then(result => {
86
+ this.robot.logger.debug(`Successfully sent message to ${room}`)
87
+ }).catch(e => this.robot.logger.error(`SlackClient#send(message) error: ${e.message}`))
88
+ } else {
89
+ return this.web.chat.postMessage({ channel: room, text: message }).then(result => {
90
+ this.robot.logger.debug(`Successfully sent message (string) to ${room}`)
91
+ }).catch(e => this.robot.logger.error(`SlackClient#send(string) error: ${e.message}`))
92
+ }
93
+ }
94
+ loadUsers(callback) {
95
+ const combinedResults = { members: [] };
96
+ var pageLoaded = (error, results) => {
97
+ if (error) {
98
+ return callback(error);
99
+ }
100
+
101
+ for (var member of results.members) {
102
+ combinedResults.members.push(member);
103
+ }
104
+
105
+ if(results?.response_metadata?.next_cursor) {
106
+ return this.web.users.list({
107
+ limit: this.apiPageSize,
108
+ cursor: results.response_metadata.next_cursor
109
+ }, pageLoaded);
110
+ } else {
111
+ return callback(null, combinedResults);
112
+ }
113
+ };
114
+ return this.web.users.list({ limit: this.apiPageSize }, pageLoaded);
115
+ }
116
+
117
+ async fetchUser(userId) {
118
+ if (this.robot.brain.data.users[userId] != null) { return Promise.resolve(this.robot.brain.data.users[userId]); }
119
+ const r = await this.web.users.info(userId);
120
+ this.updateUserInBrain(r.user);
121
+ return r.user;
122
+ }
123
+ fetchConversation(conversationId) {
124
+ const expiration = Date.now() - SlackClient.CONVERSATION_CACHE_TTL_MS;
125
+ if (((this.channelData[conversationId] != null ? this.channelData[conversationId].channel : undefined) != null) &&
126
+ (expiration < (this.channelData[conversationId] != null ? this.channelData[conversationId].updated : undefined))) { return Promise.resolve(this.channelData[conversationId].channel); }
127
+ if (this.channelData[conversationId] != null) { delete this.channelData[conversationId]; }
128
+ return this.web.conversations.info(conversationId).then(r => {
129
+ if (r.channel != null) {
130
+ this.channelData[conversationId] = {
131
+ channel: r.channel,
132
+ updated: Date.now()
133
+ };
134
+ }
135
+ return r.channel;
136
+ });
137
+ }
138
+ updateUserInBrain(event_or_user) {
139
+ let key, value;
140
+ const user = event_or_user.type === 'user_change' ? event_or_user.user : event_or_user;
141
+ const newUser = {
142
+ id: user.id,
143
+ name: user.name,
144
+ real_name: user.real_name,
145
+ slack: {}
146
+ };
147
+ if ((user.profile != null ? user.profile.email : undefined) != null) { newUser.email_address = user.profile.email; }
148
+ for (key in user) {
149
+ value = user[key];
150
+ newUser.slack[key] = value;
151
+ }
152
+ if (user.id in this.robot.brain.data.users) {
153
+ for (key in this.robot.brain.data.users[user.id]) {
154
+ value = this.robot.brain.data.users[user.id][key];
155
+ if (!(key in newUser)) {
156
+ newUser[key] = value;
157
+ }
158
+ }
159
+ }
160
+ delete this.robot.brain.data.users[user.id];
161
+ return this.robot.brain.userForId(user.id, newUser);
162
+ }
163
+ async eventWrapper(event) {
164
+ if(!this.eventHandler) return;
165
+ try {
166
+ await this.eventHandler(event);
167
+ } catch (error) {
168
+ console.trace(error);
169
+ this.robot.logger.error(`An error occurred while processing an event from SlackBot's SlackClient: ${error.message}.`);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (SlackClient.CONVERSATION_CACHE_TTL_MS === NaN) {
175
+ throw new Error('HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS must be a number. It could not be parsed.');
176
+ }
177
+
178
+ class SlackBot extends Adapter {
179
+ constructor(robot, options) {
180
+ super(robot);
181
+ this.options = options;
182
+ this.robot.logger.info(`hubot-slack adapter v${pkg.version}`);
183
+ this.client = new SlackClient(this.options, this.robot);
184
+ }
185
+ async run() {
186
+ if (!this.options.botToken) {
187
+ return this.robot.logger.error("No botToken provided to Hubot");
188
+ }
189
+
190
+ if (!this.options.appToken) {
191
+ return this.robot.logger.error("No appToken provided to Hubot");
192
+ }
193
+ const needle = this.options.botToken.substring(0, 5);
194
+ if (!["xoxb-", "xoxp-"].includes(needle)) {
195
+ return this.robot.logger.error("Invalid botToken provided, please follow the upgrade instructions");
196
+ }
197
+
198
+ if (!["xapp-"].includes(this.options.appToken.substring(0, 5))) {
199
+ return this.robot.logger.error("Invalid appToken provided, please follow the upgrade instructions");
200
+ }
201
+
202
+ this.client.socket.on("open", this.open.bind(this));
203
+ this.client.socket.on("close", this.close.bind(this));
204
+ this.client.socket.on("disconnect", this.disconnect.bind(this));
205
+ this.client.socket.on("error", this.error.bind(this));
206
+ this.client.socket.on("authenticated", this.authenticated.bind(this));
207
+ this.client.onEvent(this.eventHandler.bind(this));
208
+
209
+ // Brain will emit 'loaded' the first time it connects to its storage and then again each time a key is set
210
+ this.robot.brain.on("loaded", () => {
211
+ if(this.brainIsLoaded) return;
212
+ this.brainIsLoaded = true;
213
+ // The following code should only run after the first time the brain connects to its storage
214
+
215
+ // There's a race condition where the connection can happen after the above `@client.loadUsers` call finishes,
216
+ // in which case the calls to save users in `@usersLoaded` would not persist. It is still necessary to call the
217
+ // method there in the case Hubot is running without brain storage.
218
+ // NOTE: is this actually true? won't the brain have the users in memory and persist to storage as soon as the
219
+ // connection is complete?
220
+ // NOTE: this seems wasteful. when there is brain storage, it will end up loading all the users twice.
221
+ this.client.loadUsers(this.usersLoaded.bind(this));
222
+ this.isLoaded = true;
223
+ });
224
+
225
+ // TODO: set this to false as soon as connection closes (even if reconnect will happen later)
226
+ // TODO: check this value when connection finishes (even if its a reconnection)
227
+ // TODO: build a map of enterprise users and local users
228
+ this.needsUserListSync = true;
229
+ if (!this.options.disableUserSync) {
230
+ // Synchronize workspace users to brain
231
+ this.client.loadUsers(this.usersLoaded.bind(this));
232
+ } else {
233
+ this.brainIsLoaded = true;
234
+ }
235
+
236
+ // Start logging in
237
+ await this.client.connect()
238
+ this.robot.logger.info("Connected to Slack on run");
239
+ this.emit('connected');
240
+ }
241
+
242
+ /**
243
+ * Hubot is sending a message to Slack
244
+ *
245
+ * @public
246
+ * @param {Object} envelope - fully documented in SlackClient
247
+ * @param {...(string|Object)} messages - fully documented in SlackClient
248
+ */
249
+ send(envelope, ...messages) {
250
+ this.robot.logger.debug('Sending message to Slack');
251
+ let callback = function() {};
252
+ if (typeof(messages[messages.length - 1]) === "function") {
253
+ callback = messages.pop();
254
+ }
255
+ const messagePromises = messages.map(message => {
256
+ if (typeof(message) === "function") { return Promise.resolve(); }
257
+ // NOTE: perhaps do envelope manipulation here instead of in the client (separation of concerns)
258
+ if (message !== "") { return this.client.send(envelope, message); }
259
+ });
260
+ return Promise.all(messagePromises).then(callback.bind(null, null), callback);
261
+ }
262
+
263
+ /**
264
+ * Hubot is replying to a Slack message
265
+ * @public
266
+ * @param {Object} envelope - fully documented in SlackClient
267
+ * @param {...(string|Object)} messages - fully documented in SlackClient
268
+ */
269
+ reply(envelope, ...messages) {
270
+ this.robot.logger.debug('replying to message');
271
+ let callback = function() {};
272
+ if (typeof(messages[messages.length - 1]) === "function") {
273
+ callback = messages.pop();
274
+ }
275
+ const messagePromises = messages.map(message => {
276
+ if (typeof(message) === "function") {
277
+ return Promise.resolve();
278
+ }
279
+
280
+ if (message !== "") {
281
+ // TODO: channel prefix matching should be removed
282
+ if (envelope.room[0] !== "D") { message = `<@${envelope.user.id}>: ${message}`; }
283
+ return this.client.send(envelope, message);
284
+ }
285
+ });
286
+ return Promise.all(messagePromises).then(callback.bind(null, null), callback);
287
+ }
288
+
289
+ /**
290
+ * Hubot is setting the Slack conversation topic
291
+ * @public
292
+ * @param {Object} envelope - fully documented in SlackClient
293
+ * @param {...string} strings - strings that will be newline separated and set to the conversation topic
294
+ */
295
+ setTopic(envelope, ...strings) {
296
+ // TODO: if the sender is interested in the completion, the last item in `messages` will be a function
297
+ // TODO: this will fail if sending an object as a value in strings
298
+ return this.client.setTopic(envelope.room, strings.join("\n"));
299
+ }
300
+
301
+ /**
302
+ * Hubot is sending a reaction
303
+ * NOTE: the super class implementation is just an alias for send, but potentially, we can detect
304
+ * if the envelope has a specific message and send a reactji. the fallback would be to just send the
305
+ * emoji as a message in the channel
306
+ */
307
+ // emote: (envelope, strings...) ->
308
+
309
+
310
+ /**
311
+ * Slack client has opened the connection
312
+ * @private
313
+ */
314
+ open() {
315
+ this.robot.logger.info("Connected to Slack Socket");
316
+
317
+ // Tell Hubot we're connected so it can load scripts
318
+ return this.emit("connected");
319
+ }
320
+
321
+ /**
322
+ * Slack client has authenticated
323
+ *
324
+ * @private
325
+ * @param identity - the response from calling the Slack Web API method
326
+ */
327
+ async authenticated(identity) {
328
+ if(this.self) return;
329
+ this.self = await this.client.web.auth.test();
330
+ this.robot.logger.debug(this.self);
331
+ this.robot.name = this.self.user;
332
+ return this.robot.logger.info(`Logged in as @${this.robot.name} in workspace ${this.self.team}`);
333
+ }
334
+
335
+ /**
336
+ * Slack client has closed the connection
337
+ * @private
338
+ */
339
+ close() {
340
+ // NOTE: not confident that @options.autoReconnect works
341
+ if (this.options.autoReconnect) {
342
+ this.robot.logger.info("Disconnected from Slack Socket");
343
+ return this.robot.logger.info("Waiting for reconnect...");
344
+ } else {
345
+ return this.disconnect();
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Slack client has closed the connection and will not reconnect
351
+ * @private
352
+ */
353
+ disconnect() {
354
+ this.robot.logger.info("Disconnected from Slack Socket");
355
+ this.robot.logger.info("Exiting...");
356
+ this.client.disconnect();
357
+ // NOTE: Node recommends not to call process.exit() but Hubot itself uses this mechanism for shutting down
358
+ // Can we make sure the brain is flushed to persistence? Do we need to cleanup any state (or timestamp anything)?
359
+ return process.exit(1);
360
+ }
361
+
362
+ /**
363
+ * Slack client received an error
364
+ *
365
+ * @private
366
+ * @param error - An error emitted
367
+ */
368
+ error(error) {
369
+ this.robot.logger.error(`SlackBot error: ${JSON.stringify(error)}`);
370
+ // Assume that scripts can handle slowing themselves down, all other errors are bubbled up through Hubot
371
+ // NOTE: should rate limit errors also bubble up?
372
+ if (error.code !== -1) {
373
+ return this.robot.emit("error", error);
374
+ }
375
+ }
376
+
377
+ replaceBotIdWithName(event) {
378
+ const botId = this.self.user_id;
379
+ const botName = this.self.user;
380
+ const text = event.text ?? event.message?.text ?? '';
381
+ if(text.includes(`<@${botId}>`)) {
382
+ return text.replace(`<@${botId}>`, `@${botName}`);
383
+ }
384
+ return text;
385
+ }
386
+ addBotIdToMessage(event) {
387
+ let text = event.text ?? event.message?.text ?? '';
388
+ if (text && event?.channel_type == 'im' && !text.includes(this.self.user_id)) {
389
+ text = `<@${this.self.user_id}> ${text}`;
390
+ }
391
+ return text;
392
+ }
393
+ /**
394
+ * Incoming Slack event handler
395
+ *
396
+ * This method is used to ingest Slack events and prepare them as Hubot Message objects. The messages are passed
397
+ * to the Robot#receive() method, which allows executes receive middleware and eventually triggers the various
398
+ * listeners that are created by scripts.
399
+ *
400
+ * Depending on the exact type of event, additional properties may be present. The following describes the "special"
401
+ * ones that have meaningful handling across all types.
402
+ *
403
+ * @private
404
+ * @param {Object} event
405
+ * @param {string} event.type - this specifies the event type
406
+ * @param {SlackUserInfo} [event.user] - the description of the user creating this event as returned by `users.info`
407
+ * @param {string} [event.channel] - the conversation ID for where this event took place
408
+ * @param {SlackBotInfo} [event.bot] - the description of the bot creating this event as returned by `bots.info`
409
+ */
410
+ async eventHandler(message) {
411
+ if(!message?.body?.event?.user) {
412
+ if (message?.ack) {
413
+ return await message?.ack();
414
+ }
415
+ return;
416
+ }
417
+
418
+ let msg;
419
+ const {user, channel} = message.event;
420
+ const event_team_id = message.event.team;
421
+
422
+ const userFromBrain = this.robot.brain.users()[user];
423
+ if (!userFromBrain) {
424
+ const userResponse = await this.client.web.users.info({
425
+ user
426
+ })
427
+ this.robot.brain.userForId(user, userResponse.user);
428
+ }
429
+
430
+ const from = this.robot.brain.users()[user];
431
+
432
+ // Ignore anything we sent
433
+ if (from?.id === this.self.user_id) {
434
+ return;
435
+ }
436
+
437
+ this.robot.logger.debug(`event ${JSON.stringify(message, null, 2)} user = ${user}`);
438
+
439
+ // TODO: I don't know what the schema looks like for this so i'm commenting it out
440
+ // until i can figure it out.
441
+ // if (this.options.installedTeamOnly) {
442
+ // // Skip events generated by other workspace users in a shared channel
443
+ // if ((event_team_id != null) && (event_team_id !== this.self.installed_team_id)) {
444
+ // this.robot.logger.debug(`Skipped an event generated by an other workspace user (team: ${event_team_id}) in shared channel (channel: ${channel})`);
445
+ // return await message?.ack();
446
+ // }
447
+ // }
448
+
449
+
450
+ // Hubot expects all user objects to have a room property that is used in the envelope for the message after it
451
+ // is received
452
+ from.room = channel ?? '';
453
+ from.name = from.profile.display_name;
454
+
455
+ // add the bot id to the message if it's a direct message
456
+
457
+ message.body.event.text = this.addBotIdToMessage(message.body.event);
458
+ message.body.event.text = this.replaceBotIdWithName(message.body.event);
459
+ this.robot.logger.debug(`Text = ${message.body.event.text}`);
460
+ try {
461
+ switch (message.event.type) {
462
+ case "member_joined_channel":
463
+ // this event type always has a channel
464
+ this.robot.logger.debug(`Received enter message for user: ${from.id}, joining: ${channel}`);
465
+ msg = new EnterMessage(from);
466
+ msg.ts = message.event.ts;
467
+ this.receive(msg);
468
+ break;
469
+ case "member_left_channel":
470
+ this.robot.logger.debug(`Received leave message for user: ${from.id}, joining: ${channel}`);
471
+ msg = new LeaveMessage(user);
472
+ msg.ts = message.ts;
473
+ this.receive(msg);
474
+ break;
475
+ case "reaction_added": case "reaction_removed":
476
+ // Once again Hubot expects all user objects to have a room property that is used in the envelope for the message
477
+ // after it is received. If the reaction is to a message, then the `event.item.channel` contain a conversation ID.
478
+ // Otherwise reactions can be on files and file comments, which are "global" and aren't contained in a
479
+ // conversation. In that situation we fallback to an empty string.
480
+ from.room = message.body.event.item.type === "message" ? message.body.event.item.channel : "";
481
+
482
+ // Reaction messages may contain an `event.item_user` property containing a fetched SlackUserInfo object. Before
483
+ // the message is received by Hubot, turn that data into a Hubot User object.
484
+ const item_user = (message.body.event.item_user != null) ? this.robot.brain.userForId(message.body.event.item_user.id, message.body.event.item_user) : {};
485
+
486
+ this.robot.logger.debug(`Received reaction message from: ${from.id}, reaction: ${message.body.event.reaction}, item type: ${message.body.event.item.type}`);
487
+ this.receive(new ReactionMessage(message.body.event.type, from, message.body.event.reaction, item_user, message.body.event.item, message.body.event.event_ts));
488
+ break;
489
+ case "file_shared":
490
+ this.robot.logger.debug(`Received file_shared message from: ${message.body.event.user_id}, file_id: ${message.body.event.file_id}`);
491
+ this.receive(new FileSharedMessage(from, message.body.event.file_id, message.body.event.event_ts));
492
+ break;
493
+ default:
494
+ this.robot.logger.debug(`Received generic message: ${message.event.type}`);
495
+ SlackTextMessage.makeSlackTextMessage(from, null, message?.body?.event.text, message?.body?.event, channel, this.robot.name, this.robot.alias, this.client, (error, message) => {
496
+ if (error) { return this.robot.logger.error(`Dropping message due to error ${error.message}`); }
497
+ return this.receive(message);
498
+ });
499
+ break;
500
+ }
501
+ } catch (e) {
502
+ this.robot.logger.error(e);
503
+ }
504
+
505
+ if (message?.ack) {
506
+ return await message?.ack();
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Callback for fetching all users in workspace. Delegates to `updateUserInBrain()` to write all users to Hubot brain
512
+ *
513
+ * @private
514
+ * @param {Error} [error] - describes an error that occurred while fetching users
515
+ * @param {SlackUsersList} [res] - the response from the Slack Web API method `users.list`
516
+ */
517
+ usersLoaded(err, res) {
518
+ if (err || !res.members.length) {
519
+ this.robot.logger.error("Can't fetch users");
520
+ return;
521
+ }
522
+ return res.members.map((member) => this.client.updateUserInBrain(member));
523
+ }
524
+ }
525
+
526
+ module.exports = SlackBot;