@pipedream/slack_v2 0.6.0 → 0.7.0

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 (62) hide show
  1. package/actions/add-emoji-reaction/add-emoji-reaction.mjs +1 -1
  2. package/actions/add-reaction/add-reaction.mjs +1 -1
  3. package/actions/archive-channel/archive-channel.mjs +1 -1
  4. package/actions/browse-files/browse-files.mjs +1 -1
  5. package/actions/create-channel/create-channel.mjs +1 -1
  6. package/actions/create-reminder/create-reminder.mjs +1 -1
  7. package/actions/delete-file/delete-file.mjs +1 -1
  8. package/actions/delete-message/delete-message.mjs +48 -6
  9. package/actions/edit-message/edit-message.mjs +1 -1
  10. package/actions/find-message/find-message.mjs +1 -1
  11. package/actions/find-user-by-email/find-user-by-email.mjs +1 -1
  12. package/actions/find-user-by-id/find-user-by-id.mjs +1 -1
  13. package/actions/get-channel-details/get-channel-details.mjs +6 -3
  14. package/actions/get-channel-history/get-channel-history.mjs +15 -2
  15. package/actions/get-current-user/get-current-user.mjs +2 -2
  16. package/actions/get-file/get-file.mjs +1 -1
  17. package/actions/get-thread-replies/get-thread-replies.mjs +17 -2
  18. package/actions/get-user-details/get-user-details.mjs +8 -4
  19. package/actions/invite-user-to-channel/invite-user-to-channel.mjs +72 -14
  20. package/actions/kick-user/kick-user.mjs +1 -1
  21. package/actions/list-channels/list-channels.mjs +54 -6
  22. package/actions/list-emojis/list-emojis.mjs +1 -1
  23. package/actions/list-files/list-files.mjs +1 -1
  24. package/actions/list-group-members/list-group-members.mjs +1 -1
  25. package/actions/list-icon-emoji-options/list-icon-emoji-options.mjs +1 -1
  26. package/actions/list-members-in-channel/list-members-in-channel.mjs +9 -5
  27. package/actions/list-messages/list-messages.mjs +1 -1
  28. package/actions/list-reminder-options/list-reminder-options.mjs +1 -1
  29. package/actions/list-replies/list-replies.mjs +1 -1
  30. package/actions/list-user-group-options/list-user-group-options.mjs +1 -1
  31. package/actions/list-users/list-users.mjs +1 -1
  32. package/actions/post-message/post-message.mjs +1 -1
  33. package/actions/reply-to-a-message/reply-to-a-message.mjs +1 -1
  34. package/actions/search/search.mjs +1 -1
  35. package/actions/send-block-kit-message/send-block-kit-message.mjs +1 -1
  36. package/actions/send-large-message/send-large-message.mjs +1 -1
  37. package/actions/send-message/send-message.mjs +1 -1
  38. package/actions/send-message-advanced/send-message-advanced.mjs +1 -1
  39. package/actions/send-message-to-channel/send-message-to-channel.mjs +1 -1
  40. package/actions/send-message-to-user-or-group/send-message-to-user-or-group.mjs +1 -1
  41. package/actions/set-channel-description/set-channel-description.mjs +1 -1
  42. package/actions/set-channel-topic/set-channel-topic.mjs +7 -4
  43. package/actions/set-status/set-status.mjs +1 -1
  44. package/actions/update-group-members/update-group-members.mjs +1 -1
  45. package/actions/update-message/update-message.mjs +1 -1
  46. package/actions/update-profile/update-profile.mjs +1 -1
  47. package/actions/upload-file/upload-file.mjs +1 -1
  48. package/actions/verify-slack-signature/verify-slack-signature.mjs +1 -1
  49. package/common/constants.mjs +5 -0
  50. package/common/utils.mjs +69 -0
  51. package/package.json +1 -1
  52. package/slack_v2.app.mjs +165 -24
  53. package/sources/new-channel-created/new-channel-created.mjs +1 -1
  54. package/sources/new-interaction-event-received/new-interaction-event-received.mjs +1 -1
  55. package/sources/new-keyword-mention/new-keyword-mention.mjs +1 -1
  56. package/sources/new-message-in-channels/new-message-in-channels.mjs +1 -1
  57. package/sources/new-private-channel-created/new-private-channel-created.mjs +1 -1
  58. package/sources/new-reaction-added/new-reaction-added.mjs +1 -1
  59. package/sources/new-saved-message/new-saved-message.mjs +1 -1
  60. package/sources/new-user-added/new-user-added.mjs +1 -1
  61. package/sources/new-user-mention/new-user-mention.mjs +1 -1
  62. package/actions/approve-workflow/approve-workflow.mjs +0 -92
