Skip to content

Commit 20cd921

Browse files
committed
feat(slack): native markdown_text for outgoing messages (vercel/chat#440)
Port of upstream 3546b3f. Slack natively renders markdown via the markdown_text parameter on chat.postMessage / postEphemeral / update / scheduleMessage, so the adapter passes markdown through directly instead of converting to mrkdwn. - str / PostableRaw messages still go to text (preserves literal *). - PostableMarkdown / PostableAst go to markdown_text (12k char limit). - to_blocks_with_table, _mdast_table_to_slack_block, and the _render_with_table_blocks call sites are removed (tables now ride along in markdown_text). - SlackMarkdownConverter alias removed; use SlackFormatConverter. - render_formatted(ast) / from_ast now return standard markdown (was mrkdwn) via stringify_markdown. - response_url payloads reject markdown_text (no_text), so those render through the retained mrkdwn node renderer (to_response_url_text). - Incoming message events still arrive as mrkdwn and parse unchanged. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit f07815e327ef1f2ba8ff688b2eb593af818c4ab6)
1 parent 80508ff commit 20cd921

4 files changed

Lines changed: 512 additions & 607 deletions

File tree

src/chat_sdk/adapters/slack/adapter.py

Lines changed: 51 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
SlackThreadId,
5656
SlackWebhookVerifier,
5757
)
58-
from chat_sdk.emoji import convert_emoji_placeholders, emoji_to_slack, resolve_emoji_from_slack
58+
from chat_sdk.emoji import emoji_to_slack, resolve_emoji_from_slack
5959
from chat_sdk.logger import ConsoleLogger, Logger
6060
from chat_sdk.modals import ModalElement, OptionsLoadGroup, SelectOptionElement
6161
from chat_sdk.shared.adapter_utils import extract_card, extract_files
@@ -3354,35 +3354,6 @@ def _is_message_from_self(self, event: dict[str, Any]) -> bool:
33543354
return True
33553355
return bool(self._bot_id and event.get("bot_id") == self._bot_id)
33563356

3357-
# ==================================================================
3358-
# Table block rendering
3359-
# ==================================================================
3360-
3361-
def _render_with_table_blocks(self, message: AdapterPostableMessage) -> dict[str, Any] | None:
3362-
"""Try to render a message with native Slack table blocks.
3363-
3364-
Returns ``{"text": ..., "blocks": ...}`` if the message contains tables,
3365-
``None`` otherwise.
3366-
"""
3367-
ast: dict[str, Any] | None = None
3368-
if isinstance(message, dict):
3369-
ast = message.get("ast") # type: ignore[union-attr]
3370-
elif hasattr(message, "ast"):
3371-
ast = getattr(message, "ast", None)
3372-
elif hasattr(message, "markdown"):
3373-
# We don't have a full markdown->AST parser in Python; skip table blocks
3374-
return None
3375-
3376-
if not ast:
3377-
return None
3378-
3379-
blocks = self._format_converter.to_blocks_with_table(ast)
3380-
if not blocks:
3381-
return None
3382-
3383-
fallback_text = convert_emoji_placeholders(self._format_converter.render_postable(message), "slack")
3384-
return {"text": fallback_text, "blocks": blocks}
3385-
33863357
# ==================================================================
33873358
# Post / Edit / Delete messages
33883359
# ==================================================================
@@ -3446,38 +3417,21 @@ async def post_message(self, thread_id: str, message: AdapterPostableMessage) ->
34463417
),
34473418
)
34483419

