@pipedream/slack_v2 0.0.1

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 (58) hide show
  1. package/LICENSE +41 -0
  2. package/actions/add-emoji-reaction/add-emoji-reaction.mjs +48 -0
  3. package/actions/approve-workflow/approve-workflow.mjs +92 -0
  4. package/actions/archive-channel/archive-channel.mjs +38 -0
  5. package/actions/common/build-blocks.mjs +182 -0
  6. package/actions/common/send-message.mjs +264 -0
  7. package/actions/create-channel/create-channel.mjs +40 -0
  8. package/actions/create-reminder/create-reminder.mjs +52 -0
  9. package/actions/delete-file/delete-file.mjs +39 -0
  10. package/actions/delete-message/delete-message.mjs +45 -0
  11. package/actions/find-message/find-message.mjs +83 -0
  12. package/actions/find-user-by-email/find-user-by-email.mjs +32 -0
  13. package/actions/get-current-user/get-current-user.mjs +68 -0
  14. package/actions/get-file/get-file.mjs +59 -0
  15. package/actions/invite-user-to-channel/invite-user-to-channel.mjs +45 -0
  16. package/actions/kick-user/kick-user.mjs +56 -0
  17. package/actions/list-channels/list-channels.mjs +52 -0
  18. package/actions/list-files/list-files.mjs +93 -0
  19. package/actions/list-group-members/list-group-members.mjs +68 -0
  20. package/actions/list-members-in-channel/list-members-in-channel.mjs +71 -0
  21. package/actions/list-replies/list-replies.mjs +74 -0
  22. package/actions/list-users/list-users.mjs +60 -0
  23. package/actions/reply-to-a-message/reply-to-a-message.mjs +54 -0
  24. package/actions/send-block-kit-message/send-block-kit-message.mjs +48 -0
  25. package/actions/send-large-message/send-large-message.mjs +95 -0
  26. package/actions/send-message/send-message.mjs +56 -0
  27. package/actions/send-message-advanced/send-message-advanced.mjs +75 -0
  28. package/actions/send-message-to-channel/send-message-to-channel.mjs +45 -0
  29. package/actions/send-message-to-user-or-group/send-message-to-user-or-group.mjs +93 -0
  30. package/actions/set-channel-description/set-channel-description.mjs +37 -0
  31. package/actions/set-channel-topic/set-channel-topic.mjs +37 -0
  32. package/actions/set-status/set-status.mjs +49 -0
  33. package/actions/update-group-members/update-group-members.mjs +72 -0
  34. package/actions/update-message/update-message.mjs +59 -0
  35. package/actions/update-profile/update-profile.mjs +94 -0
  36. package/actions/upload-file/upload-file.mjs +104 -0
  37. package/common/constants.mjs +31 -0
  38. package/package.json +22 -0
  39. package/slack_v2.app.mjs +1112 -0
  40. package/sources/common/base.mjs +184 -0
  41. package/sources/common/constants.mjs +58 -0
  42. package/sources/new-channel-created/new-channel-created.mjs +32 -0
  43. package/sources/new-channel-created/test-event.mjs +44 -0
  44. package/sources/new-interaction-event-received/README.md +85 -0
  45. package/sources/new-interaction-event-received/new-interaction-event-received.mjs +105 -0
  46. package/sources/new-interaction-event-received/test-event.mjs +86 -0
  47. package/sources/new-keyword-mention/new-keyword-mention.mjs +99 -0
  48. package/sources/new-keyword-mention/test-event.mjs +28 -0
  49. package/sources/new-message-in-channels/new-message-in-channels.mjs +106 -0
  50. package/sources/new-message-in-channels/test-event.mjs +45 -0
  51. package/sources/new-reaction-added/new-reaction-added.mjs +119 -0
  52. package/sources/new-reaction-added/test-event.mjs +193 -0
  53. package/sources/new-saved-message/new-saved-message.mjs +32 -0
  54. package/sources/new-saved-message/test-event.mjs +37 -0
  55. package/sources/new-user-added/new-user-added.mjs +32 -0
  56. package/sources/new-user-added/test-event.mjs +48 -0
  57. package/sources/new-user-mention/new-user-mention.mjs +124 -0
  58. package/sources/new-user-mention/test-event.mjs +28 -0
