@chat-adapter/slack 4.20.1 → 4.20.2

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/dist/index.d.ts CHANGED
@@ -175,6 +175,7 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
175
175
  private readonly formatConverter;
176
176
  private static USER_CACHE_TTL_MS;
177
177
  private static CHANNEL_CACHE_TTL_MS;
178
+ private static REVERSE_INDEX_TTL_MS;
178
179
  private readonly clientId;
179
180
  private readonly clientSecret;
180
181
  private readonly encryptionKey;
@@ -291,6 +292,7 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
291
292
  * Fires when a user (including the bot) joins a channel.
292
293
  */
293
294
  private handleMemberJoinedChannel;
295
+ private handleUserChange;
294
296
  /**
295
297
  * Publish a Home tab view for a user.
296
298
  * Slack API: views.publish
@@ -342,14 +344,24 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
342
344
  * Includes a fetchData method that uses the bot token for auth.
343
345
  */
344
346
  private createAttachment;
347
+ /**
348
+ * Resolve @name mentions in text to Slack <@USER_ID> format using the
349
+ * reverse user cache. When multiple users share a display name, prefers
350
+ * the one who is a participant in the given thread.
351
+ */
352
+ private resolveOutgoingMentions;
353
+ /**
354
+ * Pre-process an outgoing message to resolve @name mentions before rendering.
355
+ */
356
+ private resolveMessageMentions;
345
357
  /**
346
358
  * Try to render a message using native Slack table blocks.
347
359
  * Returns blocks + fallback text if the message contains tables, null otherwise.
348
360
  */
349
361
  private renderWithTableBlocks;
