@chat-adapter/slack 4.11.0 → 4.13.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.
- package/dist/index.d.ts +83 -1
- package/dist/index.js +495 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -854,6 +854,21 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
854
854
|
}
|
|
855
855
|
const contentType = request.headers.get("content-type") || "";
|
|
856
856
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
857
|
+
const params = new URLSearchParams(body);
|
|
858
|
+
if (params.has("command") && !params.has("payload")) {
|
|
859
|
+
const teamId = params.get("team_id");
|
|
860
|
+
if (!this.defaultBotToken && teamId) {
|
|
861
|
+
const ctx = await this.resolveTokenForTeam(teamId);
|
|
862
|
+
if (ctx) {
|
|
863
|
+
return this.requestContext.run(
|
|
864
|
+
ctx,
|
|
865
|
+
() => this.handleSlashCommand(params, options)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
this.logger.warn("Could not resolve token for slash command");
|
|
869
|
+
}
|
|
870
|
+
return this.handleSlashCommand(params, options);
|
|
871
|
+
}
|
|
857
872
|
if (!this.defaultBotToken) {
|
|
858
873
|
const teamId = this.extractTeamIdFromInteractive(body);
|
|
859
874
|
if (teamId) {
|
|
@@ -909,6 +924,18 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
909
924
|
this.handleMessageEvent(slackEvent, options);
|
|
910
925
|
} else if (event.type === "reaction_added" || event.type === "reaction_removed") {
|
|
911
926
|
this.handleReactionEvent(event, options);
|
|
927
|
+
} else if (event.type === "assistant_thread_started") {
|
|
928
|
+
this.handleAssistantThreadStarted(
|
|
929
|
+
event,
|
|
930
|
+
options
|
|
931
|
+
);
|
|
932
|
+
} else if (event.type === "assistant_thread_context_changed") {
|
|
933
|
+
this.handleAssistantContextChanged(
|
|
934
|
+
event,
|
|
935
|
+
options
|
|
936
|
+
);
|
|
937
|
+
} else if (event.type === "app_home_opened" && event.tab === "home") {
|
|
938
|
+
this.handleAppHomeOpened(event, options);
|
|
912
939
|
}
|
|
913
940
|
}
|
|
914
941
|
}
|
|
@@ -941,6 +968,46 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
941
968
|
return new Response("", { status: 200 });
|
|
942
969
|
}
|
|
943
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Handle Slack slash command payloads.
|
|
973
|
+
* Slash commands are sent as form-urlencoded with command, text, user_id, channel_id, etc.
|
|
974
|
+
*/
|
|
975
|
+
async handleSlashCommand(params, options) {
|
|
976
|
+
if (!this.chat) {
|
|
977
|
+
this.logger.warn("Chat instance not initialized, ignoring slash command");
|
|
978
|
+
return new Response("", { status: 200 });
|
|
979
|
+
}
|
|
980
|
+
const command = params.get("command") || "";
|
|
981
|
+
const text = params.get("text") || "";
|
|
982
|
+
const userId = params.get("user_id") || "";
|
|
983
|
+
const channelId = params.get("channel_id") || "";
|
|
984
|
+
const triggerId = params.get("trigger_id") || void 0;
|
|
985
|
+
this.logger.debug("Processing Slack slash command", {
|
|
986
|
+
command,
|
|
987
|
+
text,
|
|
988
|
+
userId,
|
|
989
|
+
channelId,
|
|
990
|
+
triggerId
|
|
991
|
+
});
|
|
992
|
+
const userInfo = await this.lookupUser(userId);
|
|
993
|
+
const event = {
|
|
994
|
+
command,
|
|
995
|
+
text,
|
|
996
|
+
user: {
|
|
997
|
+
userId,
|
|
998
|
+
userName: userInfo.displayName,
|
|
999
|
+
fullName: userInfo.realName,
|
|
1000
|
+
isBot: false,
|
|
1001
|
+
isMe: false
|
|
1002
|
+
},
|
|
1003
|
+
adapter: this,
|
|
1004
|
+
raw: Object.fromEntries(params),
|
|
1005
|
+
triggerId,
|
|
1006
|
+
channelId: channelId ? `slack:${channelId}` : ""
|
|
1007
|
+
};
|
|
1008
|
+
this.chat.processSlashCommand(event, options);
|
|
1009
|
+
return new Response("", { status: 200 });
|
|
1010
|
+
}
|
|
944
1011
|
/**
|
|
945
1012
|
* Handle block_actions payload (button clicks in Block Kit).
|
|
946
1013
|
*/
|
|
@@ -952,20 +1019,18 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
952
1019
|
const channel = payload.channel?.id || payload.container?.channel_id;
|
|
953
1020
|
const messageTs = payload.message?.ts || payload.container?.message_ts;
|
|
954
1021
|
const threadTs = payload.message?.thread_ts || payload.container?.thread_ts || messageTs;
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
messageTs
|
|
959
|
-
});
|
|
1022
|
+
const isViewAction = payload.container?.type === "view";
|
|
1023
|
+
if (!isViewAction && !channel) {
|
|
1024
|
+
this.logger.warn("Missing channel in block_actions", { channel });
|
|
960
1025
|
return;
|
|
961
1026
|
}
|
|
962
|
-
const threadId = this.encodeThreadId({
|
|
1027
|
+
const threadId = channel && (threadTs || messageTs) ? this.encodeThreadId({
|
|
963
1028
|
channel,
|
|
964
|
-
threadTs: threadTs || messageTs
|
|
965
|
-
});
|
|
1029
|
+
threadTs: threadTs || messageTs || ""
|
|
1030
|
+
}) : "";
|
|
966
1031
|
const isEphemeral = payload.container?.is_ephemeral === true;
|
|
967
1032
|
const responseUrl = payload.response_url;
|
|
968
|
-
const messageId = isEphemeral && responseUrl ? this.encodeEphemeralMessageId(messageTs, responseUrl, payload.user.id) : messageTs;
|
|
1033
|
+
const messageId = isEphemeral && responseUrl && messageTs ? this.encodeEphemeralMessageId(messageTs, responseUrl, payload.user.id) : messageTs || "";
|
|
969
1034
|
for (const action of payload.actions) {
|
|
970
1035
|
const actionValue = action.selected_option?.value ?? action.value;
|
|
971
1036
|
const actionEvent = {
|
|
@@ -1149,17 +1214,20 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1149
1214
|
return;
|
|
1150
1215
|
}
|
|
1151
1216
|
const isDM = event.channel_type === "im";
|
|
1152
|
-
const threadTs = isDM ? "" : event.thread_ts || event.ts;
|
|
1217
|
+
const threadTs = isDM ? event.thread_ts || "" : event.thread_ts || event.ts;
|
|
1153
1218
|
const threadId = this.encodeThreadId({
|
|
1154
1219
|
channel: event.channel,
|
|
1155
1220
|
threadTs
|
|
1156
1221
|
});
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
threadId
|
|
1160
|
-
()
|
|
1161
|
-
|
|
1162
|
-
|
|
1222
|
+
const isMention = isDM || event.type === "app_mention";
|
|
1223
|
+
const factory = async () => {
|
|
1224
|
+
const msg = await this.parseSlackMessage(event, threadId);
|
|
1225
|
+
if (isMention) {
|
|
1226
|
+
msg.isMention = true;
|
|
1227
|
+
}
|
|
1228
|
+
return msg;
|
|
1229
|
+
};
|
|
1230
|
+
this.chat.processMessage(this, threadId, factory, options);
|
|
1163
1231
|
}
|
|
1164
1232
|
/**
|
|
1165
1233
|
* Handle reaction events from Slack (reaction_added, reaction_removed).
|
|
@@ -1203,9 +1271,196 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1203
1271
|
};
|
|
1204
1272
|
this.chat.processReaction({ ...reactionEvent, adapter: this }, options);
|
|
1205
1273
|
}
|
|
1206
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* Handle assistant_thread_started events from Slack's Assistants API.
|
|
1276
|
+
* Fires when a user opens a new assistant thread (DM with the bot).
|
|
1277
|
+
*/
|
|
1278
|
+
handleAssistantThreadStarted(event, options) {
|
|
1279
|
+
if (!this.chat) {
|
|
1280
|
+
this.logger.warn(
|
|
1281
|
+
"Chat instance not initialized, ignoring assistant_thread_started"
|
|
1282
|
+
);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (!event.assistant_thread) {
|
|
1286
|
+
this.logger.warn(
|
|
1287
|
+
"Malformed assistant_thread_started: missing assistant_thread"
|
|
1288
|
+
);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const { channel_id, thread_ts, user_id, context } = event.assistant_thread;
|
|
1292
|
+
const threadId = this.encodeThreadId({
|
|
1293
|
+
channel: channel_id,
|
|
1294
|
+
threadTs: thread_ts
|
|
1295
|
+
});
|
|
1296
|
+
this.chat.processAssistantThreadStarted(
|
|
1297
|
+
{
|
|
1298
|
+
threadId,
|
|
1299
|
+
userId: user_id,
|
|
1300
|
+
channelId: channel_id,
|
|
1301
|
+
threadTs: thread_ts,
|
|
1302
|
+
context: {
|
|
1303
|
+
channelId: context.channel_id,
|
|
1304
|
+
teamId: context.team_id,
|
|
1305
|
+
enterpriseId: context.enterprise_id,
|
|
1306
|
+
threadEntryPoint: context.thread_entry_point,
|
|
1307
|
+
forceSearch: context.force_search
|
|
1308
|
+
},
|
|
1309
|
+
adapter: this
|
|
1310
|
+
},
|
|
1311
|
+
options
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Handle assistant_thread_context_changed events from Slack's Assistants API.
|
|
1316
|
+
* Fires when a user navigates to a different channel with the assistant panel open.
|
|
1317
|
+
*/
|
|
1318
|
+
handleAssistantContextChanged(event, options) {
|
|
1319
|
+
if (!this.chat) {
|
|
1320
|
+
this.logger.warn(
|
|
1321
|
+
"Chat instance not initialized, ignoring assistant_thread_context_changed"
|
|
1322
|
+
);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (!event.assistant_thread) {
|
|
1326
|
+
this.logger.warn(
|
|
1327
|
+
"Malformed assistant_thread_context_changed: missing assistant_thread"
|
|
1328
|
+
);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const { channel_id, thread_ts, user_id, context } = event.assistant_thread;
|
|
1332
|
+
const threadId = this.encodeThreadId({
|
|
1333
|
+
channel: channel_id,
|
|
1334
|
+
threadTs: thread_ts
|
|
1335
|
+
});
|
|
1336
|
+
this.chat.processAssistantContextChanged(
|
|
1337
|
+
{
|
|
1338
|
+
threadId,
|
|
1339
|
+
userId: user_id,
|
|
1340
|
+
channelId: channel_id,
|
|
1341
|
+
threadTs: thread_ts,
|
|
1342
|
+
context: {
|
|
1343
|
+
channelId: context.channel_id,
|
|
1344
|
+
teamId: context.team_id,
|
|
1345
|
+
enterpriseId: context.enterprise_id,
|
|
1346
|
+
threadEntryPoint: context.thread_entry_point,
|
|
1347
|
+
forceSearch: context.force_search
|
|
1348
|
+
},
|
|
1349
|
+
adapter: this
|
|
1350
|
+
},
|
|
1351
|
+
options
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Handle app_home_opened events from Slack.
|
|
1356
|
+
* Fires when a user opens the bot's Home tab.
|
|
1357
|
+
*/
|
|
1358
|
+
handleAppHomeOpened(event, options) {
|
|
1359
|
+
if (!this.chat) {
|
|
1360
|
+
this.logger.warn(
|
|
1361
|
+
"Chat instance not initialized, ignoring app_home_opened"
|
|
1362
|
+
);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
this.chat.processAppHomeOpened(
|
|
1366
|
+
{
|
|
1367
|
+
userId: event.user,
|
|
1368
|
+
channelId: event.channel,
|
|
1369
|
+
adapter: this
|
|
1370
|
+
},
|
|
1371
|
+
options
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Publish a Home tab view for a user.
|
|
1376
|
+
* Slack API: views.publish
|
|
1377
|
+
*/
|
|
1378
|
+
async publishHomeView(userId, view) {
|
|
1379
|
+
await this.client.views.publish(
|
|
1380
|
+
// biome-ignore lint/suspicious/noExplicitAny: view blocks are consumer-defined
|
|
1381
|
+
this.withToken({ user_id: userId, view })
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Set suggested prompts for an assistant thread.
|
|
1386
|
+
* Slack Assistants API: assistant.threads.setSuggestedPrompts
|
|
1387
|
+
*/
|
|
1388
|
+
async setSuggestedPrompts(channelId, threadTs, prompts, title) {
|
|
1389
|
+
await this.client.assistant.threads.setSuggestedPrompts(
|
|
1390
|
+
this.withToken({
|
|
1391
|
+
channel_id: channelId,
|
|
1392
|
+
thread_ts: threadTs,
|
|
1393
|
+
prompts,
|
|
1394
|
+
title
|
|
1395
|
+
})
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Set status/thinking indicator for an assistant thread.
|
|
1400
|
+
* Slack Assistants API: assistant.threads.setStatus
|
|
1401
|
+
*/
|
|
1402
|
+
async setAssistantStatus(channelId, threadTs, status, loadingMessages) {
|
|
1403
|
+
await this.client.assistant.threads.setStatus(
|
|
1404
|
+
this.withToken({
|
|
1405
|
+
channel_id: channelId,
|
|
1406
|
+
thread_ts: threadTs,
|
|
1407
|
+
status,
|
|
1408
|
+
...loadingMessages && { loading_messages: loadingMessages }
|
|
1409
|
+
})
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Set title for an assistant thread (shown in History tab).
|
|
1414
|
+
* Slack Assistants API: assistant.threads.setTitle
|
|
1415
|
+
*/
|
|
1416
|
+
async setAssistantTitle(channelId, threadTs, title) {
|
|
1417
|
+
await this.client.assistant.threads.setTitle(
|
|
1418
|
+
this.withToken({
|
|
1419
|
+
channel_id: channelId,
|
|
1420
|
+
thread_ts: threadTs,
|
|
1421
|
+
title
|
|
1422
|
+
})
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Resolve inline user mentions in Slack mrkdwn text.
|
|
1427
|
+
* Converts <@U123> to <@U123|displayName> so that toAst/extractPlainText
|
|
1428
|
+
* renders them as @displayName instead of @U123.
|
|
1429
|
+
*
|
|
1430
|
+
* @param skipSelfMention - When true, skips the bot's own user ID so that
|
|
1431
|
+
* mention detection (which looks for @botUserId in the text) continues to
|
|
1432
|
+
* work. Set to false when parsing historical/channel messages where mention
|
|
1433
|
+
* detection doesn't apply.
|
|
1434
|
+
*/
|
|
1435
|
+
async resolveInlineMentions(text, skipSelfMention) {
|
|
1436
|
+
const mentionPattern = /<@([A-Z0-9]+)(?:\|[^>]*)?>/g;
|
|
1437
|
+
const userIds = /* @__PURE__ */ new Set();
|
|
1438
|
+
let match = mentionPattern.exec(text);
|
|
1439
|
+
while (match) {
|
|
1440
|
+
userIds.add(match[1]);
|
|
1441
|
+
match = mentionPattern.exec(text);
|
|
1442
|
+
}
|
|
1443
|
+
if (userIds.size === 0) return text;
|
|
1444
|
+
if (skipSelfMention && this._botUserId) {
|
|
1445
|
+
userIds.delete(this._botUserId);
|
|
1446
|
+
}
|
|
1447
|
+
if (userIds.size === 0) return text;
|
|
1448
|
+
const lookups = await Promise.all(
|
|
1449
|
+
[...userIds].map(async (uid) => {
|
|
1450
|
+
const info = await this.lookupUser(uid);
|
|
1451
|
+
return [uid, info.displayName];
|
|
1452
|
+
})
|
|
1453
|
+
);
|
|
1454
|
+
const nameMap = new Map(lookups);
|
|
1455
|
+
return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_m, uid) => {
|
|
1456
|
+
const name = nameMap.get(uid);
|
|
1457
|
+
return name ? `<@${uid}|${name}>` : `<@${uid}>`;
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async parseSlackMessage(event, threadId, options) {
|
|
1207
1461
|
const isMe = this.isMessageFromSelf(event);
|
|
1208
|
-
const
|
|
1462
|
+
const skipSelfMention = options?.skipSelfMention ?? true;
|
|
1463
|
+
const rawText = event.text || "";
|
|
1209
1464
|
let userName = event.username || "unknown";
|
|
1210
1465
|
let fullName = event.username || "unknown";
|
|
1211
1466
|
if (event.user && !event.username) {
|
|
@@ -1213,6 +1468,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1213
1468
|
userName = userInfo.displayName;
|
|
1214
1469
|
fullName = userInfo.realName;
|
|
1215
1470
|
}
|
|
1471
|
+
const text = await this.resolveInlineMentions(rawText, skipSelfMention);
|
|
1216
1472
|
return new Message({
|
|
1217
1473
|
id: event.ts || "",
|
|
1218
1474
|
threadId,
|
|
@@ -1686,7 +1942,10 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1686
1942
|
await streamer.append({ markdown_text: chunk });
|
|
1687
1943
|
}
|
|
1688
1944
|
}
|
|
1689
|
-
const result = await streamer.stop(
|
|
1945
|
+
const result = await streamer.stop(
|
|
1946
|
+
// biome-ignore lint/suspicious/noExplicitAny: stopBlocks are platform-specific Block Kit elements
|
|
1947
|
+
options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
|
|
1948
|
+
);
|
|
1690
1949
|
const messageTs = result.message?.ts ?? result.ts;
|
|
1691
1950
|
this.logger.debug("Slack: stream complete", { messageId: messageTs });
|
|
1692
1951
|
return {
|
|
@@ -1898,7 +2157,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1898
2157
|
}
|
|
1899
2158
|
decodeThreadId(threadId) {
|
|
1900
2159
|
const parts = threadId.split(":");
|
|
1901
|
-
if (parts.length
|
|
2160
|
+
if (parts.length < 2 || parts.length > 3 || parts[0] !== "slack") {
|
|
1902
2161
|
throw new ValidationError(
|
|
1903
2162
|
"slack",
|
|
1904
2163
|
`Invalid Slack thread ID: ${threadId}`
|
|
@@ -1906,7 +2165,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1906
2165
|
}
|
|
1907
2166
|
return {
|
|
1908
2167
|
channel: parts[1],
|
|
1909
|
-
threadTs: parts[2]
|
|
2168
|
+
threadTs: parts.length === 3 ? parts[2] : ""
|
|
1910
2169
|
};
|
|
1911
2170
|
}
|
|
1912
2171
|
parseMessage(raw) {
|
|
@@ -1950,6 +2209,221 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1950
2209
|
)
|
|
1951
2210
|
});
|
|
1952
2211
|
}
|
|
2212
|
+
// =========================================================================
|
|
2213
|
+
// Channel-level methods
|
|
2214
|
+
// =========================================================================
|
|
2215
|
+
/**
|
|
2216
|
+
* Derive channel ID from a Slack thread ID.
|
|
2217
|
+
* Slack thread IDs are "slack:CHANNEL:THREAD_TS", channel ID is "slack:CHANNEL".
|
|
2218
|
+
*/
|
|
2219
|
+
channelIdFromThreadId(threadId) {
|
|
2220
|
+
const { channel } = this.decodeThreadId(threadId);
|
|
2221
|
+
return `slack:${channel}`;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Fetch channel-level messages (conversations.history, not thread replies).
|
|
2225
|
+
*/
|
|
2226
|
+
async fetchChannelMessages(channelId, options = {}) {
|
|
2227
|
+
const channel = channelId.split(":")[1];
|
|
2228
|
+
if (!channel) {
|
|
2229
|
+
throw new ValidationError(
|
|
2230
|
+
"slack",
|
|
2231
|
+
`Invalid Slack channel ID: ${channelId}`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
const direction = options.direction ?? "backward";
|
|
2235
|
+
const limit = options.limit || 100;
|
|
2236
|
+
try {
|
|
2237
|
+
if (direction === "forward") {
|
|
2238
|
+
return await this.fetchChannelMessagesForward(
|
|
2239
|
+
channel,
|
|
2240
|
+
limit,
|
|
2241
|
+
options.cursor
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
return await this.fetchChannelMessagesBackward(
|
|
2245
|
+
channel,
|
|
2246
|
+
limit,
|
|
2247
|
+
options.cursor
|
|
2248
|
+
);
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
this.handleSlackError(error);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
async fetchChannelMessagesForward(channel, limit, cursor) {
|
|
2254
|
+
this.logger.debug("Slack API: conversations.history (forward)", {
|
|
2255
|
+
channel,
|
|
2256
|
+
limit,
|
|
2257
|
+
cursor
|
|
2258
|
+
});
|
|
2259
|
+
const result = await this.client.conversations.history(
|
|
2260
|
+
this.withToken({
|
|
2261
|
+
channel,
|
|
2262
|
+
limit,
|
|
2263
|
+
oldest: cursor,
|
|
2264
|
+
inclusive: cursor ? false : void 0
|
|
2265
|
+
})
|
|
2266
|
+
);
|
|
2267
|
+
const slackMessages = (result.messages || []).reverse();
|
|
2268
|
+
const messages = await Promise.all(
|
|
2269
|
+
slackMessages.map((msg) => {
|
|
2270
|
+
const threadTs = msg.thread_ts || msg.ts || "";
|
|
2271
|
+
const threadId = `slack:${channel}:${threadTs}`;
|
|
2272
|
+
return this.parseSlackMessage(msg, threadId, {
|
|
2273
|
+
skipSelfMention: false
|
|
2274
|
+
});
|
|
2275
|
+
})
|
|
2276
|
+
);
|
|
2277
|
+
let nextCursor;
|
|
2278
|
+
if (result.has_more && slackMessages.length > 0) {
|
|
2279
|
+
const newest = slackMessages[slackMessages.length - 1];
|
|
2280
|
+
if (newest?.ts) {
|
|
2281
|
+
nextCursor = newest.ts;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return {
|
|
2285
|
+
messages,
|
|
2286
|
+
nextCursor
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
async fetchChannelMessagesBackward(channel, limit, cursor) {
|
|
2290
|
+
this.logger.debug("Slack API: conversations.history (backward)", {
|
|
2291
|
+
channel,
|
|
2292
|
+
limit,
|
|
2293
|
+
cursor
|
|
2294
|
+
});
|
|
2295
|
+
const result = await this.client.conversations.history(
|
|
2296
|
+
this.withToken({
|
|
2297
|
+
channel,
|
|
2298
|
+
limit,
|
|
2299
|
+
latest: cursor,
|
|
2300
|
+
inclusive: cursor ? false : void 0
|
|
2301
|
+
})
|
|
2302
|
+
);
|
|
2303
|
+
const slackMessages = result.messages || [];
|
|
2304
|
+
const chronological = [...slackMessages].reverse();
|
|
2305
|
+
const messages = await Promise.all(
|
|
2306
|
+
chronological.map((msg) => {
|
|
2307
|
+
const threadTs = msg.thread_ts || msg.ts || "";
|
|
2308
|
+
const threadId = `slack:${channel}:${threadTs}`;
|
|
2309
|
+
return this.parseSlackMessage(msg, threadId, {
|
|
2310
|
+
skipSelfMention: false
|
|
2311
|
+
});
|
|
2312
|
+
})
|
|
2313
|
+
);
|
|
2314
|
+
let nextCursor;
|
|
2315
|
+
if (result.has_more && chronological.length > 0) {
|
|
2316
|
+
const oldest = chronological[0];
|
|
2317
|
+
if (oldest?.ts) {
|
|
2318
|
+
nextCursor = oldest.ts;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return {
|
|
2322
|
+
messages,
|
|
2323
|
+
nextCursor
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* List threads in a Slack channel.
|
|
2328
|
+
* Fetches channel history and filters for messages with replies.
|
|
2329
|
+
*/
|
|
2330
|
+
async listThreads(channelId, options = {}) {
|
|
2331
|
+
const channel = channelId.split(":")[1];
|
|
2332
|
+
if (!channel) {
|
|
2333
|
+
throw new ValidationError(
|
|
2334
|
+
"slack",
|
|
2335
|
+
`Invalid Slack channel ID: ${channelId}`
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
const limit = options.limit || 50;
|
|
2339
|
+
try {
|
|
2340
|
+
this.logger.debug("Slack API: conversations.history (listThreads)", {
|
|
2341
|
+
channel,
|
|
2342
|
+
limit,
|
|
2343
|
+
cursor: options.cursor
|
|
2344
|
+
});
|
|
2345
|
+
const result = await this.client.conversations.history(
|
|
2346
|
+
this.withToken({
|
|
2347
|
+
channel,
|
|
2348
|
+
limit: Math.min(limit * 3, 200),
|
|
2349
|
+
// Fetch extra since not all have threads
|
|
2350
|
+
cursor: options.cursor
|
|
2351
|
+
})
|
|
2352
|
+
);
|
|
2353
|
+
const slackMessages = result.messages || [];
|
|
2354
|
+
const threadMessages = slackMessages.filter(
|
|
2355
|
+
(msg) => (msg.reply_count ?? 0) > 0
|
|
2356
|
+
);
|
|
2357
|
+
const selected = threadMessages.slice(0, limit);
|
|
2358
|
+
const threads = await Promise.all(
|
|
2359
|
+
selected.map(async (msg) => {
|
|
2360
|
+
const threadTs = msg.ts || "";
|
|
2361
|
+
const threadId = `slack:${channel}:${threadTs}`;
|
|
2362
|
+
const rootMessage = await this.parseSlackMessage(msg, threadId, {
|
|
2363
|
+
skipSelfMention: false
|
|
2364
|
+
});
|
|
2365
|
+
return {
|
|
2366
|
+
id: threadId,
|
|
2367
|
+
rootMessage,
|
|
2368
|
+
replyCount: msg.reply_count,
|
|
2369
|
+
lastReplyAt: msg.latest_reply ? new Date(Number.parseFloat(msg.latest_reply) * 1e3) : void 0
|
|
2370
|
+
};
|
|
2371
|
+
})
|
|
2372
|
+
);
|
|
2373
|
+
const nextCursor = result.response_metadata?.next_cursor;
|
|
2374
|
+
return {
|
|
2375
|
+
threads,
|
|
2376
|
+
nextCursor: nextCursor || void 0
|
|
2377
|
+
};
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
this.handleSlackError(error);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Fetch Slack channel info/metadata.
|
|
2384
|
+
*/
|
|
2385
|
+
async fetchChannelInfo(channelId) {
|
|
2386
|
+
const channel = channelId.split(":")[1];
|
|
2387
|
+
if (!channel) {
|
|
2388
|
+
throw new ValidationError(
|
|
2389
|
+
"slack",
|
|
2390
|
+
`Invalid Slack channel ID: ${channelId}`
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
try {
|
|
2394
|
+
this.logger.debug("Slack API: conversations.info (channel)", { channel });
|
|
2395
|
+
const result = await this.client.conversations.info(
|
|
2396
|
+
this.withToken({ channel })
|
|
2397
|
+
);
|
|
2398
|
+
const info = result.channel;
|
|
2399
|
+
return {
|
|
2400
|
+
id: channelId,
|
|
2401
|
+
name: info?.name ? `#${info.name}` : void 0,
|
|
2402
|
+
isDM: info?.is_im || info?.is_mpim || false,
|
|
2403
|
+
memberCount: info?.num_members,
|
|
2404
|
+
metadata: {
|
|
2405
|
+
purpose: info?.purpose?.value,
|
|
2406
|
+
topic: info?.topic?.value
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
this.handleSlackError(error);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Post a top-level message to a channel (not in a thread).
|
|
2415
|
+
*/
|
|
2416
|
+
async postChannelMessage(channelId, message) {
|
|
2417
|
+
const channel = channelId.split(":")[1];
|
|
2418
|
+
if (!channel) {
|
|
2419
|
+
throw new ValidationError(
|
|
2420
|
+
"slack",
|
|
2421
|
+
`Invalid Slack channel ID: ${channelId}`
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
const syntheticThreadId = `slack:${channel}:`;
|
|
2425
|
+
return this.postMessage(syntheticThreadId, message);
|
|
2426
|
+
}
|
|
1953
2427
|
renderFormatted(content) {
|
|
1954
2428
|
return this.formatConverter.fromAst(content);
|
|
1955
2429
|
}
|