@chat-adapter/slack 4.20.0 → 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 +45 -4
- package/dist/index.js +319 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -107,6 +107,18 @@ interface SlackThreadId {
|
|
|
107
107
|
}
|
|
108
108
|
/** Slack event payload (raw message format) */
|
|
109
109
|
interface SlackEvent {
|
|
110
|
+
/** Rich text blocks containing structured elements (links, mentions, etc.) */
|
|
111
|
+
blocks?: Array<{
|
|
112
|
+
type: string;
|
|
113
|
+
elements?: Array<{
|
|
114
|
+
type: string;
|
|
115
|
+
elements?: Array<{
|
|
116
|
+
type: string;
|
|
117
|
+
url?: string;
|
|
118
|
+
text?: string;
|
|
119
|
+
}>;
|
|
120
|
+
}>;
|
|
121
|
+
}>;
|
|
110
122
|
bot_id?: string;
|
|
111
123
|
channel?: string;
|
|
112
124
|
/** Channel type: "channel", "group", "mpim", or "im" (DM) */
|
|
@@ -162,6 +174,8 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
162
174
|
private _botId;
|
|
163
175
|
private readonly formatConverter;
|
|
164
176
|
private static USER_CACHE_TTL_MS;
|
|
177
|
+
private static CHANNEL_CACHE_TTL_MS;
|
|
178
|
+
private static REVERSE_INDEX_TTL_MS;
|
|
165
179
|
private readonly clientId;
|
|
166
180
|
private readonly clientSecret;
|
|
167
181
|
private readonly encryptionKey;
|
|
@@ -222,6 +236,11 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
222
236
|
* Returns display name and real name, or falls back to user ID.
|
|
223
237
|
*/
|
|
224
238
|
private lookupUser;
|
|
239
|
+
/**
|
|
240
|
+
* Look up channel name from Slack API with caching via state adapter.
|
|
241
|
+
* Returns channel name, or falls back to channel ID.
|
|
242
|
+
*/
|
|
243
|
+
private lookupChannel;
|
|
225
244
|
handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
|
|
226
245
|
/** Extract and dispatch events from a validated payload */
|
|
227
246
|
private processEventPayload;
|
|
@@ -273,6 +292,7 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
273
292
|
* Fires when a user (including the bot) joins a channel.
|
|
274
293
|
*/
|
|
275
294
|
private handleMemberJoinedChannel;
|
|
295
|
+
private handleUserChange;
|
|
276
296
|
/**
|
|
277
297
|
* Publish a Home tab view for a user.
|
|
278
298
|
* Slack API: views.publish
|
|
@@ -307,20 +327,41 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
307
327
|
* detection doesn't apply.
|
|
308
328
|
*/
|
|
309
329
|
private resolveInlineMentions;
|
|
330
|
+
/**
|
|
331
|
+
* Extract link URLs from a Slack event.
|
|
332
|
+
* Uses the `blocks` field (rich_text blocks with link elements) when available,
|
|
333
|
+
* falling back to parsing `<url>` patterns from the text field.
|
|
334
|
+
*/
|
|
335
|
+
private extractLinks;
|
|
336
|
+
/**
|
|
337
|
+
* Create a LinkPreview for a URL. If the URL points to a Slack message,
|
|
338
|
+
* includes a `fetchMessage` callback that fetches and parses the linked message.
|
|
339
|
+
*/
|
|
340
|
+
private createLinkPreview;
|
|
310
341
|
private parseSlackMessage;
|
|
311
342
|
/**
|
|
312
343
|
* Create an Attachment object from a Slack file.
|
|
313
344
|
* Includes a fetchData method that uses the bot token for auth.
|
|
314
345
|
*/
|
|
315
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;
|
|
316
357
|
/**
|
|
317
358
|
* Try to render a message using native Slack table blocks.
|
|
318
359
|
* Returns blocks + fallback text if the message contains tables, null otherwise.
|
|
319
360
|
*/
|
|
320
361
|
private renderWithTableBlocks;
|
|
321
|
-
postMessage(threadId: string,
|
|
322
|
-
postEphemeral(threadId: string, userId: string,
|
|
323
|
-
scheduleMessage(threadId: string,
|
|
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: {
|
|
324
365
|
postAt: Date;
|
|
325
366
|
}): Promise<ScheduledMessage>;
|
|
326
367
|
openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
|
|
@@ -334,7 +375,7 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
334
375
|
* Returns the file IDs of uploaded files.
|
|
335
376
|
*/
|
|
336
377
|
private uploadFiles;
|
|
337
|
-
editMessage(threadId: string, messageId: string,
|
|
378
|
+
editMessage(threadId: string, messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
|
|
338
379
|
deleteMessage(threadId: string, messageId: string): Promise<void>;
|
|
339
380
|
addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
340
381
|
removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -708,6 +708,19 @@ 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]+$/;
|
|
712
|
+
function findNextMention(text) {
|
|
713
|
+
const atIdx = text.indexOf("<@");
|
|
714
|
+
const hashIdx = text.indexOf("<#");
|
|
715
|
+
if (atIdx === -1) {
|
|
716
|
+
return hashIdx;
|
|
717
|
+
}
|
|
718
|
+
if (hashIdx === -1) {
|
|
719
|
+
return atIdx;
|
|
720
|
+
}
|
|
721
|
+
return Math.min(atIdx, hashIdx);
|
|
722
|
+
}
|
|
723
|
+
var SLACK_MESSAGE_URL_PATTERN = /^https?:\/\/[^/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d+)(?:\?.*)?$/;
|
|
711
724
|
var SlackAdapter = class _SlackAdapter {
|
|
712
725
|
name = "slack";
|
|
713
726
|
userName;
|
|
@@ -720,8 +733,12 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
720
733
|
_botId = null;
|
|
721
734
|
// Bot app ID (B_xxx) - different from user ID
|
|
722
735
|
formatConverter = new SlackFormatConverter();
|
|
723
|
-
static USER_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
724
|
-
//
|
|
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
|
|
725
742
|
// Multi-workspace support
|
|
726
743
|
clientId;
|
|
727
744
|
clientSecret;
|
|
@@ -992,6 +1009,15 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
992
1009
|
{ displayName, realName },
|
|
993
1010
|
_SlackAdapter.USER_CACHE_TTL_MS
|
|
994
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
|
+
}
|
|
995
1021
|
}
|
|
996
1022
|
this.logger.debug("Fetched user info", {
|
|
997
1023
|
userId,
|
|
@@ -1004,6 +1030,37 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1004
1030
|
return { displayName: userId, realName: userId };
|
|
1005
1031
|
}
|
|
1006
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Look up channel name from Slack API with caching via state adapter.
|
|
1035
|
+
* Returns channel name, or falls back to channel ID.
|
|
1036
|
+
*/
|
|
1037
|
+
async lookupChannel(channelId) {
|
|
1038
|
+
const cacheKey = `slack:channel:${channelId}`;
|
|
1039
|
+
if (this.chat) {
|
|
1040
|
+
const cached = await this.chat.getState().get(cacheKey);
|
|
1041
|
+
if (cached) {
|
|
1042
|
+
return cached.name;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
const result = await this.client.conversations.info(
|
|
1047
|
+
this.withToken({ channel: channelId })
|
|
1048
|
+
);
|
|
1049
|
+
const name = result.channel?.name || channelId;
|
|
1050
|
+
if (this.chat) {
|
|
1051
|
+
await this.chat.getState().set(
|
|
1052
|
+
cacheKey,
|
|
1053
|
+
{ name },
|
|
1054
|
+
_SlackAdapter.CHANNEL_CACHE_TTL_MS
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
this.logger.debug("Fetched channel info", { channelId, name });
|
|
1058
|
+
return name;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
this.logger.warn("Could not fetch channel info", { channelId, error });
|
|
1061
|
+
return channelId;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1007
1064
|
async handleWebhook(request, options) {
|
|
1008
1065
|
const body = await request.text();
|
|
1009
1066
|
this.logger.debug("Slack webhook raw body", { body });
|
|
@@ -1099,6 +1156,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1099
1156
|
event,
|
|
1100
1157
|
options
|
|
1101
1158
|
);
|
|
1159
|
+
} else if (event.type === "user_change") {
|
|
1160
|
+
this.handleUserChange(event);
|
|
1102
1161
|
}
|
|
1103
1162
|
}
|
|
1104
1163
|
}
|
|
@@ -1605,6 +1664,19 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1605
1664
|
options
|
|
1606
1665
|
);
|
|
1607
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
|
+
}
|
|
1608
1680
|
/**
|
|
1609
1681
|
* Publish a Home tab view for a user.
|
|
1610
1682
|
* Slack API: views.publish
|
|
@@ -1668,41 +1740,56 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1668
1740
|
*/
|
|
1669
1741
|
async resolveInlineMentions(text, skipSelfMention) {
|
|
1670
1742
|
const userIds = /* @__PURE__ */ new Set();
|
|
1743
|
+
const channelIds = /* @__PURE__ */ new Set();
|
|
1671
1744
|
for (const segment of text.split("<")) {
|
|
1672
1745
|
const end = segment.indexOf(">");
|
|
1673
1746
|
if (end === -1) {
|
|
1674
1747
|
continue;
|
|
1675
1748
|
}
|
|
1676
1749
|
const inner = segment.slice(0, end);
|
|
1677
|
-
if (
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1750
|
+
if (inner.startsWith("@")) {
|
|
1751
|
+
const rest = inner.slice(1);
|
|
1752
|
+
const pipeIdx = rest.indexOf("|");
|
|
1753
|
+
const uid = pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest;
|
|
1754
|
+
if (SLACK_USER_ID_PATTERN.test(uid)) {
|
|
1755
|
+
userIds.add(uid);
|
|
1756
|
+
}
|
|
1757
|
+
} else if (inner.startsWith("#")) {
|
|
1758
|
+
const rest = inner.slice(1);
|
|
1759
|
+
const pipeIdx = rest.indexOf("|");
|
|
1760
|
+
if (pipeIdx === -1 && SLACK_USER_ID_PATTERN.test(rest)) {
|
|
1761
|
+
channelIds.add(rest);
|
|
1762
|
+
}
|
|
1685
1763
|
}
|
|
1686
1764
|
}
|
|
1687
|
-
if (userIds.size === 0) {
|
|
1765
|
+
if (userIds.size === 0 && channelIds.size === 0) {
|
|
1688
1766
|
return text;
|
|
1689
1767
|
}
|
|
1690
1768
|
if (skipSelfMention && this._botUserId) {
|
|
1691
1769
|
userIds.delete(this._botUserId);
|
|
1692
1770
|
}
|
|
1693
|
-
if (userIds.size === 0) {
|
|
1771
|
+
if (userIds.size === 0 && channelIds.size === 0) {
|
|
1694
1772
|
return text;
|
|
1695
1773
|
}
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1774
|
+
const [userLookups, channelLookups] = await Promise.all([
|
|
1775
|
+
Promise.all(
|
|
1776
|
+
[...userIds].map(async (uid) => {
|
|
1777
|
+
const info = await this.lookupUser(uid);
|
|
1778
|
+
return [uid, info.displayName];
|
|
1779
|
+
})
|
|
1780
|
+
),
|
|
1781
|
+
Promise.all(
|
|
1782
|
+
[...channelIds].map(async (cid) => {
|
|
1783
|
+
const name = await this.lookupChannel(cid);
|
|
1784
|
+
return [cid, name];
|
|
1785
|
+
})
|
|
1786
|
+
)
|
|
1787
|
+
]);
|
|
1788
|
+
const userNameMap = new Map(userLookups);
|
|
1789
|
+
const channelNameMap = new Map(channelLookups);
|
|
1703
1790
|
let result = "";
|
|
1704
1791
|
let remaining = text;
|
|
1705
|
-
let startIdx = remaining
|
|
1792
|
+
let startIdx = findNextMention(remaining);
|
|
1706
1793
|
while (startIdx !== -1) {
|
|
1707
1794
|
result += remaining.slice(0, startIdx);
|
|
1708
1795
|
remaining = remaining.slice(startIdx);
|
|
@@ -1710,20 +1797,89 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1710
1797
|
if (endIdx === -1) {
|
|
1711
1798
|
break;
|
|
1712
1799
|
}
|
|
1800
|
+
const prefix = remaining[1];
|
|
1713
1801
|
const inner = remaining.slice(2, endIdx);
|
|
1714
1802
|
const pipeIdx = inner.indexOf("|");
|
|
1715
|
-
const
|
|
1716
|
-
if (SLACK_USER_ID_PATTERN.test(
|
|
1717
|
-
const name =
|
|
1718
|
-
result += name ? `<@${
|
|
1803
|
+
const id = pipeIdx >= 0 ? inner.slice(0, pipeIdx) : inner;
|
|
1804
|
+
if (prefix === "@" && SLACK_USER_ID_PATTERN.test(id)) {
|
|
1805
|
+
const name = userNameMap.get(id);
|
|
1806
|
+
result += name ? `<@${id}|${name}>` : `<@${id}>`;
|
|
1807
|
+
} else if (prefix === "#" && pipeIdx === -1 && channelNameMap.has(id)) {
|
|
1808
|
+
const name = channelNameMap.get(id);
|
|
1809
|
+
result += `<#${id}|${name}>`;
|
|
1719
1810
|
} else {
|
|
1720
1811
|
result += remaining.slice(0, endIdx + 1);
|
|
1721
1812
|
}
|
|
1722
1813
|
remaining = remaining.slice(endIdx + 1);
|
|
1723
|
-
startIdx = remaining
|
|
1814
|
+
startIdx = findNextMention(remaining);
|
|
1724
1815
|
}
|
|
1725
1816
|
return result + remaining;
|
|
1726
1817
|
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Extract link URLs from a Slack event.
|
|
1820
|
+
* Uses the `blocks` field (rich_text blocks with link elements) when available,
|
|
1821
|
+
* falling back to parsing `<url>` patterns from the text field.
|
|
1822
|
+
*/
|
|
1823
|
+
extractLinks(event) {
|
|
1824
|
+
const urls = /* @__PURE__ */ new Set();
|
|
1825
|
+
if (event.blocks) {
|
|
1826
|
+
for (const block of event.blocks) {
|
|
1827
|
+
if (block.type === "rich_text" && block.elements) {
|
|
1828
|
+
for (const section of block.elements) {
|
|
1829
|
+
if (section.elements) {
|
|
1830
|
+
for (const element of section.elements) {
|
|
1831
|
+
if (element.type === "link" && element.url) {
|
|
1832
|
+
urls.add(element.url);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (urls.size === 0 && event.text) {
|
|
1841
|
+
const urlPattern = /<(https?:\/\/[^>]+)>/g;
|
|
1842
|
+
for (const match of event.text.matchAll(urlPattern)) {
|
|
1843
|
+
const raw = match[1];
|
|
1844
|
+
const pipeIdx = raw.indexOf("|");
|
|
1845
|
+
urls.add(pipeIdx >= 0 ? raw.slice(0, pipeIdx) : raw);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return [...urls].map((url) => this.createLinkPreview(url));
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Create a LinkPreview for a URL. If the URL points to a Slack message,
|
|
1852
|
+
* includes a `fetchMessage` callback that fetches and parses the linked message.
|
|
1853
|
+
*/
|
|
1854
|
+
createLinkPreview(url) {
|
|
1855
|
+
const match = SLACK_MESSAGE_URL_PATTERN.exec(url);
|
|
1856
|
+
if (!match) {
|
|
1857
|
+
return { url };
|
|
1858
|
+
}
|
|
1859
|
+
const channel = match[1];
|
|
1860
|
+
const rawTs = match[2];
|
|
1861
|
+
const ts = `${rawTs.slice(0, rawTs.length - 6)}.${rawTs.slice(rawTs.length - 6)}`;
|
|
1862
|
+
const threadId = this.encodeThreadId({ channel, threadTs: ts });
|
|
1863
|
+
return {
|
|
1864
|
+
url,
|
|
1865
|
+
fetchMessage: async () => {
|
|
1866
|
+
const result = await this.client.conversations.history(
|
|
1867
|
+
this.withToken({
|
|
1868
|
+
channel,
|
|
1869
|
+
latest: ts,
|
|
1870
|
+
inclusive: true,
|
|
1871
|
+
limit: 1
|
|
1872
|
+
})
|
|
1873
|
+
);
|
|
1874
|
+
const messages = result.messages || [];
|
|
1875
|
+
const target = messages.find((msg) => msg.ts === ts);
|
|
1876
|
+
if (!target) {
|
|
1877
|
+
throw new Error(`Message not found: ${url}`);
|
|
1878
|
+
}
|
|
1879
|
+
return this.parseSlackMessage(target, threadId);
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1727
1883
|
async parseSlackMessage(event, threadId, options) {
|
|
1728
1884
|
const isMe = this.isMessageFromSelf(event);
|
|
1729
1885
|
const skipSelfMention = options?.skipSelfMention ?? true;
|
|
@@ -1735,6 +1891,24 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1735
1891
|
userName = userInfo.displayName;
|
|
1736
1892
|
fullName = userInfo.realName;
|
|
1737
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
|
+
}
|
|
1738
1912
|
const text = await this.resolveInlineMentions(rawText, skipSelfMention);
|
|
1739
1913
|
return new Message({
|
|
1740
1914
|
id: event.ts || "",
|
|
@@ -1756,7 +1930,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1756
1930
|
},
|
|
1757
1931
|
attachments: (event.files || []).map(
|
|
1758
1932
|
(file) => this.createAttachment(file)
|
|
1759
|
-
)
|
|
1933
|
+
),
|
|
1934
|
+
links: this.extractLinks(event)
|
|
1760
1935
|
});
|
|
1761
1936
|
}
|
|
1762
1937
|
/**
|
|
@@ -1794,11 +1969,119 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1794
1969
|
`Failed to fetch file: ${response.status} ${response.statusText}`
|
|
1795
1970
|
);
|
|
1796
1971
|
}
|
|
1972
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1973
|
+
if (contentType.includes("text/html")) {
|
|
1974
|
+
throw new NetworkError(
|
|
1975
|
+
"slack",
|
|
1976
|
+
`Failed to download file from Slack: received HTML login page instead of file data. Ensure your Slack app has the "files:read" OAuth scope. URL: ${url}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1797
1979
|
const arrayBuffer = await response.arrayBuffer();
|
|
1798
1980
|
return Buffer.from(arrayBuffer);
|
|
1799
1981
|
} : void 0
|
|
1800
1982
|
};
|
|
1801
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
|
+
}
|
|
1802
2085
|
/**
|
|
1803
2086
|
* Try to render a message using native Slack table blocks.
|
|
1804
2087
|
* Returns blocks + fallback text if the message contains tables, null otherwise.
|
|
@@ -1825,7 +2108,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1825
2108
|
);
|
|
1826
2109
|
return { text: fallbackText, blocks };
|
|
1827
2110
|
}
|
|
1828
|
-
async postMessage(threadId,
|
|
2111
|
+
async postMessage(threadId, _message) {
|
|
2112
|
+
const message = await this.resolveMessageMentions(_message, threadId);
|
|
1829
2113
|
const { channel, threadTs } = this.decodeThreadId(threadId);
|
|
1830
2114
|
try {
|
|
1831
2115
|
const files = extractFiles(message);
|
|
@@ -1929,7 +2213,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1929
2213
|
this.handleSlackError(error);
|
|
1930
2214
|
}
|
|
1931
2215
|
}
|
|
1932
|
-
async postEphemeral(threadId, userId,
|
|
2216
|
+
async postEphemeral(threadId, userId, _message) {
|
|
2217
|
+
const message = await this.resolveMessageMentions(_message, threadId);
|
|
1933
2218
|
const { channel, threadTs } = this.decodeThreadId(threadId);
|
|
1934
2219
|
try {
|
|
1935
2220
|
const card = extractCard(message);
|
|
@@ -2022,7 +2307,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2022
2307
|
this.handleSlackError(error);
|
|
2023
2308
|
}
|
|
2024
2309
|
}
|
|
2025
|
-
async scheduleMessage(threadId,
|
|
2310
|
+
async scheduleMessage(threadId, _message, options) {
|
|
2311
|
+
const message = await this.resolveMessageMentions(_message, threadId);
|
|
2026
2312
|
const { channel, threadTs } = this.decodeThreadId(threadId);
|
|
2027
2313
|
const postAtUnix = Math.floor(options.postAt.getTime() / 1e3);
|
|
2028
2314
|
if (postAtUnix <= Math.floor(Date.now() / 1e3)) {
|
|
@@ -2208,7 +2494,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2208
2494
|
}
|
|
2209
2495
|
return fileIds;
|
|
2210
2496
|
}
|
|
2211
|
-
async editMessage(threadId, messageId,
|
|
2497
|
+
async editMessage(threadId, messageId, _message) {
|
|
2498
|
+
const message = await this.resolveMessageMentions(_message, threadId);
|
|
2212
2499
|
const ephemeral = this.decodeEphemeralMessageId(messageId);
|
|
2213
2500
|
if (ephemeral) {
|
|
2214
2501
|
const { threadTs } = this.decodeThreadId(threadId);
|
|
@@ -2769,7 +3056,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2769
3056
|
},
|
|
2770
3057
|
attachments: (event.files || []).map(
|
|
2771
3058
|
(file) => this.createAttachment(file)
|
|
2772
|
-
)
|
|
3059
|
+
),
|
|
3060
|
+
links: this.extractLinks(event)
|
|
2773
3061
|
});
|
|
2774
3062
|
}
|
|
2775
3063
|
// =========================================================================
|