@@ -0,0 +1,1112 @@
1
+ import { WebClient } from "@slack/web-api";
2
+ import constants from "./common/constants.mjs";
3
+ import get from "lodash/get.js";
4
+ import retry from "async-retry";
5
+ import { ConfigurationError } from "@pipedream/platform";
6
+
7
+ export default {
8
+ type: "app",
9
+ app: "slack_v2",
10
+ propDefinitions: {
11
+ user: {
12
+ type: "string",
13
+ label: "User",
14
+ description: "Select a user",
15
+ async options({
16
+ prevContext, channelId,
17
+ }) {
18
+ const types = [
19
+ "im",
20
+ ];
21
+ let conversationsResp
22
+ = await this.availableConversations(types.join(), prevContext.cursor, true);
23
+ if (channelId) {
24
+ const { members } = await this.listChannelMembers({
25
+ channel: channelId,
26
+ throwRateLimitError: true,
27
+ });
28
+ conversationsResp.conversations = conversationsResp.conversations
29
+ .filter((c) => members.includes(c.user || c.id));
30
+ }
31
+ const userIds = conversationsResp.conversations.map(({ user }) => user).filter(Boolean);
32
+ const realNames = await this.realNameLookup(userIds);
33
+ return {
34
+ options: conversationsResp.conversations.filter((c) => c.user).map((c) => ({
35
+ label: `${realNames[c.user]}`,
36
+ value: c.user || c.id,
37
+ })),
38
+ context: {
39
+ cursor: conversationsResp.cursor,
40
+ },
41
+ };
42
+ },
43
+ },
44
+ group: {
45
+ type: "string",
46
+ label: "Group",
47
+ description: "Select a group",
48
+ async options({ prevContext }) {
49
+ let { cursor } = prevContext;
50
+ const types = [
51
+ "mpim",
52
+ ];
53
+ const resp = await this.availableConversations(types.join(), cursor, true);
54
+ return {
55
+ options: resp.conversations.map((c) => {
56
+ return {
57
+ label: c.purpose.value,
58
+ value: c.id,
59
+ };
60
+ }),
61
+ context: {
62
+ cursor: resp.cursor,
63
+ },
64
+ };
65
+ },
66
+ },
67
+ userGroup: {
68
+ type: "string",
69
+ label: "User Group",
70
+ description: "The encoded ID of the User Group.",
71
+ async options() {
72
+ const { usergroups } = await this.usergroupsList({
73
+ throwRateLimitError: true,
74
+ });
75
+ return usergroups.map((c) => ({
76
+ label: c.name,
77
+ value: c.id,
78
+ }));
79
+ },
80
+ },
81
+ reminder: {
82
+ type: "string",
83
+ label: "Reminder",
84
+ description: "Select a reminder",
85
+ async options() {
86
+ const { reminders } = await this.remindersList({
87
+ throwRateLimitError: true,
88
+ });
89
+ return reminders.map((c) => ({
90
+ label: c.text,
91
+ value: c.id,
92
+ }));
93
+ },
94
+ },
95
+ conversation: {
96
+ type: "string",
97
+ label: "Channel",
98
+ description: "Select a public or private channel, or a user or group",
99
+ async options({
100
+ prevContext, types,
101
+ }) {
102
+ let { cursor } = prevContext;
103
+ if (prevContext?.types) {
104
+ types = prevContext.types;
105
+ }
106
+ if (types == null) {
107
+ const { response_metadata: { scopes } } = await this.authTest({
108
+ throwRateLimitError: true,
109
+ });
110
+ types = [
111
+ "public_channel",
112
+ ];
113
+ if (scopes.includes("groups:read")) {
114
+ types.push("private_channel");
115
+ }
116
+ if (scopes.includes("mpim:read")) {
117
+ types.push("mpim");
118
+ }
119
+ if (scopes.includes("im:read")) {
120
+ types.push("im");
121
+ }
122
+ }
123
+ const conversationsResp = await this.availableConversations(types.join(), cursor, true);
124
+ let conversations, userIds, userNames, realNames;
125
+ if (types.includes("im")) {
126
+ conversations = conversationsResp.conversations;
127
+ userIds = conversations.map(({ user }) => user).filter(Boolean);
128
+ } else {
129
+ conversations = conversationsResp.conversations.filter((c) => !c.is_im);
130
+ }
131
+ if (types.includes("mpim")) {
132
+ userNames = [
133
+ ...new Set(conversations.filter((c) => c.is_mpim).map((c) => c.purpose.value)
134
+ .map((v) => v.match(/@[\w.-]+/g) || [])
135
+ .flat()
136
+ .map((u) => u.slice(1))),
137
+ ];
138
+ }
139
+ if ((userIds?.length > 0) || (userNames?.length > 0)) {
140
+ // Look up real names for userIds and userNames at the same time to
141
+ // minimize number of API calls.
142
+ realNames = await this.realNameLookup(userIds, userNames);
143
+ }
144
+
145
+ return {
146
+ options: conversations.map((c) => {
147
+ if (c.is_im) {
148
+ return {
149
+ label: `Direct messaging with: ${realNames[c.user]}`,
150
+ value: c.id,
151
+ };
152
+ } else if (c.is_mpim) {
153
+ const usernames = c.purpose.value.match(/@[\w.-]+/g) || [];
154
+ const realnames = usernames.map((u) => realNames[u.slice(1)] || u);
155
+ return {
156
+ label: realnames.length
157
+ ? `Group messaging with: ${realnames.join(", ")}`
158
+ : c.purpose.value,
159
+ value: c.id,
160
+ };
161
+ } else {
162
+ return {
163
+ label: `${c.is_private
164
+ ? "Private"
165
+ : "Public"} channel: ${c.name}`,
166
+ value: c.id,
167
+ };
168
+ }
169
+ }),
170
+ context: {
171
+ types,
172
+ cursor: conversationsResp.cursor,
173
+ },
174
+ };
175
+ },
176
+ },
177
+ channelId: {
178
+ type: "string",
179
+ label: "Channel ID",
180
+ description: "Select the channel's id.",
181
+ async options({
182
+ prevContext,
183
+ types = Object.values(constants.CHANNEL_TYPE),
184
+ channelsFilter = (channel) => channel,
185
+ excludeArchived = true,
186
+ }) {
187
+ const {
188
+ channels,
189
+ response_metadata: { next_cursor: cursor },
190
+ } = await this.conversationsList({
191
+ types: types.join(),
192
+ cursor: prevContext.cursor,
193
+ limit: constants.LIMIT,
194
+ exclude_archived: excludeArchived,
195
+ throwRateLimitError: true,
196
+ });
197
+
198
+ let userNames;
199
+ if (types.includes("im")) {
200
+ const userIds = channels.filter(({ is_im }) => is_im).map(({ user }) => user);
201
+ userNames = await this.userNameLookup(userIds);
202
+ }
203
+
204
+ const options = channels
205
+ .filter(channelsFilter)
206
+ .map((c) => {
207
+ if (c.is_im) {
208
+ return {
209
+ label: `Direct messaging with: @${userNames[c.user]}`,
210
+ value: c.id,
211
+ };
212
+ } else if (c.is_mpim) {
213
+ return {
214
+ label: c.purpose.value,
215
+ value: c.id,
216
+ };
217
+ } else {
218
+ return {
219
+ label: `${c.is_private
220
+ ? "Private"
221
+ : "Public"} channel: ${c.name}`,
222
+ value: c.id,
223
+ };
224
+ }
225
+ });
226
+
227
+ return {
228
+ options,
229
+ context: {
230
+ cursor,
231
+ },
232
+ };
233
+ },
234
+ },
235
+ team: {
236
+ type: "string",
237
+ label: "Team",
238
+ description: "Select a team.",
239
+ async options({ prevContext }) {
240
+ const {
241
+ teams,
242
+ response_metadata: { next_cursor: cursor },
243
+ } = await this.authTeamsList({
244
+ cursor: prevContext.cursor,
245
+ limit: constants.LIMIT,
246
+ throwRateLimitError: true,
247
+ });
248
+
249
+ return {
250
+ options: teams.map((team) => ({
251
+ label: team.name,
252
+ value: team.id,
253
+ })),
254
+
255
+ context: {
256
+ cursor,
257
+ },
258
+ };
259
+ },
260
+ },
261
+ messageTs: {
262
+ type: "string",
263
+ label: "Message Timestamp",
264
+ description: "Timestamp of a message. e.g. `1403051575.000407`.",
265
+ },
266
+ text: {
267
+ type: "string",
268
+ label: "Text",
269
+ description: "Text of the message to send (see Slack's [formatting docs](https://api.slack.com/reference/surfaces/formatting)). This field is usually necessary, unless you're providing only attachments instead.",
270
+ },
271
+ topic: {
272
+ type: "string",
273
+ label: "Topic",
274
+ description: "Text of the new channel topic.",
275
+ },
276
+ purpose: {
277
+ type: "string",
278
+ label: "Purpose",
279
+ description: "Text of the new channel purpose.",
280
+ },
281
+ query: {
282
+ type: "string",
283
+ label: "Query",
284
+ description: "Search query.",
285
+ },
286
+ file: {
287
+ type: "string",
288
+ label: "File ID",
289
+ description: "Specify a file by providing its ID.",
290
+ async options({
291
+ channel, page,
292
+ }) {
293
+ const { files } = await this.listFiles({
294
+ channel,
295
+ page: page + 1,
296
+ count: constants.LIMIT,
297
+ throwRateLimitError: true,
298
+ });
299
+ return files?.map(({
300
+ id: value, name: label,
301
+ }) => ({
302
+ value,
303
+ label,
304
+ })) || [];
305
+ },
306
+ },
307
+ attachments: {
308
+ type: "string",
309
+ label: "Attachments",
310
+ description: "A JSON-based array of structured attachments, presented as a URL-encoded string (e.g., `[{\"pretext\": \"pre-hello\", \"text\": \"text-world\"}]`).",
311
+ optional: true,
312
+ },
313
+ unfurl_links: {
314
+ type: "boolean",
315
+ label: "Unfurl Links",
316
+ description: "Default to `false`. Pass `true` to enable unfurling of links.",
317
+ default: false,
318
+ optional: true,
319
+ },
320
+ unfurl_media: {
321
+ type: "boolean",
322
+ label: "Unfurl Media",
323
+ description: "Defaults to `false`. Pass `true` to enable unfurling of media content.",
324
+ default: false,
325
+ optional: true,
326
+ },
327
+ parse: {
328
+ type: "string",
329
+ label: "Parse",
330
+ description: "Change how messages are treated. Defaults to none. By default, URLs will be hyperlinked. Set `parse` to `none` to remove the hyperlinks. The behavior of `parse` is different for text formatted with `mrkdwn`. By default, or when `parse` is set to `none`, `mrkdwn` formatting is implemented. To ignore `mrkdwn` formatting, set `parse` to full.",
331
+ optional: true,
332
+ },
333
+ as_user: {
334
+ type: "boolean",
335
+ label: "Send as User",
336
+ description: "Optionally pass `true` to post the message as the authenticated user, instead of as a bot. Defaults to `false`.",
337
+ default: false,
338
+ optional: true,
339
+ },
340
+ mrkdwn: {
341
+ label: "Send text as Slack mrkdwn",
342
+ type: "boolean",
343
+ description: "`true` by default. Pass `false` to disable Slack markup parsing. [See docs here](https://api.slack.com/reference/surfaces/formatting)",
344
+ default: true,
345
+ optional: true,
346
+ },
347
+ post_at: {
348
+ label: "Schedule message",
349
+ description: "Messages can only be scheduled up to 120 days in advance, and cannot be scheduled for the past. The datetime should be in ISO 8601 format. (Example: `2014-01-01T00:00:00Z`)",
350
+ type: "string",
351
+ optional: true,
352
+ },
353
+ username: {
354
+ type: "string",
355
+ label: "Bot Username",
356
+ description: "Optionally customize your bot's user name (default is `Pipedream`). Must be used in conjunction with `Send as User` set to false, otherwise ignored.",
357
+ optional: true,
358
+ },
359
+ blocks: {
360
+ type: "string",
361
+ label: "Blocks",
362
+ description: "Enter an array of [structured blocks](https://app.slack.com/block-kit-builder) as a URL-encoded string. E.g., `[{ \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \"This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and <https://pipedream.com|this is a link>\" }}]`\n\n**Tip:** Construct your blocks in a code step, return them as an array, and then pass the return value to this step.",
363
+ optional: true,
364
+ },
365
+ icon_emoji: {
366
+ type: "string",
367
+ label: "Icon (emoji)",
368
+ description: "Optionally provide an emoji to use as the icon for this message. E.g., `:fire:` Overrides `icon_url`. Must be used in conjunction with `Send as User` set to `false`, otherwise ignored.",
369
+ optional: true,
370
+ async options() {
371
+ return await this.getCustomEmojis({
372
+ throwRateLimitError: true,
373
+ });
374
+ },
375
+ },
376
+ content: {
377
+ label: "File Path or URL",
378
+ description: "The file to upload. Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/myFile.txt`)",
379
+ type: "string",
380
+ },
381
+ link_names: {
382
+ type: "boolean",
383
+ label: "Link Names",
384
+ description: "Find and link channel names and usernames.",
385
+ optional: true,
386
+ },
387
+ thread_broadcast: {
388
+ type: "boolean",
389
+ label: "Send Channel Message",
390
+ description: "If `true`, posts in the thread and channel. Used in conjunction with `Message Timestamp` and indicates whether reply should be made visible to everyone in the channel. Defaults to `false`.",
391
+ default: false,
392
+ optional: true,
393
+ },
394
+ reply_channel: {
395
+ label: "Reply Channel or Conversation ID",
396
+ type: "string",
397
+ description: "Provide the channel or conversation ID for the thread to reply to (e.g., if triggering on new Slack messages, enter `{{event.channel}}`). If the channel does not match the thread timestamp, a new message will be posted to this channel.",
398
+ optional: true,
399
+ },
400
+ icon_url: {
401
+ type: "string",
402
+ label: "Icon (image URL)",
403
+ description: "Optionally provide an image URL to use as the icon for this message. Must be used in conjunction with `Send as User` set to `false`, otherwise ignored.",
404
+ optional: true,
405
+ },
406
+ initial_comment: {
407
+ type: "string",
408
+ label: "Initial Comment",
409
+ description: "The message text introducing the file",
410
+ optional: true,
411
+ },
412
+ email: {
413
+ type: "string",
414
+ label: "Email",
415
+ description: "An email address belonging to a user in the workspace",
416
+ },
417
+ metadata_event_type: {
418
+ type: "string",
419
+ label: "Metadata Event Type",
420
+ description: "The name of the metadata event. Example: `task_created`",
421
+ optional: true,
422
+ },
423
+ metadata_event_payload: {
424
+ type: "string",
425
+ label: "Metadata Event Payload",
426
+ description: "The payload of the metadata event. Must be a JSON string. Example: `{ \"id\": \"11223\", \"title\": \"Redesign Homepage\"}`",
427
+ optional: true,
428
+ },
429
+ ignoreMyself: {
430
+ type: "boolean",
431
+ label: "Ignore myself",
432
+ description: "Ignore messages from me",
433
+ default: false,
434
+ },
435
+ keyword: {
436
+ type: "string",
437
+ label: "Keyword",
438
+ description: "Keyword to monitor",
439
+ },
440
+ ignoreBot: {
441
+ type: "boolean",
442
+ label: "Ignore Bots",
443
+ description: "Ignore messages from bots",
444
+ default: false,
445
+ optional: true,
446
+ },
447
+ resolveNames: {
448
+ type: "boolean",
449
+ label: "Resolve Names",
450
+ description: "Instead of returning `channel`, `team`, and `user` as IDs, return their human-readable names.",
451
+ default: false,
452
+ optional: true,
453
+ },
454
+ pageSize: {
455
+ type: "integer",
456
+ label: "Page Size",
457
+ description: "The number of results to include in a page. Default: 250",
458
+ default: constants.LIMIT,
459
+ optional: true,
460
+ },
461
+ numPages: {
462
+ type: "integer",
463
+ label: "Number of Pages",
464
+ description: "The number of pages to retrieve. Default: 1",
465
+ default: 1,
466
+ optional: true,
467
+ },
468
+ addToChannel: {
469
+ type: "boolean",
470
+ label: "Add app to channel automatically?",
471
+ description: "If `true`, the app will be added to the specified non-DM channel(s) automatically. If `false`, you must add the app to the channel manually. Defaults to `true`.",
472
+ default: true,
473
+ },
474
+ },
475
+ methods: {
476
+ getChannelLabel(resource) {
477
+ if (resource.user) {
478
+ return `Direct Messaging with: @${resource.user.name}`;
479
+ }
480
+
481
+ const {
482
+ is_private: isPrivate,
483
+ name,
484
+ } = resource.channel;
485
+
486
+ return `${isPrivate && "Private" || "Public"} channel #${name}`;
487
+ },
488
+ mySlackId() {
489
+ return this.$auth.oauth_uid;
490
+ },
491
+ getToken(opts = {}) {
492
+ // Use bot token if asBot is true and available, otherwise use user token.
493
+ const botToken = this.getBotToken();
494
+ const userToken = this.$auth.oauth_access_token;
495
+ return (opts.asBot && botToken)
496
+ ? botToken
497
+ : userToken;
498
+ },
499
+ getBotToken() {
500
+ return this.$auth.bot_token;
501
+ },
502
+ async getChannelDisplayName(channel) {
503
+ if (channel.user) {
504
+ try {
505
+ const { profile } = await this.getUserProfile({
506
+ user: channel.user,
507
+ });
508
+ return `@${profile.real_name || profile?.real_name}`;
509
+ } catch {
510
+ return "user";
511
+ }
512
+ } else if (channel.is_mpim) {
513
+ try {
514
+ const { members } = await this.listChannelMembers({
515
+ channel: channel.id,
516
+ });
517
+ const users = await Promise.all(members.map((m) => this.getUserProfile({
518
+ user: m,
519
+ })));
520
+ const realNames = users.map((u) => u.profile?.real_name || u.real_name);
521
+ return `Group Messaging with: ${realNames.join(", ")}`;
522
+ } catch {
523
+ return `Group Messaging with: ${channel.purpose.value}`;
524
+ }
525
+ }
526
+ return `#${channel?.name}`;
527
+ },
528
+ /**
529
+ * Returns a Slack Web Client object authenticated with the user's access
530
+ * token
531
+ */
532
+ sdk(opts = {}) {
533
+ return new WebClient(this.getToken(opts), {
534
+ rejectRateLimitedCalls: true,
535
+ slackApiUrl: this.$auth.base_url,
536
+ });
537
+ },
538
+ async makeRequest({
539
+ method = "", throwRateLimitError = false, asBot, as_user, ...args
540
+ } = {}) {
541
+ // Passing as_user as false with a v2 user token lacking the deprecated
542
+ // `chat:write:bot` scope results in an error. If as_user is false and a
543
+ // bot token is available, use the bot token and omit as_user. Otherwise,
544
+ // pass as_user through.
545
+ if (as_user === false && Boolean(this.getBotToken())) {
546
+ asBot = true;
547
+ } else {
548
+ args.as_user = as_user;
549
+ }
550
+
551
+ const props = method.split(".");
552
+ const sdk = props.reduce((reduction, prop) =>
553
+ reduction[prop], this.sdk({
554
+ asBot,
555
+ }));
556
+
557
+ let response;
558
+ try {
559
+ response = await this._withRetries(() => sdk(args), throwRateLimitError);
560
+ } catch (error) {
561
+ if ([
562
+ "not_in_channel",
563
+ "channel_not_found",
564
+ ].some((errorType) => `${error}`.includes(errorType)) && asBot) {
565
+ const followUp = method.startsWith("chat.")
566
+ ? "Ensure the bot is a member of the channel, or set the **Send as User** option to true to act on behalf of the authenticated user."
567
+ : "Ensure the bot is a member of the channel.";
568
+ throw new ConfigurationError(`${error}\n${followUp}`);
569
+ }
570
+ throw error;
571
+ }
572
+
573
+ if (!response.ok) {
574
+ throw response.error;
575
+ }
576
+ return response;
577
+ },
578
+ async _withRetries(apiCall, throwRateLimitError = false) {
579
+ const retryOpts = {
580
+ retries: 3,
581
+ minTimeout: 30000,
582
+ };
583
+ return retry(async (bail) => {
584
+ try {
585
+ return await apiCall();
586
+ } catch (error) {
587
+ const statusCode = get(error, "code");
588
+ if (statusCode === "slack_webapi_rate_limited_error") {
589
+ if (throwRateLimitError) {
590
+ bail(`Rate limit exceeded. ${error}`);
591
+ } else {
592
+ console.log(`Rate limit exceeded. Will retry in ${retryOpts.minTimeout / 1000} seconds`);
593
+ throw error;
594
+ }
595
+ }
596
+ bail(`${error}`);
597
+ }
598
+ }, retryOpts);
599
+ },
600
+ /**
601
+ * Returns a list of channel-like conversations in a workspace. The
602
+ * "channels" returned depend on what the calling token has access to and
603
+ * the directives placed in the types parameter.
604
+ *
605
+ * @param {string} [types] - a comma-separated list of channel types to get.
606
+ * Any combination of: `public_channel`, `private_channel`, `mpim`, `im`
607
+ * @param {string} [cursor] - a cursor returned by the previous API call,
608
+ * used to paginate through collections of data
609
+ * @returns an object containing a list of conversations and the cursor for the next
610
+ * page of conversations
611
+ */
612
+ async availableConversations(types, cursor, throwRateLimitError = false) {
613
+ const {
614
+ channels: conversations,
615
+ response_metadata: { next_cursor: nextCursor },
616
+ } = await this.usersConversations({
617
+ types,
618
+ cursor,
619
+ limit: constants.LIMIT,
620
+ exclude_archived: true,
621
+ throwRateLimitError,
622
+ });
623
+ return {
624
+ cursor: nextCursor,
625
+ conversations,
626
+ };
627
+ },
628
+ async userNameLookup(ids = [], throwRateLimitError = true, args = {}) {
629
+ let cursor;
630
+ const userNames = {};
631
+ do {
632
+ const {
633
+ members: users,
634
+ response_metadata: { next_cursor: nextCursor },
635
+ } = await this.usersList({
636
+ limit: constants.LIMIT,
637
+ cursor,
638
+ throwRateLimitError,
639
+ ...args,
640
+ });
641
+
642
+ for (const user of users) {
643
+ if (ids.includes(user.id)) {
644
+ userNames[user.id] = user.name;
645
+ }
646
+ }
647
+
648
+ cursor = nextCursor;
649
+ } while (cursor && Object.keys(userNames).length < ids.length);
650
+ return userNames;
651
+ },
652
+ async realNameLookup(ids = [], usernames = [], throwRateLimitError = true, args = {}) {
653
+ const idSet = new Set(ids);
654
+ const usernameSet = new Set(usernames);
655
+ let cursor;
656
+ const realNames = {};
657
+ const targetCount = ids.length + usernames.length;
658
+ do {
659
+ const {
660
+ members: users,
661
+ response_metadata: { next_cursor: nextCursor },
662
+ } = await this.usersList({
663
+ limit: constants.LIMIT,
664
+ cursor,
665
+ throwRateLimitError,
666
+ ...args,
667
+ });
668
+
669
+ for (const user of users) {
670
+ if (idSet.has(user.id)) {
671
+ realNames[user.id] = user.profile.real_name;
672
+ }
673
+ if (usernameSet.has(user.name)) {
674
+ realNames[user.name] = user.profile.real_name;
675
+ }
676
+ }
677
+
678
+ cursor = nextCursor;
679
+ } while (cursor && Object.keys(realNames).length < targetCount);
680
+ return realNames;
681
+ },
682
+ async maybeAddAppToChannels(channelIds = []) {
683
+ if (!this.getBotToken()) {
684
+ console.log("Skipping adding app to channels: bot unavailable.");
685
+ return;
686
+ }
687
+ try {
688
+ const {
689
+ bot_id, user_id,
690
+ } = await this.authTest({
691
+ asBot: true,
692
+ });
693
+ if (!bot_id) {
694
+ console.log("Skipping adding app to channels: bot not found.");
695
+ return;
696
+ }
697
+ for (const channel of channelIds) {
698
+ try {
699
+ // Note: Trying to add the app to DM or group DM channels results in
700
+ // the error: method_not_supported_for_channel_type
701
+ await this.inviteToConversation({
702
+ channel,
703
+ users: user_id,
704
+ });
705
+ } catch (error) {
706
+ console.log(`Unable to add app to channel ${channel}: ${error}`);
707
+ }
708
+ }
709
+ } catch (error) {
710
+ console.log(`Unable to add app to channels: ${error}`);
711
+ }
712
+ },
713
+ /**
714
+ * Checks authentication & identity.
715
+ * @param {*} args Arguments object
716
+ * @returns Promise
717
+ */
718
+ authTest(args = {}) {
719
+ return this.makeRequest({
720
+ method: "auth.test",
721
+ ...args,
722
+ });
723
+ },
724
+ /**
725
+ * Lists all reminders created by or for a given user.
726
+ * @param {*} args Arguments object
727
+ * @param {string} [args.team_id] Encoded team id, required if org token is passed.
728
+ * E.g. `T1234567890`
729
+ * @returns Promise
730
+ */
731
+ remindersList(args = {}) {
732
+ return this.makeRequest({
733
+ method: "reminders.list",
734
+ ...args,
735
+ });
736
+ },
737
+ /**
738
+ * List all User Groups for a team
739
+ * @param {*} args
740
+ * @returns Promise
741
+ */
742
+ usergroupsList(args = {}) {
743
+ return this.makeRequest({
744
+ method: "usergroups.list",
745
+ ...args,
746
+ });
747
+ },
748
+ authTeamsList(args = {}) {
749
+ args.limit ||= constants.LIMIT;
750
+ return this.makeRequest({
751
+ method: "auth.teams.list",
752
+ ...args,
753
+ });
754
+ },
755
+ /**
756
+ * List conversations the calling user may access.
757
+ * Bot Scopes: `channels:read` `groups:read` `im:read` `mpim:read`
758
+ * @param {UsersConversationsArguments} args Arguments object
759
+ * @param {string} [args.cursor] Pagination value e.g. (`dXNlcjpVMDYxTkZUVDI=`)
760
+ * @param {boolean} [args.exclude_archived] Set to `true` to exclude archived channels
761
+ * from the list. Defaults to `false`
762
+ * @param {number} [args.limit] Pagination value. Defaults to `250`
763
+ * @param {string} [args.team_id] Encoded team id to list users in,
764
+ * required if org token is used
765
+ * @param {string} [args.types] Mix and match channel types by providing a
766
+ * comma-separated list of any combination of `public_channel`, `private_channel`, `mpim`, `im`
767
+ * Defaults to `public_channel`. E.g. `im,mpim`
768
+ * @param {string} [args.user] Browse conversations by a specific
769
+ * user ID's membership. Non-public channels are restricted to those where the calling user
770
+ * shares membership. E.g `W0B2345D`
771
+ * @returns Promise
772
+ */
773
+ usersConversations(args = {}) {
774
+ args.limit ||= constants.LIMIT;
775
+ return this.makeRequest({
776
+ method: "users.conversations",
777
+ user: this.$auth.oauth_uid,
778
+ ...args,
779
+ });
780
+ },
781
+ /**
782
+ * Lists all users in a Slack team.
783
+ * Bot Scopes: `users:read`
784
+ * @param {UsersListArguments} args Arguments object
785
+ * @param {string} [args.cursor] Pagination value e.g. (`dXNlcjpVMDYxTkZUVDI=`)
786
+ * @param {boolean} [args.include_locale] Set this to `true` to receive the locale
787
+ * for users. Defaults to `false`
788
+ * @param {number} [args.limit] The maximum number of items to return. Defaults to `250`
789
+ * @param {string} [args.team_id] Encoded team id to list users in,
790
+ * required if org token is used
791
+ * @returns Promise
792
+ */
793
+ usersList(args = {}) {
794
+ args.limit ||= constants.LIMIT;
795
+ return this.makeRequest({
796
+ method: "users.list",
797
+ ...args,
798
+ });
799
+ },
800
+ /**
801
+ * Lists all channels in a Slack team.
802
+ * Bot Scopes: `channels:read` `groups:read` `im:read` `mpim:read`
803
+ * @param {ConversationsListArguments} args Arguments object
804
+ * @param {string} [args.cursor] Pagination value e.g. (`dXNlcjpVMDYxTkZUVDI=`)
805
+ * @param {boolean} [args.exclude_archived] Set to `true` to exclude archived channels
806
+ * from the list. Defaults to `false`
807
+ * @param {number} [args.limit] pagination value. Defaults to `250`
808
+ * @param {string} [args.team_id] encoded team id to list users in,
809
+ * required if org token is used
810
+ * @param {string} [args.types] Mix and match channel types by providing a
811
+ * comma-separated list of any combination of `public_channel`, `private_channel`, `mpim`, `im`
812
+ * Defaults to `public_channel`. E.g. `im,mpim`
813
+ * @returns Promise
814
+ */
815
+ conversationsList(args = {}) {
816
+ args.limit ||= constants.LIMIT;
817
+ return this.makeRequest({
818
+ method: "conversations.list",
819
+ ...args,
820
+ });
821
+ },
822
+ /**
823
+ * Fetches a conversation's history of messages and events.
824
+ * Bot Scopes: `channels:history` `groups:history` `im:history` `mpim:history`
825
+ * @param {ConversationsHistoryArguments} args Arguments object
826
+ * @param {string} args.channel Conversation ID to fetch history for. E.g. `C1234567890`
827
+ * @param {string} [args.cursor] Pagination value e.g. (`dXNlcjpVMDYxTkZUVDI=`)
828
+ * @param {boolean} [args.include_all_metadata]
829
+ * @param {boolean} [args.inclusive]
830
+ * @param {string} [args.latest]
831
+ * @param {number} [args.limit]
832
+ * @param {string} [args.oldest]
833
+ * @returns Promise
834
+ */
835
+ conversationsHistory(args = {}) {
836
+ args.limit ||= constants.LIMIT;
837
+ return this.makeRequest({
838
+ method: "conversations.history",
839
+ ...args,
840
+ });
841
+ },
842
+ /**
843
+ * Retrieve information about a conversation.
844
+ * Bot Scopes: `channels:read` `groups:read` `im:read` `mpim:read`
845
+ * @param {ConversationsInfoArguments} args Arguments object
846
+ * @param {string} args.channel Conversation ID to learn more about. E.g. `C1234567890`
847
+ * @param {boolean} [args.include_locale] Set this to `true` to receive the locale
848
+ * for users. Defaults to `false`
849
+ * @param {boolean} [args.include_num_members] Set to true to include the
850
+ * member count for the specified conversation. Defaults to `false`
851
+ * @returns Promise
852
+ */
853
+ conversationsInfo(args = {}) {
854
+ return this.makeRequest({
855
+ method: "conversations.info",
856
+ ...args,
857
+ });
858
+ },
859
+ /**
860
+ * Retrieve information about a conversation.
861
+ * Bot Scopes: `users:read`
862
+ * @param {UsersInfoArguments} args arguments object
863
+ * @param {string} args.user User to get info on. E.g. `W1234567890`
864
+ * @param {boolean} [args.include_locale] Set this to true to receive the locale
865
+ * for this user. Defaults to `false`
866
+ * @returns Promise
867
+ */
868
+ usersInfo(args = {}) {
869
+ return this.makeRequest({
870
+ method: "users.info",
871
+ ...args,
872
+ });
873
+ },
874
+ /**
875
+ * Searches for messages matching a query.
876
+ * User Scopes: `search:read`
877
+ * @param {SearchMessagesArguments} args Arguments object
878
+ * @param {string} args.query Search query
879
+ * @param {number} [args.count] Number of items to return per page. Default `250`
880
+ * @param {string} [args.cursor] Use this when getting results with cursormark
881
+ * pagination. For first call send `*` for subsequent calls, send the value of
882
+ * `next_cursor` returned in the previous call's results
883
+ * @param {boolean} [args.highlight]
884
+ * @param {number} [args.page]
885
+ * @param {string} [args.sort]
886
+ * @param {string} [args.sort_dir]
887
+ * @param {string} [args.team_id] Encoded team id to search in,
888
+ * required if org token is used. E.g. `T1234567890`
889
+ * @returns Promise
890
+ */
891
+ searchMessages(args = {}) {
892
+ args.count ||= constants.LIMIT;
893
+ return this.makeRequest({
894
+ method: "search.messages",
895
+ ...args,
896
+ });
897
+ },
898
+ assistantSearch(args = {}) {
899
+ args.count ||= constants.LIMIT;
900
+ // Uses apiCall directly since assistant.search.context is not exposed as
901
+ // a method on WebClient
902
+ return this.sdk().apiCall("assistant.search.context", {
903
+ ...args,
904
+ });
905
+ },
906
+ /**
907
+ * Lists reactions made by a user.
908
+ * User Scopes: `reactions:read`
909
+ * Bot Scopes: `reactions:read`
910
+ * @param {ReactionsListArguments} args Arguments object
911
+ * @param {number} [args.count] Number of items to return per page. Default `100`
912
+ * @param {string} [args.cursor] Parameter for pagination. Set cursor equal to the
913
+ * `next_cursor` attribute returned by the previous request's response_metadata.
914
+ * This parameter is optional, but pagination is mandatory: the default value simply
915
+ * fetches the first "page" of the collection.
916
+ * @param {boolean} [args.full] If true always return the complete reaction list.
917
+ * @param {number} [args.limit] The maximum number of items to return.
918
+ * Fewer than the requested number of items may be returned, even if the end of the
919
+ * list hasn't been reached.
920
+ * @param {number} [args.page] Page number of results to return. Defaults to `1`.
921
+ * @param {string} [args.team_id] Encoded team id to list reactions in,
922
+ * required if org token is used
923
+ * @param {string} [args.user] Show reactions made by this user. Defaults to the authed user.
924
+ * @returns Promise
925
+ */
926
+ reactionsList(args = {}) {
927
+ args.limit ||= constants.LIMIT;
928
+ return this.makeRequest({
929
+ method: "reactions.list",
930
+ ...args,
931
+ });
932
+ },
933
+ async getCustomEmojis(args = {}) {
934
+ const resp = await this.sdk().emoji.list({
935
+ include_categories: true,
936
+ limit: constants.LIMIT,
937
+ ...args,
938
+ });
939
+
940
+ const emojis = Object.keys(resp.emoji);
941
+ for (const category of resp.categories) {
942
+ emojis.push(...category.emoji_names);
943
+ }
944
+ return emojis;
945
+ },
946
+ listChannelMembers(args = {}) {
947
+ args.limit ||= constants.LIMIT;
948
+ return this.makeRequest({
949
+ method: "conversations.members",
950
+ ...args,
951
+ });
952
+ },
953
+ listFiles(args = {}) {
954
+ args.count ||= constants.LIMIT;
955
+ return this.makeRequest({
956
+ method: "files.list",
957
+ // Use bot token, if available, since the required `files:read` scope
958
+ // is only requested for bot tokens in the Pipedream app.
959
+ asBot: true,
960
+ ...args,
961
+ });
962
+ },
963
+ listGroupMembers(args = {}) {
964
+ args.limit ||= constants.LIMIT;
965
+ return this.makeRequest({
966
+ method: "usergroups.users.list",
967
+ ...args,
968
+ });
969
+ },
970
+ getFileInfo(args = {}) {
971
+ return this.makeRequest({
972
+ method: "files.info",
973
+ // Use bot token, if available, since the required `files:read` scope
974
+ // is only requested for bot tokens in the Pipedream app.
975
+ asBot: true,
976
+ ...args,
977
+ });
978
+ },
979
+ getUserProfile(args = {}) {
980
+ return this.makeRequest({
981
+ method: "users.profile.get",
982
+ ...args,
983
+ });
984
+ },
985
+ getBotInfo(args = {}) {
986
+ return this.makeRequest({
987
+ method: "bots.info",
988
+ ...args,
989
+ });
990
+ },
991
+ getTeamInfo(args = {}) {
992
+ return this.makeRequest({
993
+ method: "team.info",
994
+ ...args,
995
+ });
996
+ },
997
+ getConversationReplies(args = {}) {
998
+ return this.makeRequest({
999
+ method: "conversations.replies",
1000
+ ...args,
1001
+ });
1002
+ },
1003
+ addReactions(args = {}) {
1004
+ return this.makeRequest({
1005
+ method: "reactions.add",
1006
+ ...args,
1007
+ });
1008
+ },
1009
+ postChatMessage(args = {}) {
1010
+ return this.makeRequest({
1011
+ method: "chat.postMessage",
1012
+ ...args,
1013
+ });
1014
+ },
1015
+ archiveConversations(args = {}) {
1016
+ return this.makeRequest({
1017
+ method: "conversations.archive",
1018
+ ...args,
1019
+ });
1020
+ },
1021
+ scheduleMessage(args = {}) {
1022
+ return this.makeRequest({
1023
+ method: "chat.scheduleMessage",
1024
+ ...args,
1025
+ });
1026
+ },
1027
+ createConversations(args = {}) {
1028
+ return this.makeRequest({
1029
+ method: "conversations.create",
1030
+ ...args,
1031
+ });
1032
+ },
1033
+ inviteToConversation(args = {}) {
1034
+ return this.makeRequest({
1035
+ method: "conversations.invite",
1036
+ ...args,
1037
+ });
1038
+ },
1039
+ kickUserFromConversation(args = {}) {
1040
+ return this.makeRequest({
1041
+ method: "conversations.kick",
1042
+ ...args,
1043
+ });
1044
+ },
1045
+ addReminders(args = {}) {
1046
+ return this.makeRequest({
1047
+ method: "reminders.add",
1048
+ ...args,
1049
+ });
1050
+ },
1051
+ deleteFiles(args = {}) {
1052
+ return this.makeRequest({
1053
+ method: "files.delete",
1054
+ ...args,
1055
+ });
1056
+ },
1057
+ deleteMessage(args = {}) {
1058
+ return this.makeRequest({
1059
+ method: "chat.delete",
1060
+ ...args,
1061
+ });
1062
+ },
1063
+ lookupUserByEmail(args = {}) {
1064
+ return this.makeRequest({
1065
+ method: "users.lookupByEmail",
1066
+ ...args,
1067
+ });
1068
+ },
1069
+ setChannelDescription(args = {}) {
1070
+ return this.makeRequest({
1071
+ method: "conversations.setPurpose",
1072
+ ...args,
1073
+ });
1074
+ },
1075
+ setChannelTopic(args = {}) {
1076
+ return this.makeRequest({
1077
+ method: "conversations.setTopic",
1078
+ ...args,
1079
+ });
1080
+ },
1081
+ updateProfile(args = {}) {
1082
+ return this.makeRequest({
1083
+ method: "users.profile.set",
1084
+ ...args,
1085
+ });
1086
+ },
1087
+ updateGroupMembers(args = {}) {
1088
+ return this.makeRequest({
1089
+ method: "usergroups.users.update",
1090
+ ...args,
1091
+ });
1092
+ },
1093
+ updateMessage(args = {}) {
1094
+ return this.makeRequest({
1095
+ method: "chat.update",
1096
+ ...args,
1097
+ });
1098
+ },
1099
+ getUploadUrl(args = {}) {
1100
+ return this.makeRequest({
1101
+ method: "files.getUploadURLExternal",
1102
+ ...args,
1103
+ });
1104
+ },
1105
+ completeUpload(args = {}) {
1106
+ return this.makeRequest({
1107
+ method: "files.completeUploadExternal",
1108
+ ...args,
1109
+ });
1110
+ },
1111
+ },
1112
+ };