@chat-adapter/slack 4.17.0 → 4.18.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 CHANGED
@@ -59,6 +59,12 @@ declare class SlackFormatConverter extends BaseFormatConverter {
59
59
  * Parse Slack mrkdwn into an AST.
60
60
  */
61
61
  toAst(mrkdwn: string): Root;
62
+ /**
63
+ * Convert AST to Slack blocks, using a native table block for the first table.
64
+ * Returns null if the AST contains no tables (caller should use regular text).
65
+ * Slack allows at most one table block per message; additional tables use ASCII.
66
+ */
67
+ toBlocksWithTable(ast: Root): SlackBlock[] | null;
62
68
  private nodeToMrkdwn;
63
69
  }
64
70
 
@@ -307,6 +313,11 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
307
313
  * Includes a fetchData method that uses the bot token for auth.
308
314
  */
309
315
  private createAttachment;
316
+ /**
317
+ * Try to render a message using native Slack table blocks.
318
+ * Returns blocks + fallback text if the message contains tables, null otherwise.
319
+ */
320
+ private renderWithTableBlocks;
310
321
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
311
322
  postEphemeral(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
312
323
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  defaultEmojiResolver,
18
18
  isJSX,
19
19
  Message,
20
+ parseMarkdown as parseMarkdown2,
20
21
  StreamingMarkdownRenderer,
21
22
  toModalElement
22
23
  } from "chat";
@@ -59,13 +60,14 @@ function cardToBlockKit(card) {
59
60
  alt_text: card.title || "Card image"
60
61
  });
61
62
  }
63
+ const state = { usedNativeTable: false };
62
64
  for (const child of card.children) {
63
- const childBlocks = convertChildToBlocks(child);
65
+ const childBlocks = convertChildToBlocks(child, state);
64
66
  blocks.push(...childBlocks);
65
67
  }
66
68
  return blocks;
67
69
  }