3449-
# Table blocks
3450-
table_result = self._render_with_table_blocks(message)
3451-
if table_result:
3452-
result = await client.chat_postMessage(
3453-
channel=channel,
3454-
thread_ts=thread_ts or None,
3455-
text=table_result["text"],
3456-
blocks=table_result["blocks"],
3457-
unfurl_links=False,
3458-
unfurl_media=False,
3459-
)
3460-
return RawMessage(
3461-
id=result.get("ts", ""),
3462-
thread_id=thread_id,
3463-
raw=self._augment_raw_with_uploads(
3464-
result.data if hasattr(result, "data") else result,
3465-
uploaded_file_ids,
3466-
),
3467-
)
3468-
3469-
# Regular text
3470-
text = convert_emoji_placeholders(self._format_converter.render_postable(message), "slack")
3420+
payload = self._format_converter.to_slack_payload(message)
34713421
self._logger.debug(
34723422
"Slack API: chat.postMessage",
3473-
{"channel": channel, "threadTs": thread_ts, "textLength": len(text)},
3423+
{
3424+
"channel": channel,
3425+
"threadTs": thread_ts,
3426+
"payloadKey": "markdown_text" if "markdown_text" in payload else "text",
3427+
},
34743428
)
34753429
result = await client.chat_postMessage(
34763430
channel=channel,
34773431
thread_ts=thread_ts or None,
3478-
text=text,
34793432
unfurl_links=False,
34803433
unfurl_media=False,
3434+
**payload,
34813435
)
34823436
return RawMessage(
34833437
id=result.get("ts", ""),
@@ -3547,22 +3501,16 @@ async def edit_message(
35473501
raw=result.data if hasattr(result, "data") else result,
35483502
)
35493503

3550-
table_result = self._render_with_table_blocks(message)
3551-
if table_result:
3552-
result = await client.chat_update(
3553-
channel=channel,
3554-
ts=message_id,
3555-
text=table_result["text"],
3556-
blocks=table_result["blocks"],
3557-
)
3558-
return RawMessage(
3559-
id=result.get("ts", ""),
3560-
thread_id=thread_id,
3561-
raw=result.data if hasattr(result, "data") else result,
3562-
)
3563-
3564-
text = convert_emoji_placeholders(self._format_converter.render_postable(message), "slack")
3565-
result = await client.chat_update(channel=channel, ts=message_id, text=text)
3504+
payload = self._format_converter.to_slack_payload(message)
3505+
self._logger.debug(
3506+
"Slack API: chat.update",
3507+
{
3508+
"channel": channel,
3509+
"messageId": message_id,
3510+
"payloadKey": "markdown_text" if "markdown_text" in payload else "text",
3511+
},
3512+
)
3513+
result = await client.chat_update(channel=channel, ts=message_id, **payload)
35663514
return RawMessage(
35673515
id=result.get("ts", ""),
35683516
thread_id=thread_id,
@@ -3864,28 +3812,21 @@ async def post_ephemeral(
38643812
raw=result.data if hasattr(result, "data") else result,
38653813
)
38663814

3867-
table_result = self._render_with_table_blocks(message)
3868-
if table_result:
3869-
result = await client.chat_postEphemeral(
3870-
channel=channel,
3871-
thread_ts=thread_ts or None,
3872-
user=user_id,
3873-
text=table_result["text"],
3874-
blocks=table_result["blocks"],
3875-
)
3876-
return EphemeralMessage(
3877-
id=result.get("message_ts", ""),
3878-
thread_id=thread_id,
3879-
used_fallback=False,
3880-
raw=result.data if hasattr(result, "data") else result,
3881-
)
3882-
3883-
text = convert_emoji_placeholders(self._format_converter.render_postable(message), "slack")
3815+
payload = self._format_converter.to_slack_payload(message)
3816+
self._logger.debug(
3817+
"Slack API: chat.postEphemeral",
3818+
{
3819+
"channel": channel,
3820+
"threadTs": thread_ts,
3821+
"userId": user_id,
3822+
"payloadKey": "markdown_text" if "markdown_text" in payload else "text",
3823+
},
3824+
)
38843825
result = await client.chat_postEphemeral(
38853826
channel=channel,
38863827
thread_ts=thread_ts or None,
38873828
user=user_id,
3888-
text=text,
3829+
**payload,
38893830
)
38903831
return EphemeralMessage(
38913832
id=result.get("message_ts", ""),
@@ -3948,14 +3889,23 @@ async def schedule_message(
39483889
unfurl_media=False,
39493890
)
39503891
else:
3951-
text = convert_emoji_placeholders(self._format_converter.render_postable(message), "slack")
3892+
payload = self._format_converter.to_slack_payload(message)
3893+
self._logger.debug(
3894+
"Slack API: chat.scheduleMessage",
3895+
{
3896+
"channel": channel,
3897+
"threadTs": thread_ts,
3898+
"postAt": post_at_unix,
3899+
"payloadKey": "markdown_text" if "markdown_text" in payload else "text",
3900+
},
3901+
)
39523902
result = await client.chat_scheduleMessage(
39533903
channel=channel,
39543904
thread_ts=thread_ts or None,
39553905
post_at=post_at_unix,
3956-
text=text,
39573906
unfurl_links=False,
39583907
unfurl_media=False,
3908+
**payload,
39593909
)
39603910

39613911
scheduled_message_id = result.get("scheduled_message_id", "")
@@ -4425,7 +4375,10 @@ def parse_message(self, raw: dict[str, Any]) -> Message:
44254375
return self._parse_slack_message_sync(event, thread_id)
44264376

44274377
def render_formatted(self, content: FormattedContent) -> str:
4428-
"""Render formatted content (AST) to Slack mrkdwn."""
4378+
"""Render formatted content (AST) to standard markdown.
4379+
4380+
Slack now accepts markdown natively via ``markdown_text``.
4381+
"""
44294382
return self._format_converter.from_ast(content)
44304383

44314384
# ==================================================================
@@ -4547,18 +4500,13 @@ async def _send_to_response_url(
45474500
"blocks": card_to_block_kit(card),
45484501
}
45494502
else:
4550-
table_result = self._render_with_table_blocks(message)
4551-
if table_result:
4552-
payload = {
4553-
"replace_original": True,
4554-
"text": table_result["text"],
4555-
"blocks": table_result["blocks"],
4556-
}
4557-
else:
4558-
payload = {
4559-
"replace_original": True,
4560-
"text": convert_emoji_placeholders(self._format_converter.render_postable(message), "slack"),
4561-
}
4503+
# Slack rejects `markdown_text` on response_url payloads
4504+
# (`no_text`), so markdown/AST messages are rendered to
4505+
# Slack's legacy mrkdwn format for this surface.
4506+
payload = {
4507+
"replace_original": True,
4508+
"text": self._format_converter.to_response_url_text(message),
4509+
}
45624510

45634511
if thread_ts:
45644512
payload["thread_ts"] = thread_ts

0 commit comments

Comments
 (0)