350
- postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
351
- postEphemeral(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
352
- scheduleMessage(threadId: string, message: AdapterPostableMessage, options: {
362
+ postMessage(threadId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
363
+ postEphemeral(threadId: string, userId: string, _message: AdapterPostableMessage): Promise<EphemeralMessage>;
364
+ scheduleMessage(threadId: string, _message: AdapterPostableMessage, options: {
353
365
  postAt: Date;
354
366
  }): Promise<ScheduledMessage>;
355
367
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
@@ -363,7 +375,7 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
363
375
  * Returns the file IDs of uploaded files.
364
376
  */
365
377
  private uploadFiles;
366
- editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
378
+ editMessage(threadId: string, messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
367
379
  deleteMessage(threadId: string, messageId: string): Promise<void>;
368
380
  addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
369
381
  removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
package/dist/index.js CHANGED
@@ -708,6 +708,7 @@ function radioSelectToBlock(radioSelect) {
708
708
 
709
709
  // src/index.ts
710
710
  var SLACK_USER_ID_PATTERN = /^[A-Z0-9_]+$/;
711
+ var SLACK_USER_ID_EXACT_PATTERN = /^U[A-Z0-9]+$/;
711
712
  function findNextMention(text) {
712
713
  const atIdx = text.indexOf("<@");
713
714
  const hashIdx = text.indexOf("<#");
@@ -732,10 +733,12 @@ var SlackAdapter = class _SlackAdapter {
732
733
  _botId = null;
733
734
  // Bot app ID (B_xxx) - different from user ID
734
735
  formatConverter = new SlackFormatConverter();
735
- static USER_CACHE_TTL_MS = 60 * 60 * 1e3;
736
- // 1 hour
737
- static CHANNEL_CACHE_TTL_MS = 60 * 60 * 1e3;
738
- // 1 hour
736
+ static USER_CACHE_TTL_MS = 8 * 24 * 60 * 60 * 1e3;
737
+ // 8 days
738
+ static CHANNEL_CACHE_TTL_MS = 8 * 24 * 60 * 60 * 1e3;
739
+ // 8 days
740
+ static REVERSE_INDEX_TTL_MS = 8 * 24 * 60 * 60 * 1e3;
741
+ // 8 days
739
742
  // Multi-workspace support
740
743
  clientId;
741
744
  clientSecret;
@@ -1006,6 +1009,15 @@ var SlackAdapter = class _SlackAdapter {
1006
1009
  { displayName, realName },
1007
1010
  _SlackAdapter.USER_CACHE_TTL_MS
1008
1011
  );
1012
+ const normalizedName = displayName.toLowerCase();
1013
+ const reverseKey = `slack:user-by-name:${normalizedName}`;
1014
+ const existing = await this.chat.getState().getList(reverseKey);
1015
+ if (!existing.includes(userId)) {
1016
+ await this.chat.getState().appendToList(reverseKey, userId, {
1017
+ maxLength: 50,
1018
+ ttlMs: _SlackAdapter.REVERSE_INDEX_TTL_MS
1019
+ });
1020
+ }
1009
1021
  }
1010
1022
  this.logger.debug("Fetched user info", {
1011
1023
  userId,
@@ -1144,6 +1156,8 @@ var SlackAdapter = class _SlackAdapter {
1144
1156
  event,
1145
1157
  options
1146
1158
  );
1159
+ } else if (event.type === "user_change") {
1160
+ this.handleUserChange(event);
1147
1161
  }
1148
1162
  }
1149
1163
  }
@@ -1650,6 +1664,19 @@ var SlackAdapter = class _SlackAdapter {
1650
1664
  options
1651
1665
  );
1652
1666
  }
1667
+ async handleUserChange(event) {
1668
+ if (!this.chat) {
1669
+ return;
1670
+ }
1671
+ try {
1672
+ await this.chat.getState().delete(`slack:user:${event.user.id}`);
1673
+ } catch (error) {
1674
+ this.logger.warn("Failed to invalidate user cache", {
1675
+ userId: event.user.id,
1676
+ error
1677
+ });
1678
+ }
1679
+ }
1653
1680
  /**
1654
1681
  * Publish a Home tab view for a user.
1655
1682
  * Slack API: views.publish
@@ -1864,6 +1891,24 @@ var SlackAdapter = class _SlackAdapter {
1864
1891
  userName = userInfo.displayName;
1865
1892
  fullName = userInfo.realName;
1866
1893
  }
1894
+ if (event.user && this.chat) {
1895
+ try {
1896
+ const participantKey = `slack:thread-participants:${threadId}`;
1897
+ const participants = await this.chat.getState().getList(participantKey);
1898
+ if (!participants.includes(event.user)) {
1899
+ await this.chat.getState().appendToList(participantKey, event.user, {
1900
+ maxLength: 100,
1901
+ ttlMs: _SlackAdapter.REVERSE_INDEX_TTL_MS
1902
+ });
1903
+ }
1904
+ } catch (error) {
1905
+ this.logger.warn("Failed to track thread participant", {
1906
+ threadId,
1907
+ userId: event.user,
1908
+ error
1909
+ });
1910
+ }
1911
+ }
1867
1912
  const text = await this.resolveInlineMentions(rawText, skipSelfMention);
1868
1913
  return new Message({
1869
1914
  id: event.ts || "",
@@ -1936,6 +1981,107 @@ var SlackAdapter = class _SlackAdapter {
1936
1981
  } : void 0
1937
1982
  };
1938
1983
  }
1984
+ /**
1985
+ * Resolve @name mentions in text to Slack <@USER_ID> format using the
1986
+ * reverse user cache. When multiple users share a display name, prefers
1987
+ * the one who is a participant in the given thread.
1988
+ */
1989
+ async resolveOutgoingMentions(text, threadId) {
1990
+ if (!this.chat) {
1991
+ return text;
1992
+ }
1993
+ const state = this.chat.getState();
1994
+ const mentionPattern = /@(\w+)/g;
1995
+ const mentions = /* @__PURE__ */ new Map();
1996
+ for (const match of text.matchAll(mentionPattern)) {
1997
+ const name = match[1];
1998
+ if (SLACK_USER_ID_EXACT_PATTERN.test(name)) {
1999
+ continue;
2000
+ }
2001
+ const idx = match.index;
2002
+ if (idx > 0 && text[idx - 1] === "<") {
2003
+ continue;
2004
+ }
2005
+ if (!mentions.has(name.toLowerCase())) {
2006
+ mentions.set(name.toLowerCase(), []);
2007
+ }
2008
+ }
2009
+ if (mentions.size === 0) {
2010
+ return text;
2011
+ }
2012
+ for (const name of mentions.keys()) {
2013
+ const userIds = await state.getList(`slack:user-by-name:${name}`);
2014
+ const unique = [...new Set(userIds)];
2015
+ mentions.set(name, unique);
2016
+ }
2017
+ let participants = null;
2018
+ const needsParticipants = [...mentions.values()].some(
2019
+ (ids) => ids.length > 1
2020
+ );
2021
+ if (needsParticipants) {
2022
+ const participantList = await state.getList(
2023
+ `slack:thread-participants:${threadId}`
2024
+ );
2025
+ participants = new Set(participantList);
2026
+ }
2027
+ return text.replace(
2028
+ mentionPattern,
2029
+ (match, name, offset) => {
2030
+ if (offset > 0 && text[offset - 1] === "<") {
2031
+ return match;
2032
+ }
2033
+ if (SLACK_USER_ID_EXACT_PATTERN.test(name)) {
2034
+ return match;
2035
+ }
2036
+ const userIds = mentions.get(name.toLowerCase());
2037
+ if (!userIds || userIds.length === 0) {
2038
+ return match;
2039
+ }
2040
+ if (userIds.length === 1) {
2041
+ return `<@${userIds[0]}>`;
2042
+ }
2043
+ if (participants) {
2044
+ const inThread = userIds.filter((id) => participants.has(id));
2045
+ if (inThread.length === 1) {
2046
+ return `<@${inThread[0]}>`;
2047
+ }
2048
+ }
2049
+ return match;
2050
+ }
2051
+ );
2052
+ }
2053
+ /**
2054
+ * Pre-process an outgoing message to resolve @name mentions before rendering.
2055
+ */
2056
+ async resolveMessageMentions(message, threadId) {
2057
+ if (!this.chat) {
2058
+ return message;
2059
+ }
2060
+ if (typeof message === "string") {
2061
+ return this.resolveOutgoingMentions(message, threadId);
2062
+ }
2063
+ if (typeof message === "object" && message !== null) {
2064
+ if ("raw" in message && typeof message.raw === "string") {
2065
+ return {
2066
+ ...message,
2067
+ raw: await this.resolveOutgoingMentions(
2068
+ message.raw,
2069
+ threadId
2070
+ )
2071
+ };
2072
+ }
2073
+ if ("markdown" in message && typeof message.markdown === "string") {
2074
+ return {
2075
+ ...message,
2076
+ markdown: await this.resolveOutgoingMentions(
2077
+ message.markdown,
2078
+ threadId
2079
+ )
2080
+ };
2081
+ }
2082
+ }
2083
+ return message;
2084
+ }
1939
2085
  /**
1940
2086
  * Try to render a message using native Slack table blocks.
1941
2087
  * Returns blocks + fallback text if the message contains tables, null otherwise.
@@ -1962,7 +2108,8 @@ var SlackAdapter = class _SlackAdapter {
1962
2108
  );
1963
2109
  return { text: fallbackText, blocks };
1964
2110
  }
1965
- async postMessage(threadId, message) {
2111
+ async postMessage(threadId, _message) {
2112
+ const message = await this.resolveMessageMentions(_message, threadId);
1966
2113
  const { channel, threadTs } = this.decodeThreadId(threadId);
1967
2114
  try {
1968
2115
  const files = extractFiles(message);
@@ -2066,7 +2213,8 @@ var SlackAdapter = class _SlackAdapter {
2066
2213
  this.handleSlackError(error);
2067
2214
  }
2068
2215
  }
2069
- async postEphemeral(threadId, userId, message) {
2216
+ async postEphemeral(threadId, userId, _message) {
2217
+ const message = await this.resolveMessageMentions(_message, threadId);
2070
2218
  const { channel, threadTs } = this.decodeThreadId(threadId);
2071
2219
  try {
2072
2220
  const card = extractCard(message);
@@ -2159,7 +2307,8 @@ var SlackAdapter = class _SlackAdapter {
2159
2307
  this.handleSlackError(error);
2160
2308
  }
2161
2309
  }
2162
- async scheduleMessage(threadId, message, options) {
2310
+ async scheduleMessage(threadId, _message, options) {
2311
+ const message = await this.resolveMessageMentions(_message, threadId);
2163
2312
  const { channel, threadTs } = this.decodeThreadId(threadId);
2164
2313
  const postAtUnix = Math.floor(options.postAt.getTime() / 1e3);
2165
2314
  if (postAtUnix <= Math.floor(Date.now() / 1e3)) {
@@ -2345,7 +2494,8 @@ var SlackAdapter = class _SlackAdapter {
2345
2494
  }
2346
2495
  return fileIds;
2347
2496
  }
2348
- async editMessage(threadId, messageId, message) {
2497
+ async editMessage(threadId, messageId, _message) {
2498
+ const message = await this.resolveMessageMentions(_message, threadId);
2349
2499
  const ephemeral = this.decodeEphemeralMessageId(messageId);
2350
2500
  if (ephemeral) {
2351
2501
  const { threadTs } = this.decodeThreadId(threadId);