@chat-adapter/slack 4.19.0 → 4.20.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.
- package/README.md +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +164 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
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,7 @@ 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;
|
|
165
178
|
private readonly clientId;
|
|
166
179
|
private readonly clientSecret;
|
|
167
180
|
private readonly encryptionKey;
|
|
@@ -222,6 +235,11 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
222
235
|
* Returns display name and real name, or falls back to user ID.
|
|
223
236
|
*/
|
|
224
237
|
private lookupUser;
|
|
238
|
+
/**
|
|
239
|
+
* Look up channel name from Slack API with caching via state adapter.
|
|
240
|
+
* Returns channel name, or falls back to channel ID.
|
|
241
|
+
*/
|
|
242
|
+
private lookupChannel;
|
|
225
243
|
handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
|
|
226
244
|
/** Extract and dispatch events from a validated payload */
|
|
227
245
|
private processEventPayload;
|
|
@@ -307,6 +325,17 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
307
325
|
* detection doesn't apply.
|
|
308
326
|
*/
|
|
309
327
|
private resolveInlineMentions;
|
|
328
|
+
/**
|
|
329
|
+
* Extract link URLs from a Slack event.
|
|
330
|
+
* Uses the `blocks` field (rich_text blocks with link elements) when available,
|
|
331
|
+
* falling back to parsing `<url>` patterns from the text field.
|
|
332
|
+
*/
|
|
333
|
+
private extractLinks;
|
|
334
|
+
/**
|
|
335
|
+
* Create a LinkPreview for a URL. If the URL points to a Slack message,
|
|
336
|
+
* includes a `fetchMessage` callback that fetches and parses the linked message.
|
|
337
|
+
*/
|
|
338
|
+
private createLinkPreview;
|
|
310
339
|
private parseSlackMessage;
|
|
311
340
|
/**
|
|
312
341
|
* Create an Attachment object from a Slack file.
|
package/dist/index.js
CHANGED
|
@@ -708,6 +708,18 @@ function radioSelectToBlock(radioSelect) {
|
|
|
708
708
|
|
|
709
709
|
// src/index.ts
|
|
710
710
|
var SLACK_USER_ID_PATTERN = /^[A-Z0-9_]+$/;
|
|
711
|
+
function findNextMention(text) {
|
|
712
|
+
const atIdx = text.indexOf("<@");
|
|
713
|
+
const hashIdx = text.indexOf("<#");
|
|
714
|
+
if (atIdx === -1) {
|
|
715
|
+
return hashIdx;
|
|
716
|
+
}
|
|
717
|
+
if (hashIdx === -1) {
|
|
718
|
+
return atIdx;
|
|
719
|
+
}
|
|
720
|
+
return Math.min(atIdx, hashIdx);
|
|
721
|
+
}
|
|
722
|
+
var SLACK_MESSAGE_URL_PATTERN = /^https?:\/\/[^/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d+)(?:\?.*)?$/;
|
|
711
723
|
var SlackAdapter = class _SlackAdapter {
|
|
712
724
|
name = "slack";
|
|
713
725
|
userName;
|
|
@@ -722,6 +734,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
722
734
|
formatConverter = new SlackFormatConverter();
|
|
723
735
|
static USER_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
724
736
|
// 1 hour
|
|
737
|
+
static CHANNEL_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
738
|
+
// 1 hour
|
|
725
739
|
// Multi-workspace support
|
|
726
740
|
clientId;
|
|
727
741
|
clientSecret;
|
|
@@ -1004,6 +1018,37 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1004
1018
|
return { displayName: userId, realName: userId };
|
|
1005
1019
|
}
|
|
1006
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Look up channel name from Slack API with caching via state adapter.
|
|
1023
|
+
* Returns channel name, or falls back to channel ID.
|
|
1024
|
+
*/
|
|
1025
|
+
async lookupChannel(channelId) {
|
|
1026
|
+
const cacheKey = `slack:channel:${channelId}`;
|
|
1027
|
+
if (this.chat) {
|
|
1028
|
+
const cached = await this.chat.getState().get(cacheKey);
|
|
1029
|
+
if (cached) {
|
|
1030
|
+
return cached.name;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
const result = await this.client.conversations.info(
|
|
1035
|
+
this.withToken({ channel: channelId })
|
|
1036
|
+
);
|
|
1037
|
+
const name = result.channel?.name || channelId;
|
|
1038
|
+
if (this.chat) {
|
|
1039
|
+
await this.chat.getState().set(
|
|
1040
|
+
cacheKey,
|
|
1041
|
+
{ name },
|
|
1042
|
+
_SlackAdapter.CHANNEL_CACHE_TTL_MS
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
this.logger.debug("Fetched channel info", { channelId, name });
|
|
1046
|
+
return name;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
this.logger.warn("Could not fetch channel info", { channelId, error });
|
|
1049
|
+
return channelId;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1007
1052
|
async handleWebhook(request, options) {
|
|
1008
1053
|
const body = await request.text();
|
|
1009
1054
|
this.logger.debug("Slack webhook raw body", { body });
|
|
@@ -1406,7 +1451,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1406
1451
|
channel: event.channel,
|
|
1407
1452
|
threadTs
|
|
1408
1453
|
});
|
|
1409
|
-
const isMention =
|
|
1454
|
+
const isMention = event.type === "app_mention";
|
|
1410
1455
|
const factory = async () => {
|
|
1411
1456
|
const msg = await this.parseSlackMessage(event, threadId);
|
|
1412
1457
|
if (isMention) {
|
|
@@ -1668,41 +1713,56 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1668
1713
|
*/
|
|
1669
1714
|
async resolveInlineMentions(text, skipSelfMention) {
|
|
1670
1715
|
const userIds = /* @__PURE__ */ new Set();
|
|
1716
|
+
const channelIds = /* @__PURE__ */ new Set();
|
|
1671
1717
|
for (const segment of text.split("<")) {
|
|
1672
1718
|
const end = segment.indexOf(">");
|
|
1673
1719
|
if (end === -1) {
|
|
1674
1720
|
continue;
|
|
1675
1721
|
}
|
|
1676
1722
|
const inner = segment.slice(0, end);
|
|
1677
|
-
if (
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1723
|
+
if (inner.startsWith("@")) {
|
|
1724
|
+
const rest = inner.slice(1);
|
|
1725
|
+
const pipeIdx = rest.indexOf("|");
|
|
1726
|
+
const uid = pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest;
|
|
1727
|
+
if (SLACK_USER_ID_PATTERN.test(uid)) {
|
|
1728
|
+
userIds.add(uid);
|
|
1729
|
+
}
|
|
1730
|
+
} else if (inner.startsWith("#")) {
|
|
1731
|
+
const rest = inner.slice(1);
|
|
1732
|
+
const pipeIdx = rest.indexOf("|");
|
|
1733
|
+
if (pipeIdx === -1 && SLACK_USER_ID_PATTERN.test(rest)) {
|
|
1734
|
+
channelIds.add(rest);
|
|
1735
|
+
}
|
|
1685
1736
|
}
|
|
1686
1737
|
}
|
|
1687
|
-
if (userIds.size === 0) {
|
|
1738
|
+
if (userIds.size === 0 && channelIds.size === 0) {
|
|
1688
1739
|
return text;
|
|
1689
1740
|
}
|
|
1690
1741
|
if (skipSelfMention && this._botUserId) {
|
|
1691
1742
|
userIds.delete(this._botUserId);
|
|
1692
1743
|
}
|
|
1693
|
-
if (userIds.size === 0) {
|
|
1744
|
+
if (userIds.size === 0 && channelIds.size === 0) {
|
|
1694
1745
|
return text;
|
|
1695
1746
|
}
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1747
|
+
const [userLookups, channelLookups] = await Promise.all([
|
|
1748
|
+
Promise.all(
|
|
1749
|
+
[...userIds].map(async (uid) => {
|
|
1750
|
+
const info = await this.lookupUser(uid);
|
|
1751
|
+
return [uid, info.displayName];
|
|
1752
|
+
})
|
|
1753
|
+
),
|
|
1754
|
+
Promise.all(
|
|
1755
|
+
[...channelIds].map(async (cid) => {
|
|
1756
|
+
const name = await this.lookupChannel(cid);
|
|
1757
|
+
return [cid, name];
|
|
1758
|
+
})
|
|
1759
|
+
)
|
|
1760
|
+
]);
|
|
1761
|
+
const userNameMap = new Map(userLookups);
|
|
1762
|
+
const channelNameMap = new Map(channelLookups);
|
|
1703
1763
|
let result = "";
|
|
1704
1764
|
let remaining = text;
|
|
1705
|
-
let startIdx = remaining
|
|
1765
|
+
let startIdx = findNextMention(remaining);
|
|
1706
1766
|
while (startIdx !== -1) {
|
|
1707
1767
|
result += remaining.slice(0, startIdx);
|
|
1708
1768
|
remaining = remaining.slice(startIdx);
|
|
@@ -1710,20 +1770,89 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1710
1770
|
if (endIdx === -1) {
|
|
1711
1771
|
break;
|
|
1712
1772
|
}
|
|
1773
|
+
const prefix = remaining[1];
|
|
1713
1774
|
const inner = remaining.slice(2, endIdx);
|
|
1714
1775
|
const pipeIdx = inner.indexOf("|");
|
|
1715
|
-
const
|
|
1716
|
-
if (SLACK_USER_ID_PATTERN.test(
|
|
1717
|
-
const name =
|
|
1718
|
-
result += name ? `<@${
|
|
1776
|
+
const id = pipeIdx >= 0 ? inner.slice(0, pipeIdx) : inner;
|
|
1777
|
+
if (prefix === "@" && SLACK_USER_ID_PATTERN.test(id)) {
|
|
1778
|
+
const name = userNameMap.get(id);
|
|
1779
|
+
result += name ? `<@${id}|${name}>` : `<@${id}>`;
|
|
1780
|
+
} else if (prefix === "#" && pipeIdx === -1 && channelNameMap.has(id)) {
|
|
1781
|
+
const name = channelNameMap.get(id);
|
|
1782
|
+
result += `<#${id}|${name}>`;
|
|
1719
1783
|
} else {
|
|
1720
1784
|
result += remaining.slice(0, endIdx + 1);
|
|
1721
1785
|
}
|
|
1722
1786
|
remaining = remaining.slice(endIdx + 1);
|
|
1723
|
-
startIdx = remaining
|
|
1787
|
+
startIdx = findNextMention(remaining);
|
|
1724
1788
|
}
|
|
1725
1789
|
return result + remaining;
|
|
1726
1790
|
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Extract link URLs from a Slack event.
|
|
1793
|
+
* Uses the `blocks` field (rich_text blocks with link elements) when available,
|
|
1794
|
+
* falling back to parsing `<url>` patterns from the text field.
|
|
1795
|
+
*/
|
|
1796
|
+
extractLinks(event) {
|
|
1797
|
+
const urls = /* @__PURE__ */ new Set();
|
|
1798
|
+
if (event.blocks) {
|
|
1799
|
+
for (const block of event.blocks) {
|
|
1800
|
+
if (block.type === "rich_text" && block.elements) {
|
|
1801
|
+
for (const section of block.elements) {
|
|
1802
|
+
if (section.elements) {
|
|
1803
|
+
for (const element of section.elements) {
|
|
1804
|
+
if (element.type === "link" && element.url) {
|
|
1805
|
+
urls.add(element.url);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (urls.size === 0 && event.text) {
|
|
1814
|
+
const urlPattern = /<(https?:\/\/[^>]+)>/g;
|
|
1815
|
+
for (const match of event.text.matchAll(urlPattern)) {
|
|
1816
|
+
const raw = match[1];
|
|
1817
|
+
const pipeIdx = raw.indexOf("|");
|
|
1818
|
+
urls.add(pipeIdx >= 0 ? raw.slice(0, pipeIdx) : raw);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return [...urls].map((url) => this.createLinkPreview(url));
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Create a LinkPreview for a URL. If the URL points to a Slack message,
|
|
1825
|
+
* includes a `fetchMessage` callback that fetches and parses the linked message.
|
|
1826
|
+
*/
|
|
1827
|
+
createLinkPreview(url) {
|
|
1828
|
+
const match = SLACK_MESSAGE_URL_PATTERN.exec(url);
|
|
1829
|
+
if (!match) {
|
|
1830
|
+
return { url };
|
|
1831
|
+
}
|
|
1832
|
+
const channel = match[1];
|
|
1833
|
+
const rawTs = match[2];
|
|
1834
|
+
const ts = `${rawTs.slice(0, rawTs.length - 6)}.${rawTs.slice(rawTs.length - 6)}`;
|
|
1835
|
+
const threadId = this.encodeThreadId({ channel, threadTs: ts });
|
|
1836
|
+
return {
|
|
1837
|
+
url,
|
|
1838
|
+
fetchMessage: async () => {
|
|
1839
|
+
const result = await this.client.conversations.history(
|
|
1840
|
+
this.withToken({
|
|
1841
|
+
channel,
|
|
1842
|
+
latest: ts,
|
|
1843
|
+
inclusive: true,
|
|
1844
|
+
limit: 1
|
|
1845
|
+
})
|
|
1846
|
+
);
|
|
1847
|
+
const messages = result.messages || [];
|
|
1848
|
+
const target = messages.find((msg) => msg.ts === ts);
|
|
1849
|
+
if (!target) {
|
|
1850
|
+
throw new Error(`Message not found: ${url}`);
|
|
1851
|
+
}
|
|
1852
|
+
return this.parseSlackMessage(target, threadId);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1727
1856
|
async parseSlackMessage(event, threadId, options) {
|
|
1728
1857
|
const isMe = this.isMessageFromSelf(event);
|
|
1729
1858
|
const skipSelfMention = options?.skipSelfMention ?? true;
|
|
@@ -1756,7 +1885,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1756
1885
|
},
|
|
1757
1886
|
attachments: (event.files || []).map(
|
|
1758
1887
|
(file) => this.createAttachment(file)
|
|
1759
|
-
)
|
|
1888
|
+
),
|
|
1889
|
+
links: this.extractLinks(event)
|
|
1760
1890
|
});
|
|
1761
1891
|
}
|
|
1762
1892
|
/**
|
|
@@ -1794,6 +1924,13 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1794
1924
|
`Failed to fetch file: ${response.status} ${response.statusText}`
|
|
1795
1925
|
);
|
|
1796
1926
|
}
|
|
1927
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1928
|
+
if (contentType.includes("text/html")) {
|
|
1929
|
+
throw new NetworkError(
|
|
1930
|
+
"slack",
|
|
1931
|
+
`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}`
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1797
1934
|
const arrayBuffer = await response.arrayBuffer();
|
|
1798
1935
|
return Buffer.from(arrayBuffer);
|
|
1799
1936
|
} : void 0
|
|
@@ -2769,7 +2906,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2769
2906
|
},
|
|
2770
2907
|
attachments: (event.files || []).map(
|
|
2771
2908
|
(file) => this.createAttachment(file)
|
|
2772
|
-
)
|
|
2909
|
+
),
|
|
2910
|
+
links: this.extractLinks(event)
|
|
2773
2911
|
});
|
|
2774
2912
|
}
|
|
2775
2913
|
// =========================================================================
|