68
- function convertChildToBlocks(child) {
70
+ function convertChildToBlocks(child, state) {
69
71
  switch (child.type) {
70
72
  case "text":
71
73
  return [convertTextToBlock(child)];
@@ -76,13 +78,13 @@ function convertChildToBlocks(child) {
76
78
  case "actions":
77
79
  return [convertActionsToBlock(child)];
78
80
  case "section":
79
- return convertSectionToBlocks(child);
81
+ return convertSectionToBlocks(child, state);
80
82
  case "fields":
81
83
  return [convertFieldsToBlock(child)];
82
84
  case "link":
83
85
  return [convertLinkToBlock(child)];
84
86
  case "table":
85
- return convertTableToBlocks(child);
87
+ return convertTableToBlocks(child, state);
86
88
  default: {
87
89
  const text = cardChildToFallbackText(child);
88
90
  if (text) {
@@ -250,10 +252,10 @@ function convertRadioSelectToElement(radioSelect) {
250
252
  }
251
253
  return element;
252
254
  }
253
- function convertTableToBlocks(element) {
255
+ function convertTableToBlocks(element, state) {
254
256
  const MAX_ROWS = 100;
255
257
  const MAX_COLS = 20;
256
- if (element.rows.length > MAX_ROWS || element.headers.length > MAX_COLS) {
258
+ if (state.usedNativeTable || element.rows.length > MAX_ROWS || element.headers.length > MAX_COLS) {
257
259
  return [
258
260
  {
259
261
  type: "section",
@@ -266,31 +268,28 @@ ${tableElementToAscii(element.headers, element.rows)}
266
268
  }
267
269
  ];
268
270
  }
269
- const columns = element.headers.map((header, i) => ({
270
- id: `col_${i}`,
271
- header: {
272
- type: "plain_text",
273
- text: convertEmoji(header)
274
- }
271
+ state.usedNativeTable = true;
272
+ const headerRow = element.headers.map((header) => ({
273
+ type: "raw_text",
274
+ text: convertEmoji(header)
275
275
  }));
276
- const rows = element.rows.map((row) => ({
277
- cells: row.map((cell) => ({
278
- type: "plain_text",
276
+ const dataRows = element.rows.map(
277
+ (row) => row.map((cell) => ({
278
+ type: "raw_text",
279
279
  text: convertEmoji(cell)
280
280
  }))
281
- }));
281
+ );
282
282
  return [
283
283
  {
284
284
  type: "table",
285
- columns,
286
- rows
285
+ rows: [headerRow, ...dataRows]
287
286
  }
288
287
  ];
289
288
  }
290
- function convertSectionToBlocks(element) {
289
+ function convertSectionToBlocks(element, state) {
291
290
  const blocks = [];
292
291
  for (const child of element.children) {
293
- blocks.push(...convertChildToBlocks(child));
292
+ blocks.push(...convertChildToBlocks(child, state));
294
293
  }
295
294
  return blocks;
296
295
  }
@@ -428,16 +427,68 @@ var SlackFormatConverter = class extends BaseFormatConverter {
428
427
  */
429
428
  toAst(mrkdwn) {
430
429
  let markdown = mrkdwn;
431
- markdown = markdown.replace(/<@([^|>]+)\|([^>]+)>/g, "@$2");
432
- markdown = markdown.replace(/<@([^>]+)>/g, "@$1");
433
- markdown = markdown.replace(/<#[^|>]+\|([^>]+)>/g, "#$1");
434
- markdown = markdown.replace(/<#([^>]+)>/g, "#$1");
430
+ markdown = markdown.replace(/<@([A-Z0-9_]+)\|([^>]+)>/g, "@$2");
431
+ markdown = markdown.replace(/<@([A-Z0-9_]+)>/g, "@$1");
432
+ markdown = markdown.replace(/<#[A-Z0-9_]+\|([^>]+)>/g, "#$1");
433
+ markdown = markdown.replace(/<#([A-Z0-9_]+)>/g, "#$1");
435
434
  markdown = markdown.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, "[$2]($1)");
436
435
  markdown = markdown.replace(/<(https?:\/\/[^>]+)>/g, "$1");
437
436
  markdown = markdown.replace(/(?<![_*\\])\*([^*\n]+)\*(?![_*])/g, "**$1**");
438
437
  markdown = markdown.replace(/(?<!~)~([^~\n]+)~(?!~)/g, "~~$1~~");
439
438
  return parseMarkdown(markdown);
440
439
  }
440
+ /**
441
+ * Convert AST to Slack blocks, using a native table block for the first table.
442
+ * Returns null if the AST contains no tables (caller should use regular text).
443
+ * Slack allows at most one table block per message; additional tables use ASCII.
444
+ */
445
+ toBlocksWithTable(ast) {
446
+ const hasTable = ast.children.some((node) => isTableNode(node));
447
+ if (!hasTable) {
448
+ return null;
449
+ }
450
+ const blocks = [];
451
+ let usedNativeTable = false;
452
+ let textBuffer = [];
453
+ const flushText = () => {
454
+ if (textBuffer.length > 0) {
455
+ const text = textBuffer.join("\n\n");
456
+ if (text.trim()) {
457
+ blocks.push({
458
+ type: "section",
459
+ text: { type: "mrkdwn", text }
460
+ });
461
+ }
462
+ textBuffer = [];
463
+ }
464
+ };
465
+ for (const child of ast.children) {
466
+ const node = child;
467
+ if (isTableNode(node)) {
468
+ flushText();
469
+ if (usedNativeTable) {
470
+ blocks.push({
471
+ type: "section",
472
+ text: {
473
+ type: "mrkdwn",
474
+ text: `\`\`\`
475
+ ${tableToAscii(node)}
476
+ \`\`\``
477
+ }
478
+ });
479
+ } else {
480
+ blocks.push(
481
+ mdastTableToSlackBlock(node, this.nodeToMrkdwn.bind(this))
482
+ );
483
+ usedNativeTable = true;
484
+ }
485
+ } else {
486
+ textBuffer.push(this.nodeToMrkdwn(node));
487
+ }
488
+ }
489
+ flushText();
490
+ return blocks;
491
+ }
441
492
  nodeToMrkdwn(node) {
442
493
  if (isParagraphNode(node)) {
443
494
  return getNodeChildren(node).map((child) => this.nodeToMrkdwn(child)).join("");
@@ -489,6 +540,26 @@ ${tableToAscii(node)}
489
540
  return this.defaultNodeToText(node, (child) => this.nodeToMrkdwn(child));
490
541
  }
491
542
  };
543
+ function mdastTableToSlackBlock(node, cellConverter) {
544
+ const rows = [];
545
+ for (const row of node.children) {
546
+ const cells = getNodeChildren(row).map((cell) => ({
547
+ type: "raw_text",
548
+ text: getNodeChildren(cell).map(cellConverter).join("")
549
+ }));
550
+ rows.push(cells);
551
+ }
552
+ const block = { type: "table", rows };
553
+ if (node.align) {
554
+ const columnSettings = node.align.map(
555
+ (a) => ({
556
+ align: a || "left"
557
+ })
558
+ );
559
+ block.column_settings = columnSettings;
560
+ }
561
+ return block;
562
+ }
492
563
 
493
564
  // src/modals.ts
494
565
  function encodeModalMetadata(meta) {
@@ -636,6 +707,7 @@ function radioSelectToBlock(radioSelect) {
636
707
  }
637
708
 
638
709
  // src/index.ts
710
+ var SLACK_USER_ID_PATTERN = /^[A-Z0-9_]+$/;
639
711
  var SlackAdapter = class _SlackAdapter {
640
712
  name = "slack";
641
713
  userName;
@@ -1569,12 +1641,22 @@ var SlackAdapter = class _SlackAdapter {
1569
1641
  * detection doesn't apply.
1570
1642
  */
1571
1643
  async resolveInlineMentions(text, skipSelfMention) {
1572
- const mentionPattern = /<@([A-Z0-9]+)(?:\|[^>]*)?>/g;
1573
1644
  const userIds = /* @__PURE__ */ new Set();
1574
- let match = mentionPattern.exec(text);
1575
- while (match) {
1576
- userIds.add(match[1]);
1577
- match = mentionPattern.exec(text);
1645
+ for (const segment of text.split("<")) {
1646
+ const end = segment.indexOf(">");
1647
+ if (end === -1) {
1648
+ continue;
1649
+ }
1650
+ const inner = segment.slice(0, end);
1651
+ if (!inner.startsWith("@")) {
1652
+ continue;
1653
+ }
1654
+ const rest = inner.slice(1);
1655
+ const pipeIdx = rest.indexOf("|");
1656
+ const uid = pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest;
1657
+ if (SLACK_USER_ID_PATTERN.test(uid)) {
1658
+ userIds.add(uid);
1659
+ }
1578
1660
  }
1579
1661
  if (userIds.size === 0) {
1580
1662
  return text;
@@ -1592,10 +1674,29 @@ var SlackAdapter = class _SlackAdapter {
1592
1674
  })
1593
1675
  );
1594
1676
  const nameMap = new Map(lookups);
1595
- return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_m, uid) => {
1596
- const name = nameMap.get(uid);
1597
- return name ? `<@${uid}|${name}>` : `<@${uid}>`;
1598
- });
1677
+ let result = "";
1678
+ let remaining = text;
1679
+ let startIdx = remaining.indexOf("<@");
1680
+ while (startIdx !== -1) {
1681
+ result += remaining.slice(0, startIdx);
1682
+ remaining = remaining.slice(startIdx);
1683
+ const endIdx = remaining.indexOf(">");
1684
+ if (endIdx === -1) {
1685
+ break;
1686
+ }
1687
+ const inner = remaining.slice(2, endIdx);
1688
+ const pipeIdx = inner.indexOf("|");
1689
+ const uid = pipeIdx >= 0 ? inner.slice(0, pipeIdx) : inner;
1690
+ if (SLACK_USER_ID_PATTERN.test(uid)) {
1691
+ const name = nameMap.get(uid);
1692
+ result += name ? `<@${uid}|${name}>` : `<@${uid}>`;
1693
+ } else {
1694
+ result += remaining.slice(0, endIdx + 1);
1695
+ }
1696
+ remaining = remaining.slice(endIdx + 1);
1697
+ startIdx = remaining.indexOf("<@");
1698
+ }
1699
+ return result + remaining;
1599
1700
  }
1600
1701
  async parseSlackMessage(event, threadId, options) {
1601
1702
  const isMe = this.isMessageFromSelf(event);
@@ -1672,6 +1773,32 @@ var SlackAdapter = class _SlackAdapter {
1672
1773
  } : void 0
1673
1774
  };
1674
1775
  }
1776
+ /**
1777
+ * Try to render a message using native Slack table blocks.
1778
+ * Returns blocks + fallback text if the message contains tables, null otherwise.
1779
+ */
1780
+ renderWithTableBlocks(message) {
1781
+ let ast = null;
1782
+ if (typeof message === "object" && message !== null) {
1783
+ if ("ast" in message) {
1784
+ ast = message.ast;
1785
+ } else if ("markdown" in message) {
1786
+ ast = parseMarkdown2(message.markdown);
1787
+ }
1788
+ }
1789
+ if (!ast) {
1790
+ return null;
1791
+ }
1792
+ const blocks = this.formatConverter.toBlocksWithTable(ast);
1793
+ if (!blocks) {
1794
+ return null;
1795
+ }
1796
+ const fallbackText = convertEmojiPlaceholders(
1797
+ this.formatConverter.renderPostable(message),
1798
+ "slack"
1799
+ );
1800
+ return { text: fallbackText, blocks };
1801
+ }
1675
1802
  async postMessage(threadId, message) {
1676
1803
  const { channel, threadTs } = this.decodeThreadId(threadId);
1677
1804
  try {
@@ -1718,6 +1845,33 @@ var SlackAdapter = class _SlackAdapter {
1718
1845
  raw: result2
1719
1846
  };
1720
1847
  }
1848
+ const tableResult = this.renderWithTableBlocks(message);
1849
+ if (tableResult) {
1850
+ this.logger.debug("Slack API: chat.postMessage (table blocks)", {
1851
+ channel,
1852
+ threadTs,
1853
+ blockCount: tableResult.blocks.length
1854
+ });
1855
+ const result2 = await this.client.chat.postMessage(
1856
+ this.withToken({
1857
+ channel,
1858
+ thread_ts: threadTs,
1859
+ text: tableResult.text,
1860
+ blocks: tableResult.blocks,
1861
+ unfurl_links: false,
1862
+ unfurl_media: false
1863
+ })
1864
+ );
1865
+ this.logger.debug("Slack API: chat.postMessage response", {
1866
+ messageId: result2.ts,
1867
+ ok: result2.ok
1868
+ });
1869
+ return {
1870
+ id: result2.ts,
1871
+ threadId,
1872
+ raw: result2
1873
+ };
1874
+ }
1721
1875
  const text = convertEmojiPlaceholders(
1722
1876
  this.formatConverter.renderPostable(message),
1723
1877
  "slack"
@@ -1782,6 +1936,34 @@ var SlackAdapter = class _SlackAdapter {
1782
1936
  raw: result2
1783
1937
  };
1784
1938
  }
1939
+ const tableResult = this.renderWithTableBlocks(message);
1940
+ if (tableResult) {
1941
+ this.logger.debug("Slack API: chat.postEphemeral (table blocks)", {
1942
+ channel,
1943
+ threadTs,
1944
+ userId,
1945
+ blockCount: tableResult.blocks.length
1946
+ });
1947
+ const result2 = await this.client.chat.postEphemeral(
1948
+ this.withToken({
1949
+ channel,
1950
+ thread_ts: threadTs || void 0,
1951
+ user: userId,
1952
+ text: tableResult.text,
1953
+ blocks: tableResult.blocks
1954
+ })
1955
+ );
1956
+ this.logger.debug("Slack API: chat.postEphemeral response", {
1957
+ messageTs: result2.message_ts,
1958
+ ok: result2.ok
1959
+ });
1960
+ return {
1961
+ id: result2.message_ts || "",
1962
+ threadId,
1963
+ usedFallback: false,
1964
+ raw: result2
1965
+ };
1966
+ }
1785
1967
  const text = convertEmojiPlaceholders(
1786
1968
  this.formatConverter.renderPostable(message),
1787
1969
  "slack"
@@ -1958,6 +2140,31 @@ var SlackAdapter = class _SlackAdapter {
1958
2140
  raw: result2
1959
2141
  };
1960
2142
  }
2143
+ const tableResult = this.renderWithTableBlocks(message);
2144
+ if (tableResult) {
2145
+ this.logger.debug("Slack API: chat.update (table blocks)", {
2146
+ channel,
2147
+ messageId,
2148
+ blockCount: tableResult.blocks.length
2149
+ });
2150
+ const result2 = await this.client.chat.update(
2151
+ this.withToken({
2152
+ channel,
2153
+ ts: messageId,
2154
+ text: tableResult.text,
2155
+ blocks: tableResult.blocks
2156
+ })
2157
+ );
2158
+ this.logger.debug("Slack API: chat.update response", {
2159
+ messageId: result2.ts,
2160
+ ok: result2.ok
2161
+ });
2162
+ return {
2163
+ id: result2.ts,
2164
+ threadId,
2165
+ raw: result2
2166
+ };
2167
+ }
1961
2168
  const text = convertEmojiPlaceholders(
1962
2169
  this.formatConverter.renderPostable(message),
1963
2170
  "slack"
@@ -2764,13 +2971,22 @@ var SlackAdapter = class _SlackAdapter {
2764
2971
  blocks: cardToBlockKit(card)
2765
2972
  };
2766
2973
  } else {
2767
- payload = {
2768
- replace_original: true,
2769
- text: convertEmojiPlaceholders(
2770
- this.formatConverter.renderPostable(message),
2771
- "slack"
2772
- )
2773
- };
2974
+ const tableResult = this.renderWithTableBlocks(message);
2975
+ if (tableResult) {
2976
+ payload = {
2977
+ replace_original: true,
2978
+ text: tableResult.text,
2979
+ blocks: tableResult.blocks
2980
+ };
2981
+ } else {
2982
+ payload = {
2983
+ replace_original: true,
2984
+ text: convertEmojiPlaceholders(
2985
+ this.formatConverter.renderPostable(message),
2986
+ "slack"
2987
+ )
2988
+ };
2989
+ }
2774
2990
  }
2775
2991
  if (options?.threadTs) {
2776
2992
  payload.thread_ts = options.threadTs;