@@ -3,8 +3,8 @@ import slack from "../../slack_v2.app.mjs";
3
3
  export default {
4
4
  key: "slack_v2-list-members-in-channel",
5
5
  name: "List Members in Channel",
6
- description: "Retrieve members of a channel. [See the documentation](https://api.slack.com/methods/conversations.members)",
7
- version: "0.0.28",
6
+ description: "Retrieve members of a channel. Accepts a channel ID or NAME (e.g. general or #general) — names are resolved automatically. [See the documentation](https://api.slack.com/methods/conversations.members)",
7
+ version: "0.1.0",
8
8
  annotations: {
9
9
  destructiveHint: false,
10
10
  openWorldHint: true,
@@ -22,7 +22,7 @@ export default {
22
22
  returnUsernames: {
23
23
  type: "boolean",
24
24
  label: "Return Usernames",
25
- description: "Optionally, return usernames in addition to IDs",
25
+ description: "Optionally, return usernames in addition to IDs. The username may be `null` for members whose name cannot be resolved, such as external (Slack Connect) users.",
26
26
  optional: true,
27
27
  },
28
28
  pageSize: {
@@ -40,8 +40,12 @@ export default {
40
40
  },
41
41
  async run({ $ }) {
42
42
  let channelMembers = [];
43
+ // Accept a channel NAME as well as an ID. Every AI-optimized tool in this app resolves
44
+ // names server-side, so an agent that read "#seinfeld-general" from the prompt
45
+ // reasonably passes it here too — and used to get channel_not_found.
46
+ const channel = await this.slack.resolveChannelId(this.conversation);
43
47
  const params = {
44
- channel: this.conversation,
48
+ channel,
45
49
  limit: this.pageSize,
46
50
  };
47
51
  let page = 0;
@@ -59,7 +63,7 @@ export default {
59
63
  const usernames = await this.slack.userNameLookup(channelMembers);
60
64
  channelMembers = channelMembers?.map((id) => ({
61
65
  id,
62
- username: usernames[id],
66
+ username: usernames[id] ?? null,
63
67
  })) || [];
64
68
  }
65
69
 
@@ -5,7 +5,7 @@ export default {
5
5
  name: "List Messages",
6
6
  description:
7
7
  "Retrieve messages from a Slack conversation, including reactions. [See the documentation](https://api.slack.com/methods/conversations.history)",
8
- version: "0.0.3",
8
+ version: "0.0.5",
9
9
  annotations: {
10
10
  destructiveHint: false,
11
11
  openWorldHint: true,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-list-reminder-options",
5
5
  name: "List Reminder Options",
6
6
  description: "Retrieves available options for the Reminder field.",
7
- version: "0.0.1",
7
+ version: "0.0.2",
8
8
  type: "action",
9
9
  annotations: {
10
10
  destructiveHint: false,
@@ -5,7 +5,7 @@ export default {
5
5
  key: "slack_v2-list-replies",
6
6
  name: "List Replies",
7
7
  description: "Retrieve a thread of messages posted to a conversation. [See the documentation](https://api.slack.com/methods/conversations.replies)",
8
- version: "0.0.28",
8
+ version: "0.0.30",
9
9
  annotations: {
10
10
  destructiveHint: false,
11
11
  openWorldHint: true,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-list-user-group-options",
5
5
  name: "List User Group Options",
6
6
  description: "Retrieves available options for the User Group field.",
7
- version: "0.0.1",
7
+ version: "0.0.2",
8
8
  type: "action",
9
9
  annotations: {
10
10
  destructiveHint: false,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-list-users",
5
5
  name: "List Users",
6
6
  description: "Return a list of all users in a workspace. [See the documentation](https://api.slack.com/methods/users.list)",
7
- version: "0.0.28",
7
+ version: "0.0.29",
8
8
  annotations: {
9
9
  destructiveHint: false,
10
10
  openWorldHint: true,
@@ -10,7 +10,7 @@ export default {
10
10
  + " To reply to a thread, provide `thread_ts` from **Get Channel History**."
11
11
  + " Supports plain text with Slack mrkdwn formatting and Block Kit blocks."
12
12
  + " [See the documentation](https://api.slack.com/methods/chat.postMessage)",
13
- version: "0.0.1",
13
+ version: "0.0.2",
14
14
  type: "action",
15
15
  annotations: {
16
16
  destructiveHint: false,
@@ -6,7 +6,7 @@ export default {
6
6
  key: "slack_v2-reply-to-a-message",
7
7
  name: "Reply to a Message Thread",
8
8
  description: "Send a message as a threaded reply. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here",
9
- version: "0.2.3",
9
+ version: "0.2.5",
10
10
  annotations: {
11
11
  destructiveHint: false,
12
12
  openWorldHint: true,
@@ -9,7 +9,7 @@ export default {
9
9
  + " Use **Get User Details** first to find your user ID for filtering by 'my' messages."
10
10
  + " Returns matching messages with channel context, timestamps, and permalinks."
11
11
  + " [See the documentation](https://api.slack.com/methods/assistant.search.context)",
12
- version: "0.0.1",
12
+ version: "0.0.2",
13
13
  type: "action",
14
14
  annotations: {
15
15
  destructiveHint: false,
@@ -7,7 +7,7 @@ export default {
7
7
  key: "slack_v2-send-block-kit-message",
8
8
  name: "Build and Send a Block Kit Message",
9
9
  description: "Configure custom blocks and send to a channel, group, or user. [See the documentation](https://api.slack.com/tools/block-kit-builder).",
10
- version: "0.5.3",
10
+ version: "0.5.5",
11
11
  annotations: {
12
12
  destructiveHint: false,
13
13
  openWorldHint: true,
@@ -5,7 +5,7 @@ export default {
5
5
  key: "slack_v2-send-large-message",
6
6
  name: "Send a Large Message (3000+ characters)",
7
7
  description: "Send a large message (more than 3000 characters) to a channel, group or user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here",
8
- version: "0.1.3",
8
+ version: "0.1.5",
9
9
  annotations: {
10
10
  destructiveHint: false,
11
11
  openWorldHint: true,
@@ -6,7 +6,7 @@ export default {
6
6
  key: "slack_v2-send-message",
7
7
  name: "Send Message",
8
8
  description: "Send a message to a user, group, private channel or public channel. [See the documentation](https://api.slack.com/methods/chat.postMessage)",
9
- version: "0.1.3",
9
+ version: "0.1.5",
10
10
  annotations: {
11
11
  destructiveHint: false,
12
12
  openWorldHint: true,
@@ -7,7 +7,7 @@ export default {
7
7
  key: "slack_v2-send-message-advanced",
8
8
  name: "Send Message (Advanced)",
9
9
  description: "Customize advanced settings and send a message to a channel, group or user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here",
10
- version: "0.1.3",
10
+ version: "0.1.5",
11
11
  annotations: {
12
12
  destructiveHint: false,
13
13
  openWorldHint: true,
@@ -6,7 +6,7 @@ export default {
6
6
  key: "slack_v2-send-message-to-channel",
7
7
  name: "Send Message to Channel",
8
8
  description: "Send a message to a public or private channel. [See the documentation](https://api.slack.com/methods/chat.postMessage)",
9
- version: "0.1.3",
9
+ version: "0.1.5",
10
10
  annotations: {
11
11
  destructiveHint: false,
12
12
  openWorldHint: true,
@@ -7,7 +7,7 @@ export default {
7
7
  key: "slack_v2-send-message-to-user-or-group",
8
8
  name: "Send Message to User or Group",
9
9
  description: "Send a message to a user or group. [See the documentation](https://api.slack.com/methods/chat.postMessage)",
10
- version: "0.1.3",
10
+ version: "0.1.5",
11
11
  annotations: {
12
12
  destructiveHint: false,
13
13
  openWorldHint: true,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-set-channel-description",
5
5
  name: "Set Channel Description",
6
6
  description: "Change the description or purpose of a channel. [See the documentation](https://api.slack.com/methods/conversations.setPurpose)",
7
- version: "0.0.13",
7
+ version: "0.0.15",
8
8
  annotations: {
9
9
  destructiveHint: false,
10
10
  openWorldHint: true,
@@ -3,8 +3,8 @@ import slack from "../../slack_v2.app.mjs";
3
3
  export default {
4
4
  key: "slack_v2-set-channel-topic",
5
5
  name: "Set Channel Topic",
6
- description: "Set the topic on a selected channel. [See the documentation](https://api.slack.com/methods/conversations.setTopic)",
7
- version: "0.0.28",
6
+ description: "Set the topic on a channel, specified by ID or by name — names are resolved automatically. [See the documentation](https://api.slack.com/methods/conversations.setTopic)",
7
+ version: "0.1.0",
8
8
  annotations: {
9
9
  destructiveHint: true,
10
10
  openWorldHint: true,
@@ -27,11 +27,14 @@ export default {
27
27
  },
28
28
  },
29
29
  async run({ $ }) {
30
+ // Accept a channel NAME as well as an ID. Slack answers a name here with a bare
31
+ // `internal_error`, which tells an agent nothing about what it did wrong.
32
+ const channel = await this.slack.resolveChannelId(this.conversation);
30
33
  const response = await this.slack.setChannelTopic({
31
- channel: this.conversation,
34
+ channel,
32
35
  topic: this.topic,
33
36
  });
34
- $.export("$summary", `Successfully set topic for channel with ID ${this.conversation}`);
37
+ $.export("$summary", `Successfully set topic for channel with ID ${channel}`);
35
38
  return response;
36
39
  },
37
40
  };
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-set-status",
5
5
  name: "Set Status",
6
6
  description: "Set the current status for a user. [See the documentation](https://api.slack.com/methods/users.profile.set)",
7
- version: "0.0.13",
7
+ version: "0.0.14",
8
8
  annotations: {
9
9
  destructiveHint: true,
10
10
  openWorldHint: true,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-update-group-members",
5
5
  name: "Update Group Members",
6
6
  description: "Update the list of users for a User Group. [See the documentation](https://api.slack.com/methods/usergroups.users.update)",
7
- version: "0.0.13",
7
+ version: "0.0.15",
8
8
  annotations: {
9
9
  destructiveHint: true,
10
10
  openWorldHint: true,
@@ -4,7 +4,7 @@ export default {
4
4
  key: "slack_v2-update-message",
5
5
  name: "Update Message",
6
6
  description: "Update a message. [See the documentation](https://api.slack.com/methods/chat.update)",
7
- version: "0.2.3",
7
+ version: "0.2.5",
8
8
  annotations: {
9
9
  destructiveHint: true,
10
10
  openWorldHint: true,
@@ -5,7 +5,7 @@ export default {
5
5
  key: "slack_v2-update-profile",
6
6
  name: "Update Profile",
7
7
  description: "Update basic profile field such as name or title. [See the documentation](https://api.slack.com/methods/users.profile.set)",
8
- version: "0.0.28",
8
+ version: "0.0.30",
9
9
  annotations: {
10
10
  destructiveHint: true,
11
11
  openWorldHint: true,
@@ -8,7 +8,7 @@ export default {
8
8
  key: "slack_v2-upload-file",
9
9
  name: "Upload File",
10
10
  description: "Upload a file. [See the documentation](https://api.slack.com/messaging/files#uploading_files)",
11
- version: "0.1.7",
11
+ version: "0.1.9",
12
12
  annotations: {
13
13
  destructiveHint: false,
14
14
  openWorldHint: true,
@@ -5,7 +5,7 @@ export default {
5
5
  key: "slack_v2-verify-slack-signature",
6
6
  name: "Verify Slack Signature",
7
7
  description: "Verifying requests from Slack, slack signs its requests using a secret that's unique to your app. [See the documentation](https://api.slack.com/authentication/verifying-requests-from-slack)",
8
- version: "0.0.3",
8
+ version: "0.0.4",
9
9
  annotations: {
10
10
  destructiveHint: false,
11
11
  openWorldHint: true,
@@ -1,5 +1,9 @@
1
1
  const MAX_RESOURCES = 800;
2
2
  const LIMIT = 250;
3
+ // Caps `users.list` pagination in name lookups, since unresolvable ids (e.g.
4
+ // external Slack Connect users) would otherwise force a full directory scan
5
+ // and exhaust the method's rate limit.
6
+ const MAX_NAME_LOOKUP_PAGES = 5;
3
7
 
4
8
  const CHANNEL_TYPE = {
5
9
  PUBLIC: "public_channel",
@@ -26,6 +30,7 @@ const CHANNEL_TYPE_OPTIONS = [
26
30
  export default {
27
31
  MAX_RESOURCES,
28
32
  LIMIT,
33
+ MAX_NAME_LOOKUP_PAGES,
29
34
  CHANNEL_TYPE,
30
35
  CHANNEL_TYPE_OPTIONS,
31
36
  };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared helpers for the `fields` projection offered by the list/history actions.
3
+ *
4
+ * Three actions (Get Channel History, Get Thread Replies, List Channels) let the caller
5
+ * name the properties they want back. Both halves of that feature — normalizing the prop
6
+ * value and applying it — were copy-pasted into each action; keeping one copy means a fix
7
+ * to either lands everywhere at once.
8
+ */
9
+
10
+ /**
11
+ * Normalize a `string[]` prop that an LLM caller may well send as a CSV string.
12
+ *
13
+ * `fields` is declared `string[]`, but MCP/LLM callers routinely pass `"text,ts,user"`.
14
+ * Spreading a bare string into a keep-set yields single characters, which match no key,
15
+ * so the caller would silently get back nothing at all.
16
+ *
17
+ * @param {string|string[]|undefined} value - the raw prop value
18
+ * @returns {string[]|null} the requested field names, or null when none were requested
19
+ */
20
+ export function parseFields(value) {
21
+ if (Array.isArray(value)) return value;
22
+ if (typeof value === "string" && value.length) {
23
+ return value.split(",")
24
+ .map((f) => f.trim())
25
+ .filter(Boolean);
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Keep only the requested properties of a record.
32
+ *
33
+ * Unknown names are ignored rather than returned as `undefined`, so a typo shrinks the
34
+ * payload instead of corrupting it with null-valued keys.
35
+ *
36
+ * @param {object} record - a message or channel object
37
+ * @param {string[]} fields - property names to keep
38
+ * @returns {object} the projected record
39
+ */
40
+ export function pickFields(record, fields) {
41
+ const out = {};
42
+ for (const field of fields) {
43
+ if (record[field] !== undefined) out[field] = record[field];
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * Apply a `fields` projection to a list, or return it untouched when none was requested.
50
+ *
51
+ * The no-fields path returns the ORIGINAL array, which is what makes `fields` additive:
52
+ * a caller that omits it gets exactly what these actions have always returned.
53
+ *
54
+ * @param {object[]} records - the records to project
55
+ * @param {string|string[]|undefined} value - the raw `fields` prop value
56
+ * @returns {object[]} projected records, or `records` unchanged
57
+ */
58
+ export function projectFields(records, value) {
59
+ const fields = parseFields(value);
60
+ return fields?.length
61
+ ? records.map((r) => pickFields(r, fields))
62
+ : records;
63
+ }
64
+
65
+ export default {
66
+ parseFields,
67
+ pickFields,
68
+ projectFields,
69
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/slack_v2",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Pipedream Slack_v2 Components",
5
5
  "main": "slack_v2.app.mjs",
6
6
  "keywords": [
package/slack_v2.app.mjs CHANGED
@@ -11,32 +11,82 @@ export default {
11
11
  user: {
12
12
  type: "string",
13
13
  label: "User",
14
- description: "Select a user",
14
+ description: "Select a user, or search by name or email",
15
+ useQuery: true,
15
16
  async options({
16
- prevContext, channelId,
17
+ prevContext, channelId, query,
17
18
  }) {
18
- const types = [
19
- "im",
20
- ];
21
- let conversationsResp
22
- = await this.availableConversations(types.join(), prevContext.cursor, true);
19
+ let nameQuery = query
20
+ ?.trim()
21
+ .toLowerCase()
22
+ .replace(/^@/, "");
23
+ if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(nameQuery ?? "")) {
24
+ try {
25
+ const { user } = await this.lookupUserByEmail({
26
+ email: nameQuery,
27
+ throwRateLimitError: true,
28
+ });
29
+ return {
30
+ options: [
31
+ {
32
+ label: user.profile?.real_name || user.name,
33
+ value: user.id,
34
+ },
35
+ ],
36
+ };
37
+ } catch {
38
+ // Token may lack `users:read.email`, or no member has this email.
39
+ // Fall back to matching names below with the email's local part.
40
+ nameQuery = nameQuery.split("@")[0].replace(/[._-]+/g, " ");
41
+ }
42
+ }
43
+ let channelMembers;
23
44
  if (channelId) {
24
45
  const { members } = await this.listChannelMembers({
25
46
  channel: channelId,
26
47
  throwRateLimitError: true,
27
48
  });
28
- conversationsResp.conversations = conversationsResp.conversations
29
- .filter((c) => members.includes(c.user || c.id));
49
+ channelMembers = new Set(members);
30
50
  }
31
- const userIds = conversationsResp.conversations.map(({ user }) => user).filter(Boolean);
32
- const realNames = await this.realNameLookup(userIds);
51
+ const options = [];
52
+ let cursor = prevContext?.cursor;
53
+ let pages = 0;
54
+ do {
55
+ const {
56
+ members,
57
+ response_metadata: { next_cursor: nextCursor },
58
+ } = await this.usersList({
59
+ limit: constants.LIMIT,
60
+ cursor,
61
+ throwRateLimitError: true,
62
+ });
63
+ for (const member of members) {
64
+ const isSelectable = !member.deleted
65
+ && !member.is_bot
66
+ && member.id !== "USLACKBOT"
67
+ && (!channelMembers || channelMembers.has(member.id));
68
+ if (isSelectable) {
69
+ const names = [
70
+ member.profile?.real_name,
71
+ member.profile?.display_name,
72
+ member.name,
73
+ ];
74
+ if (!nameQuery || names.some((name) => name?.toLowerCase().includes(nameQuery))) {
75
+ options.push({
76
+ label: names.find(Boolean) ?? member.id,
77
+ value: member.id,
78
+ });
79
+ }
80
+ }
81
+ }
82
+ cursor = nextCursor;
83
+ } while (cursor
84
+ && options.length < constants.LIMIT
85
+ && ++pages < constants.MAX_NAME_LOOKUP_PAGES);
33
86
  return {
34
- options: conversationsResp.conversations.filter((c) => c.user).map((c) => ({
35
- label: `${realNames[c.user]}`,
36
- value: c.user || c.id,
37
- })),
87
+ options,
38
88
  context: {
39
- cursor: conversationsResp.cursor,
89
+ cursor,
40
90
  },
41
91
  };
42
92
  },
@@ -146,12 +196,12 @@ export default {
146
196
  options: conversations.map((c) => {
147
197
  if (c.is_im) {
148
198
  return {
149
- label: `Direct messaging with: ${realNames[c.user]}`,
199
+ label: `Direct messaging with: ${realNames?.[c.user] ?? c.user}`,
150
200
  value: c.id,
151
201
  };
152
202
  } else if (c.is_mpim) {
153
203
  const usernames = c.purpose.value.match(/@[\w.-]+/g) || [];
154
- const realnames = usernames.map((u) => realNames[u.slice(1)] || u);
204
+ const realnames = usernames.map((u) => realNames?.[u.slice(1)] || u);
155
205
  return {
156
206
  label: realnames.length
157
207
  ? `Group messaging with: ${realnames.join(", ")}`
@@ -205,8 +255,11 @@ export default {
205
255
  .filter(channelsFilter)
206
256
  .map((c) => {
207
257
  if (c.is_im) {
258
+ const userName = userNames?.[c.user];
208
259
  return {
209
- label: `Direct messaging with: @${userNames[c.user]}`,
260
+ label: `Direct messaging with: ${userName
261
+ ? `@${userName}`
262
+ : c.user}`,
210
263
  value: c.id,
211
264
  };
212
265
  } else if (c.is_mpim) {
@@ -626,8 +679,13 @@ export default {
626
679
  conversations,
627
680
  };
628
681
  },
682
+ // Resolves user names by scanning `users.list`, capped at
683
+ // MAX_NAME_LOOKUP_PAGES pages per call: some ids may be unresolvable
684
+ // (e.g. external Slack Connect users), so callers must handle missing
685
+ // entries in the result.
629
686
  async userNameLookup(ids = [], throwRateLimitError = true, args = {}) {
630
687
  let cursor;
688
+ let pages = 0;
631
689
  const userNames = {};
632
690
  do {
633
691
  const {
@@ -647,13 +705,21 @@ export default {
647
705
  }
648
706
 
649
707
  cursor = nextCursor;
650
- } while (cursor && Object.keys(userNames).length < ids.length);
708
+ } while (cursor
709
+ && ++pages < constants.MAX_NAME_LOOKUP_PAGES
710
+ && Object.keys(userNames).length < ids.length);
651
711
  return userNames;
652
712
  },
713
+ // Resolves real names by scanning `users.list`, capped at
714
+ // MAX_NAME_LOOKUP_PAGES pages per call: some ids/usernames may be
715
+ // unresolvable (e.g. external Slack Connect users, or stale usernames in
716
+ // mpim purpose text), so callers must handle missing entries in the
717
+ // result.
653
718
  async realNameLookup(ids = [], usernames = [], throwRateLimitError = true, args = {}) {
654
719
  const idSet = new Set(ids);
655
720
  const usernameSet = new Set(usernames);
656
721
  let cursor;
722
+ let pages = 0;
657
723
  const realNames = {};
658
724
  const targetCount = ids.length + usernames.length;
659
725
  do {
@@ -677,7 +743,9 @@ export default {
677
743
  }
678
744
 
679
745
  cursor = nextCursor;
680
- } while (cursor && Object.keys(realNames).length < targetCount);
746
+ } while (cursor
747
+ && ++pages < constants.MAX_NAME_LOOKUP_PAGES
748
+ && Object.keys(realNames).length < targetCount);
681
749
  return realNames;
682
750
  },
683
751
  async maybeAddAppToChannels(channelIds = []) {
@@ -899,10 +967,15 @@ export default {
899
967
  assistantSearch(args = {}) {
900
968
  args.count ||= constants.LIMIT;
901
969
  // Uses apiCall directly since assistant.search.context is not exposed as
902
- // a method on WebClient
903
- return this.sdk().apiCall("assistant.search.context", {
970
+ // a method on WebClient — but it must STILL go through _withRetries. Calling
971
+ // apiCall bare was the one path in this app that skipped the retry wrapper, and
972
+ // the client is built with `rejectRateLimitedCalls: true`, so a 429 rejected
973
+ // instantly instead of backing off. assistant.search.context rate-limits readily
974
+ // (Slack returns retryAfter: 60), which surfaced to agents as a hard tool error
975
+ // on 8 of 12 search calls in one eval run.
976
+ return this._withRetries(() => this.sdk().apiCall("assistant.search.context", {
904
977
  ...args,
905
- });
978
+ }));
906
979
  },
907
980
  /**
908
981
  * Lists reactions made by a user.
@@ -1124,6 +1197,74 @@ export default {
1124
1197
  normalizeChannel(input) {
1125
1198
  return input.replace(/^#/, "");
1126
1199
  },
1200
+ /**
1201
+ * Accept a user ID, an email, or a display/real name and return the user ID.
1202
+ * Agents naturally pass whatever identifier the user said — an email in
1203
+ * "invite dylan@pipedream.com" — and the raw API answers `user_not_found`,
1204
+ * which reads as a broken tool rather than a wrong-shaped argument.
1205
+ */
1206
+ async resolveUserId(input) {
1207
+ if (/^[UW][A-Z0-9]{6,}$/i.test(input)) return input;
1208
+ if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input)) {
1209
+ const { user } = await this.makeRequest({
1210
+ method: "users.lookupByEmail",
1211
+ email: input,
1212
+ });
1213
+ return user.id;
1214
+ }
1215
+ const name = input.replace(/^@/, "").toLowerCase();
1216
+ const matches = [];
1217
+ let cursor;
1218
+ do {
1219
+ const {
1220
+ members, response_metadata: metadata,
1221
+ } = await this.makeRequest({
1222
+ method: "users.list",
1223
+ limit: 200,
1224
+ cursor,
1225
+ });
1226
+ for (const member of members) {
1227
+ const isMatch = [
1228
+ member.name,
1229
+ member.real_name,
1230
+ member.profile?.display_name,
1231
+ ].filter(Boolean).some((n) => String(n).toLowerCase() === name);
1232
+ if (isMatch) matches.push(member);
1233
+ }
1234
+ cursor = metadata?.next_cursor;
1235
+ } while (cursor);
1236
+ if (matches.length === 1) return matches[0].id;
1237
+ if (matches.length > 1) {
1238
+ throw new ConfigurationError(`Multiple users match "${input}". Provide a user ID or an email address instead.`);
1239
+ }
1240
+ throw new ConfigurationError(`User "${input}" not found. Provide a user ID, an email address, or an exact display name.`);
1241
+ },
1242
+ /**
1243
+ * Resolve one OR MORE user identifiers to the comma-separated `users` string Slack's
1244
+ * conversations.* methods accept.
1245
+ *
1246
+ * The list form has to keep working: `users` is documented as comma-separated, the prop
1247
+ * is a plain string, and callers that predate resolveUserId() pass "U123,U456" straight
1248
+ * through. Resolving the whole string as a single identifier would send it to an
1249
+ * exhaustive users.list scan that cannot match, then throw.
1250
+ *
1251
+ * @param {string|string[]} input - id / email / display name, or a comma-separated list
1252
+ * or array of them
1253
+ * @returns {Promise<string>} Comma-separated user IDs
1254
+ */
1255
+ async resolveUserIds(input) {
1256
+ const raw = Array.isArray(input)
1257
+ ? input
1258
+ : String(input).split(",");
1259
+ const tokens = raw
1260
+ .map((t) => String(t).trim())
1261
+ .filter(Boolean);
1262
+ const ids = [];
1263
+ for (const token of tokens) {
1264
+ ids.push(await this.resolveUserId(token));
1265
+ }
1266
+ return ids.join(",");
1267
+ },
1127
1268
  /**
1128
1269
  * Resolves a channel name (e.g. "general", "#general") to its ID.
1129
1270
  * If the input already looks like an ID (starts with C/D/G/U + 8+ alphanums),
@@ -5,7 +5,7 @@ export default {
5
5
  ...common,
6
6
  key: "slack_v2-new-channel-created",
7
7
  name: "New Channel Created (Instant)",
8
- version: "0.0.14",
8
+ version: "0.0.15",
9
9
  description: "Emit new event when a new channel is created.",
10
10
  type: "source",
11
11
  dedupe: "unique",