@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/client.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const SocketModeClient = require('@slack/socket-mode').SocketModeClient;
|
|
3
|
+
const WebClient = require('@slack/web-api').WebClient;
|
|
4
|
+
|
|
5
|
+
class SlackClient extends EventEmitter {
|
|
6
|
+
/**
|
|
7
|
+
* Number of milliseconds which the information returned by `conversations.info` is considered to be valid. The default
|
|
8
|
+
* value is 5 minutes, and it can be customized by setting the `HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS` environment
|
|
9
|
+
* variable. Setting this number higher will reduce the number of requests made to the Web API, which may be helpful if
|
|
10
|
+
* your Hubot is experiencing rate limiting errors. However, setting this number too high will result in stale data
|
|
11
|
+
* being referenced, and your scripts may experience errors related to channel info (like the name) being incorrect
|
|
12
|
+
* after a user changes it in Slack.
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
15
|
+
static CONVERSATION_CACHE_TTL_MS =
|
|
16
|
+
process.env.HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS
|
|
17
|
+
? parseInt(process.env.HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS, 10)
|
|
18
|
+
: (5 * 60 * 1000);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {Object} options - Configuration options for this SlackClient instance
|
|
23
|
+
* @param {string} options.token - Slack API token for authentication
|
|
24
|
+
* @param {string} options.apiPageSize - Number used for limit when making paginated requests to Slack Web API list methods
|
|
25
|
+
* @param {Object} [options.socketModeOptions={}] - Configuration options for owned SocketModeClient instance
|
|
26
|
+
* @param {Robot} robot - Hubot robot instance
|
|
27
|
+
*/
|
|
28
|
+
constructor(options, robot) {
|
|
29
|
+
super();
|
|
30
|
+
this.robot = robot;
|
|
31
|
+
this.socket = new SocketModeClient({ appToken: options.appToken, ...options.socketModeOptions });
|
|
32
|
+
this.web = new WebClient(options.botToken, {
|
|
33
|
+
logger: robot.logger,
|
|
34
|
+
logLevel: options.logLevel ?? 'info',
|
|
35
|
+
maxRequestConcurrency: options?.maxRequestConcurrency ?? 1,
|
|
36
|
+
retryConfig: options?.retryConfig,
|
|
37
|
+
agent: options?.agent,
|
|
38
|
+
tls: options?.tls,
|
|
39
|
+
timeout: options?.timeout,
|
|
40
|
+
rejectRateLimitedCalls: options?.rejectRateLimitedCalls,
|
|
41
|
+
headers: options?.headers,
|
|
42
|
+
teamId: options?.teamId
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.apiPageSize = 100;
|
|
46
|
+
if (!isNaN(options.apiPageSize)) {
|
|
47
|
+
this.apiPageSize = parseInt(options.apiPageSize, 10);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.robot.logger.debug(`SocketModeClient initialized with options: ${JSON.stringify(options?.socketModeOptions)}`);
|
|
51
|
+
|
|
52
|
+
// Map to convert bot user IDs (BXXXXXXXX) to user representations for events from custom
|
|
53
|
+
// integrations and apps without a bot user
|
|
54
|
+
this.botUserIdMap = {
|
|
55
|
+
"B01": { id: "B01", user_id: "USLACKBOT" }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Map to convert conversation IDs to conversation representations
|
|
59
|
+
this.channelData = {};
|
|
60
|
+
|
|
61
|
+
// Event handling
|
|
62
|
+
// NOTE: add channel join and leave events
|
|
63
|
+
this.socket.on('authenticated', this.eventWrapper, this);
|
|
64
|
+
this.socket.on("message", this.eventWrapper, this);
|
|
65
|
+
this.socket.on("reaction_added", this.eventWrapper, this);
|
|
66
|
+
this.socket.on("reaction_removed", this.eventWrapper, this);
|
|
67
|
+
this.socket.on("member_joined_channel", this.eventWrapper, this);
|
|
68
|
+
this.socket.on("member_left_channel", this.eventWrapper, this);
|
|
69
|
+
this.socket.on("file_shared", this.eventWrapper, this);
|
|
70
|
+
this.socket.on("user_change", this.updateUserInBrain, this);
|
|
71
|
+
this.eventHandler = undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Open connection to the Slack Socket
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
async connect() {
|
|
80
|
+
this.robot.logger.debug(`SocketModeClient#start()`);
|
|
81
|
+
let startResponse = null;
|
|
82
|
+
try {
|
|
83
|
+
startResponse = await this.socket.start()
|
|
84
|
+
this.robot.logger.info(startResponse);
|
|
85
|
+
const response = await this.web.auth.test();
|
|
86
|
+
this.robot.self = response.user;
|
|
87
|
+
this.robot.logger.info('Connected to Slack after starting socket client.');
|
|
88
|
+
// this.emit('connected');
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.robot.logger.error(`Error connecting to Slack: ${e.message}`);
|
|
91
|
+
this.emit('error', e);
|
|
92
|
+
}
|
|
93
|
+
return startResponse;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set event handler
|
|
98
|
+
*
|
|
99
|
+
* @public
|
|
100
|
+
* @param {SlackClient~eventHandler} callback
|
|
101
|
+
*/
|
|
102
|
+
onEvent(callback) {
|
|
103
|
+
if (this.eventHandler !== callback) {
|
|
104
|
+
return this.eventHandler = callback;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Disconnect from the Slack Socket
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
disconnect() {
|
|
114
|
+
this.socket.disconnect();
|
|
115
|
+
// NOTE: removal of event listeners possibly does not belong in disconnect, because they are not added in connect.
|
|
116
|
+
return this.socket.removeAllListeners();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set a channel's topic
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
* @param {string} conversationId - Slack conversation ID
|
|
124
|
+
* @param {string} topic - new topic
|
|
125
|
+
*/
|
|
126
|
+
async setTopic(conversationId, topic) {
|
|
127
|
+
this.robot.logger.debug(`SlackClient#setTopic() with topic ${topic}`);
|
|
128
|
+
|
|
129
|
+
// The `conversations.info` method is used to find out if this conversation can have a topic set
|
|
130
|
+
// NOTE: There's a performance cost to making this request, which can be avoided if instead the attempt to set the
|
|
131
|
+
// topic is made regardless of the conversation type. If the conversation type is not compatible, the call would
|
|
132
|
+
// fail, which is exactly the outcome in this implementation.
|
|
133
|
+
try {
|
|
134
|
+
const res = await this.web.conversations.info(conversationId)
|
|
135
|
+
const conversation = res.channel;
|
|
136
|
+
if (!conversation.is_im && !conversation.is_mpim) {
|
|
137
|
+
return await this.web.conversations.setTopic(conversationId, topic);
|
|
138
|
+
} else {
|
|
139
|
+
return this.robot.logger.debug(`Conversation ${conversationId} is a DM or MPDM. These conversation types do not have topics.`);
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
this.robot.logger.error(`Error setting topic in conversation ${conversationId}: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Send a message to Slack using the Web API.
|
|
148
|
+
*
|
|
149
|
+
* This method is usually called when a Hubot script is sending a message in response to an incoming message. The
|
|
150
|
+
* response object has a `send()` method, which triggers execution of all response middleware, and ultimately calls
|
|
151
|
+
* `send()` on the Adapter. SlackBot, the adapter in this case, delegates that call to this method; once for every item
|
|
152
|
+
* (since its method signature is variadic). The `envelope` is created by the Hubot Response object.
|
|
153
|
+
*
|
|
154
|
+
* This method can also be called when a script directly calls `robot.send()` or `robot.adapter.send()`. That bypasses
|
|
155
|
+
* the execution of the response middleware and directly calls into SlackBot#send(). In this case, the `envelope`
|
|
156
|
+
* parameter is up to the script.
|
|
157
|
+
*
|
|
158
|
+
* The `envelope.room` property is intended to be a conversation ID. Even when that is not the case, this method will
|
|
159
|
+
* makes a reasonable attempt at sending the message. If the property is set to a public or private channel name, it
|
|
160
|
+
* will still work. When there's no `room` in the envelope, this method will fallback to the `id` property. That
|
|
161
|
+
* affordance allows scripts to use Hubot User objects, Slack users (as obtained from the response to `users.info`),
|
|
162
|
+
* and Slack conversations (as obtained from the response to `conversations.info`) as possible envelopes. In the first
|
|
163
|
+
* two cases, envelope.id` will contain a user ID (`Uxxx` or `Wxxx`). Since Hubot runs using a bot token (`xoxb`),
|
|
164
|
+
* passing a user ID as the `channel` argument to `chat.postMessage` (with `as_user=true`) results in a DM from the bot
|
|
165
|
+
* user (if `as_user=false` it would instead result in a DM from slackbot). Leaving `as_user=true` has no effect when
|
|
166
|
+
* the `channel` argument is a conversation ID.
|
|
167
|
+
*
|
|
168
|
+
* NOTE: This method no longer accepts `envelope.room` set to a user name. Using it in this manner will result in a
|
|
169
|
+
* `channel_not_found` error.
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
* @param {Object} envelope - a Hubot Response envelope
|
|
173
|
+
* @param {Message} [envelope.message] - the Hubot Message that was received and generated the Response which is now
|
|
174
|
+
* being used to send an outgoing message
|
|
175
|
+
* @param {User} [envelope.user] - the Hubot User object representing the user who sent `envelope.message`
|
|
176
|
+
* @param {string} [envelope.room] - a Slack conversation ID for where `envelope.message` was received, usually an
|
|
177
|
+
* alias of `envelope.user.room`
|
|
178
|
+
* @param {string} [envelope.id] - a Slack conversation ID similar to `envelope.room`
|
|
179
|
+
* @param {string|Object} message - the outgoing message to be sent, can be a simple string or a key/value object of
|
|
180
|
+
* optional arguments for the Slack Web API method `chat.postMessage`.
|
|
181
|
+
*/
|
|
182
|
+
send(envelope, message) {
|
|
183
|
+
const room = envelope.room || envelope.id;
|
|
184
|
+
if ((room == null)) {
|
|
185
|
+
this.robot.logger.error("Cannot send message without a valid room. Envelopes should contain a room property set to " +
|
|
186
|
+
"a Slack conversation ID."
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.robot.logger.debug(`SlackClient#send() room: ${room}, message: ${message}`);
|
|
192
|
+
|
|
193
|
+
const options = {
|
|
194
|
+
as_user: true,
|
|
195
|
+
link_names: 1,
|
|
196
|
+
// when the incoming message was inside a thread, send responses as replies to the thread
|
|
197
|
+
// NOTE: consider building a new (backwards-compatible) format for room which includes the thread_ts.
|
|
198
|
+
// e.g. "#{conversationId} #{thread_ts}" - this would allow a portable way to say the message is in a thread
|
|
199
|
+
thread_ts: (envelope.message != null ? envelope.message.thread_ts : undefined)
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (typeof message !== "string") {
|
|
203
|
+
return this.web.chat.postMessage({channel: room, text: message.text}, Object.assign(message, options))
|
|
204
|
+
.catch(error => {
|
|
205
|
+
return this.robot.logger.error(`SlackClient#send() error: ${error.message}`);
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
return this.web.chat.postMessage({channel: room, text: message.text}, options)
|
|
209
|
+
.catch(error => {
|
|
210
|
+
return this.robot.logger.error(`SlackClient#send() error: ${error.message}`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Fetch users from Slack API using pagination
|
|
217
|
+
*
|
|
218
|
+
* @public
|
|
219
|
+
* @param {SlackClient~usersCallback} callback
|
|
220
|
+
*/
|
|
221
|
+
loadUsers(callback) {
|
|
222
|
+
// some properties of the real results are left out because they are not used
|
|
223
|
+
const combinedResults = { members: [] };
|
|
224
|
+
var pageLoaded = (error, results) => {
|
|
225
|
+
if (error) { return callback(error); }
|
|
226
|
+
// merge results into combined results
|
|
227
|
+
for (var member of results.members) { combinedResults.members.push(member); }
|
|
228
|
+
|
|
229
|
+
if(results?.response_metadata?.next_cursor) {
|
|
230
|
+
// fetch next page
|
|
231
|
+
return this.web.users.list({
|
|
232
|
+
limit: this.apiPageSize,
|
|
233
|
+
cursor: results.response_metadata.next_cursor
|
|
234
|
+
}, pageLoaded);
|
|
235
|
+
} else {
|
|
236
|
+
// pagination complete, run callback with results
|
|
237
|
+
return callback(null, combinedResults);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return this.web.users.list({ limit: this.apiPageSize }, pageLoaded);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fetch user info from the brain. If not available, call users.info
|
|
245
|
+
* @public
|
|
246
|
+
*/
|
|
247
|
+
async fetchUser(userId) {
|
|
248
|
+
// User exists in the brain - retrieve this representation
|
|
249
|
+
if (this.robot.brain.data.users[userId] != null) {
|
|
250
|
+
return Promise.resolve(this.robot.brain.data.users[userId]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// User is not in brain - call users.info
|
|
254
|
+
// The user will be added to the brain in EventHandler
|
|
255
|
+
const r = await this.web.users.info({user: userId})
|
|
256
|
+
return this.updateUserInBrain(r.user);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Fetch bot user info from the bot -> user map
|
|
261
|
+
* @public
|
|
262
|
+
*/
|
|
263
|
+
async fetchBotUser(botId) {
|
|
264
|
+
if (this.botUserIdMap[botId] != null) {
|
|
265
|
+
return Promise.resolve(this.botUserIdMap[botId]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Bot user is not in mapping - call bots.info
|
|
269
|
+
this.robot.logger.debug(`SlackClient#fetchBotUser() Calling bots.info API for bot_id: ${botId}`);
|
|
270
|
+
const r = await this.web.bots.info({bot: botId})
|
|
271
|
+
return r.bot;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Fetch conversation info from conversation map. If not available, call conversations.info
|
|
276
|
+
* @public
|
|
277
|
+
*/
|
|
278
|
+
async fetchConversation(conversationId) {
|
|
279
|
+
// Current date minus time of expiration for conversation info
|
|
280
|
+
const expiration = Date.now() - SlackClient.CONVERSATION_CACHE_TTL_MS;
|
|
281
|
+
|
|
282
|
+
// Check whether conversation is held in client's channelData map and whether information is expired
|
|
283
|
+
if (((this.channelData[conversationId] != null ? this.channelData[conversationId].channel : undefined) != null) &&
|
|
284
|
+
(expiration < (this.channelData[conversationId] != null ? this.channelData[conversationId].updated : undefined))) { return Promise.resolve(this.channelData[conversationId].channel); }
|
|
285
|
+
|
|
286
|
+
// Delete data from map if it's expired
|
|
287
|
+
if (this.channelData[conversationId] != null) { delete this.channelData[conversationId]; }
|
|
288
|
+
|
|
289
|
+
// Return conversations.info promise
|
|
290
|
+
const r = await this.web.conversations.info(conversationId)
|
|
291
|
+
if (r.channel != null) {
|
|
292
|
+
this.channelData[conversationId] = {
|
|
293
|
+
channel: r.channel,
|
|
294
|
+
updated: Date.now()
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return r.channel;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Will return a Hubot user object in Brain.
|
|
302
|
+
* User can represent a Slack human user or bot user
|
|
303
|
+
*
|
|
304
|
+
* The returned user from a message or reaction event is guaranteed to contain:
|
|
305
|
+
*
|
|
306
|
+
* id {String}: Slack user ID
|
|
307
|
+
* slack.is_bot {Boolean}: Flag indicating whether user is a bot
|
|
308
|
+
* name {String}: Slack username
|
|
309
|
+
* real_name {String}: Name of Slack user or bot
|
|
310
|
+
* room {String}: Slack channel ID for event (will be empty string if no channel in event)
|
|
311
|
+
*
|
|
312
|
+
* This may be called as a handler for `user_change` events or to update a
|
|
313
|
+
* a single user with its latest SlackUserInfo object.
|
|
314
|
+
*
|
|
315
|
+
* @private
|
|
316
|
+
* @param {SlackUserInfo|SlackUserChangeEvent} event_or_user - an object containing information about a Slack user
|
|
317
|
+
* that should be updated in the brain
|
|
318
|
+
*/
|
|
319
|
+
updateUserInBrain(event_or_user) {
|
|
320
|
+
// if this method was invoked as a `user_change` event handler, unwrap the user from the event
|
|
321
|
+
let key, value;
|
|
322
|
+
const user = event_or_user.type === 'user_change' ? event_or_user.user : event_or_user;
|
|
323
|
+
|
|
324
|
+
// create a full representation of the user in the shape we persist for Hubot brain based on the parameter
|
|
325
|
+
// all top-level properties of the user are meant to be shared across adapters
|
|
326
|
+
const newUser = {
|
|
327
|
+
id: user.id,
|
|
328
|
+
name: user.name,
|
|
329
|
+
real_name: user.real_name,
|
|
330
|
+
slack: {}
|
|
331
|
+
};
|
|
332
|
+
// don't create keys for properties that have no value, because the empty value will become authoritative
|
|
333
|
+
if ((user.profile != null ? user.profile.email : undefined) != null) { newUser.email_address = user.profile.email; }
|
|
334
|
+
// all "non-standard" keys of a user are namespaced inside the slack property, so they don't interfere with other
|
|
335
|
+
// adapters (in case this hubot switched between adapters)
|
|
336
|
+
for (key in user) {
|
|
337
|
+
value = user[key];
|
|
338
|
+
newUser.slack[key] = value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// merge any existing representation of this user already stored in the brain into the new representation
|
|
342
|
+
if (user.id in this.robot.brain.data.users) {
|
|
343
|
+
for (key in this.robot.brain.data.users[user.id]) {
|
|
344
|
+
// the merge strategy is to only copy over data for keys that do not exist in the new representation
|
|
345
|
+
// this means the entire `slack` property is treated as one value
|
|
346
|
+
value = this.robot.brain.data.users[user.id][key];
|
|
347
|
+
if (!(key in newUser)) {
|
|
348
|
+
newUser[key] = value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// remove the existing representation and write the new representation to the brain
|
|
354
|
+
delete this.robot.brain.data.users[user.id];
|
|
355
|
+
return this.robot.brain.userForId(user.id, newUser);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Processes events to fetch additional data or rearrange the shape of an event before handing off to the eventHandler
|
|
360
|
+
*
|
|
361
|
+
* @private
|
|
362
|
+
* @param {SlackEvent} event - One of any of the events listed in <https://api.slack.com/events> with Events API enabled.
|
|
363
|
+
*/
|
|
364
|
+
eventWrapper(event) {
|
|
365
|
+
if (this.eventHandler) {
|
|
366
|
+
// fetch full representations of the user, bot, and potentially the item_user.
|
|
367
|
+
const fetches = [];
|
|
368
|
+
if (event.user) {
|
|
369
|
+
fetches.push(this.fetchUser(event.user));
|
|
370
|
+
} else if (event.bot_id) {
|
|
371
|
+
fetches.push(this.fetchBotUser(event.bot_id));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (event.item_user) {
|
|
375
|
+
fetches.push(this.fetchUser(event.item_user));
|
|
376
|
+
}
|
|
377
|
+
// after fetches complete...
|
|
378
|
+
return Promise.all(fetches)
|
|
379
|
+
.then(fetched => {
|
|
380
|
+
if (event.user) {
|
|
381
|
+
event.user = fetched.shift();
|
|
382
|
+
} else if (event.bot_id) {
|
|
383
|
+
let bot = fetched.shift();
|
|
384
|
+
if (this.botUserIdMap[event.bot_id]) {
|
|
385
|
+
event.user = bot;
|
|
386
|
+
// bot_id exists on all messages with subtype bot_message
|
|
387
|
+
// these messages only have a user_id property if sent from a bot user (xoxb token). therefore
|
|
388
|
+
// the above assignment will not happen for all messages from custom integrations or apps without a bot user
|
|
389
|
+
} else if (bot.user_id != null) {
|
|
390
|
+
return this.web.users.info({user: bot.user_id}).then(res => {
|
|
391
|
+
event.user = res.user;
|
|
392
|
+
this.botUserIdMap[event.bot_id] = res.user;
|
|
393
|
+
return event;
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
// bot doesn't have an associated user id
|
|
397
|
+
this.botUserIdMap[event.bot_id] = false;
|
|
398
|
+
event.user = {};
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
event.user = {};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (event.item_user) {
|
|
405
|
+
event.item_user = fetched.shift();
|
|
406
|
+
}
|
|
407
|
+
return event;
|
|
408
|
+
}).then(fetchedEvent => {
|
|
409
|
+
// hand the event off to the eventHandler
|
|
410
|
+
try {
|
|
411
|
+
this.eventHandler(fetchedEvent);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.robot.logger.error(`An error occurred while processing an event: from Slack Client ${error.message}.`);
|
|
415
|
+
}
|
|
416
|
+
}).catch(error => {
|
|
417
|
+
return this.robot.logger.error(`Incoming message dropped due to error fetching info for a property: ${error.message}.`);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* A handler for all incoming Slack events that are meaningful for the Adapter
|
|
425
|
+
*
|
|
426
|
+
* @callback SlackClient~eventHandler
|
|
427
|
+
* @param {Object} event
|
|
428
|
+
* @param {SlackUserInfo} event.user
|
|
429
|
+
* @param {string} event.channel
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Callback that recieves a list of users
|
|
434
|
+
*
|
|
435
|
+
* @callback SlackClient~usersCallback
|
|
436
|
+
* @param {Error|null} error - an error if one occurred
|
|
437
|
+
* @param {Object} results
|
|
438
|
+
* @param {Array<SlackUserInfo>} results.members
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
if (SlackClient.CONVERSATION_CACHE_TTL_MS === NaN) {
|
|
442
|
+
throw new Error('HUBOT_SLACK_CONVERSATION_CACHE_TTL_MS must be a number. It could not be parsed.');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
module.exports = SlackClient;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
let {Robot} = require.main.require("hubot/es2015.js");
|
|
2
|
+
const {ReactionMessage, FileSharedMessage, MeMessage} = require("./message");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adds a Listener for ReactionMessages with the provided matcher, options, and callback
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
* @param {Function} [matcher] - a function to determine if the listener should run. must return something
|
|
9
|
+
* truthy if it should and that value with be available on `response.match`.
|
|
10
|
+
* @param {Object} [options] - an object of additional parameters keyed on extension name.
|
|
11
|
+
* @param {Function} callback - a function that is called with a Response object if the matcher function returns true
|
|
12
|
+
*/
|
|
13
|
+
Robot.prototype.hearReaction = function(matcher, options, callback) {
|
|
14
|
+
let matchReaction = msg => msg instanceof ReactionMessage;
|
|
15
|
+
|
|
16
|
+
if (!options && !callback) {
|
|
17
|
+
return this.listen(matchReaction, matcher);
|
|
18
|
+
|
|
19
|
+
} else if (matcher instanceof Function) {
|
|
20
|
+
matchReaction = msg => msg instanceof ReactionMessage && matcher(msg);
|
|
21
|
+
|
|
22
|
+
} else {
|
|
23
|
+
callback = options;
|
|
24
|
+
options = matcher;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return this.listen(matchReaction, options, callback);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adds a Listener for MeMessages with the provided matcher, options, and callback
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
* @param {Function} [matcher] - a function to determine if the listener should run. must return something
|
|
35
|
+
* truthy if it should and that value with be available on `response.match`.
|
|
36
|
+
* @param {Object} [options] - an object of additional parameters keyed on extension name.
|
|
37
|
+
* @param {Function} callback - a function that is called with a Response object if the matcher function returns true
|
|
38
|
+
*/
|
|
39
|
+
Robot.prototype.hearMeMessage = function(matcher, options, callback) {
|
|
40
|
+
let matchMeMessage = msg => msg instanceof MeMessage;
|
|
41
|
+
|
|
42
|
+
if (!options && !callback) {
|
|
43
|
+
return this.listen(matchMeMessage, matcher);
|
|
44
|
+
|
|
45
|
+
} else if (matcher instanceof Function) {
|
|
46
|
+
matchMeMessage = msg => msg instanceof MeMessage && matcher(msg);
|
|
47
|
+
|
|
48
|
+
} else {
|
|
49
|
+
callback = options;
|
|
50
|
+
options = matcher;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this.listen(matchMeMessage, options, callback);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Adds a Listener for FileSharedMessages with the provided matcher, options, and callback
|
|
58
|
+
*
|
|
59
|
+
* @public
|
|
60
|
+
* @param {Function} [matcher] - a function to determine if the listener should run. must return something
|
|
61
|
+
* truthy if it should and that value with be available on `response.match`.
|
|
62
|
+
* @param {Object} [options] - an object of additional parameters keyed on extension name.
|
|
63
|
+
* @param {Function} callback - a function that is called with a Response object if the matcher function returns true
|
|
64
|
+
*/
|
|
65
|
+
Robot.prototype.fileShared = function(matcher, options, callback) {
|
|
66
|
+
let matchFileShare = msg => msg instanceof FileSharedMessage;
|
|
67
|
+
|
|
68
|
+
if (!options && !callback) {
|
|
69
|
+
return this.listen(matchFileShare, matcher);
|
|
70
|
+
|
|
71
|
+
} else if (matcher instanceof Function) {
|
|
72
|
+
matchFileShare = msg => msg instanceof FileSharedMessage && matcher(msg);
|
|
73
|
+
|
|
74
|
+
} else {
|
|
75
|
+
callback = options;
|
|
76
|
+
options = matcher;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.listen(matchFileShare, options, callback);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// NOTE: extend Response type with a method for creating a new thread from the incoming message
|
package/src/mention.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class SlackMention {
|
|
2
|
+
/**
|
|
3
|
+
* SlackMention is an instance of a mention within a SlackTextMessage.
|
|
4
|
+
* @constructor
|
|
5
|
+
* @param {string} id - The user or conversation id that the mention references
|
|
6
|
+
* @param {string} type - The type of mention ('user' or 'conversation')
|
|
7
|
+
* @param {Object} info - An object with additional info about the message reference, not guaranteed
|
|
8
|
+
*/
|
|
9
|
+
constructor(id, type, info) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.info = info ?? undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
module.exports = SlackMention;
|