@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.
- package/.github/CODE_OF_CONDUCT.md +11 -0
- package/.github/contributing.md +60 -0
- package/.github/issue_template.md +48 -0
- package/.github/maintainers_guide.md +91 -0
- package/.github/pull_request_template.md +8 -0
- package/.github/workflows/ci-build.yml +74 -0
- package/LICENSE +22 -0
- package/README.md +29 -0
- package/docs/Gemfile +2 -0
- package/docs/README.md +15 -0
- package/docs/_config.yml +29 -0
- package/docs/_includes/analytics.html +7 -0
- package/docs/_includes/head.html +33 -0
- package/docs/_includes/page_header.html +20 -0
- package/docs/_includes/side_nav.html +22 -0
- package/docs/_includes/tag_manager.html +13 -0
- package/docs/_layouts/changelog.html +15 -0
- package/docs/_layouts/default.html +47 -0
- package/docs/_layouts/page.html +10 -0
- package/docs/_pages/FAQ.md +63 -0
- package/docs/_pages/about.md +16 -0
- package/docs/_pages/advanced_usage.md +102 -0
- package/docs/_pages/auth.md +44 -0
- package/docs/_pages/basic_usage.md +302 -0
- package/docs/_pages/changelog.html +17 -0
- package/docs/_pages/upgrading.md +49 -0
- package/docs/_posts/2016-07-15-v4.0.0.md +14 -0
- package/docs/_posts/2016-07-19-v4.0.1.md +5 -0
- package/docs/_posts/2016-08-03-v4.0.2.md +4 -0
- package/docs/_posts/2016-09-12-v4.0.3.md +8 -0
- package/docs/_posts/2016-09-12-v4.0.4.md +4 -0
- package/docs/_posts/2016-09-14-v4.0.5.md +4 -0
- package/docs/_posts/2016-09-21-v4.1.0.md +4 -0
- package/docs/_posts/2016-10-12-v4.2.0.md +4 -0
- package/docs/_posts/2016-10-12-v4.2.1.md +4 -0
- package/docs/_posts/2016-11-05-v4.2.2.md +8 -0
- package/docs/_posts/2017-01-05-v4.3.0.md +5 -0
- package/docs/_posts/2017-01-09-v4.3.1.md +5 -0
- package/docs/_posts/2017-02-15-v4.3.2.md +5 -0
- package/docs/_posts/2017-02-17-v4.3.3.md +4 -0
- package/docs/_posts/2017-03-29-v4.3.4.md +5 -0
- package/docs/_posts/2017-08-24-v4.4.0.md +7 -0
- package/docs/_posts/2018-06-08-v4.5.0.md +13 -0
- package/docs/_posts/2018-06-14-v4.5.1.md +4 -0
- package/docs/_posts/2018-07-03-v4.5.2.md +5 -0
- package/docs/_posts/2018-07-17-v4.5.3.md +12 -0
- package/docs/_posts/2018-08-10-v4.5.4.md +7 -0
- package/docs/_posts/2018-10-01-v4.5.5.md +7 -0
- package/docs/_posts/2018-12-21-v4.6.0.md +6 -0
- package/docs/_posts/2019-04-29-v4.7.0.md +6 -0
- package/docs/_posts/2019-04-30-v4.7.1.md +5 -0
- package/docs/_posts/2020-04-03-v4.7.2.md +6 -0
- package/docs/_posts/2020-05-19-v4.8.0.md +10 -0
- package/docs/_posts/2020-10-19-v4.8.1.md +7 -0
- package/docs/_posts/2021-01-26-v4.9.0.md +8 -0
- package/docs/_posts/2022-01-12-v4.10.0.md +8 -0
- package/docs/index.md +93 -0
- package/docs/styles/docs.css +37 -0
- package/package.json +50 -0
- package/slack.js +20 -0
- package/src/SlackAdapter.mjs +302 -0
- package/src/SlackAdapter.test.mjs +342 -0
- package/src/bot.js +526 -0
- package/src/client.js +445 -0
- package/src/extensions.js +82 -0
- package/src/mention.js +15 -0
- package/src/message.js +307 -0
- package/src/testing.mjs +24 -0
- package/test/bot.js +769 -0
- package/test/client.js +446 -0
- package/test/message.js +329 -0
- 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;
|