-
Notifications
You must be signed in to change notification settings - Fork 406
Fix code blocks in Slack messages #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,26 +90,88 @@ def to_slack_link(match: re.Match[str]) -> str: | |
| ) | ||
|
|
||
|
|
||
| def transform_code_block(text: str) -> str: | ||
| """ | ||
| Remove the language specifier from a Markdown code block. | ||
| Slack does not support language-specific code blocks. | ||
|
|
||
| Example: | ||
| ```python | ||
| print("Hello, World!") | ||
| ``` | ||
|
|
||
| becomes: | ||
| ``` | ||
| print("Hello, World!") | ||
| ``` | ||
| """ | ||
| # Remove the language specifier from the code block | ||
| code_block_pattern = r"```(?:\w+)?(.*?)```" | ||
|
|
||
| return re.sub(code_block_pattern, r"```\1```", text, flags=re.DOTALL) | ||
|
|
||
|
|
||
| class SlackMessage: | ||
| """ | ||
| Handles transformation of Markdown to Slack-formatted messages. | ||
| """ | ||
|
|
||
| _transforms = [ | ||
| convert_md_links_to_slack, | ||
| transform_code_block, | ||
| ] | ||
|
|
||
| def __init__(self, text: str, channel_id=None, ts=None, thread_ts=None, attachments=None): | ||
| self.text = text | ||
| self.channel = channel_id | ||
| self.ts = ts | ||
| self.thread_ts = thread_ts | ||
| self.attachments = attachments or [] | ||
|
|
||
| @staticmethod | ||
| async def from_message(channel_id: str, ts: str): | ||
| text = await fetch_current_message_text(channel_id, ts) | ||
| return SlackMessage(text, channel_id=channel_id, ts=ts) | ||
|
|
||
| def _get_transformed_text(self) -> str: | ||
| t = self.text | ||
| for fn in self._transforms: | ||
| t = fn(t) | ||
| return t | ||
|
|
||
| def append(self, text, delimiter="\n\n"): | ||
| self.text = f"{self.text}{delimiter}{text}" | ||
|
|
||
| def json(self): | ||
| return { | ||
| k: v for k, v in { | ||
| "text": self._get_transformed_text(), | ||
| "channel": self.channel, | ||
| "ts": self.ts, | ||
| "thread_ts": self.thread_ts, | ||
| "attachments": self.attachments or None, | ||
| }.items() if v is not None | ||
| } | ||
|
|
||
|
|
||
| async def post_slack_message( | ||
| message: str, | ||
| channel_id: str, | ||
| attachments: list[dict[str, Any]] | None = None, | ||
| thread_ts: str | None = None, | ||
| ) -> httpx.Response: | ||
| post_data = { | ||
| "channel": channel_id, | ||
| "text": convert_md_links_to_slack(message), | ||
| "attachments": attachments if attachments else [], | ||
| } | ||
|
|
||
| if thread_ts: | ||
| post_data["thread_ts"] = thread_ts | ||
| message = SlackMessage( | ||
| message, | ||
| channel_id=channel_id, | ||
| attachments=attachments, | ||
| thread_ts=thread_ts, | ||
| ) | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| response = await client.post( | ||
| "https://slack.com/api/chat.postMessage", | ||
| headers={"Authorization": f"Bearer {settings.slack_api_token}"}, | ||
| json=post_data, | ||
| json=message.json(), | ||
| ) | ||
| response_data = response.json() | ||
|
|
||
|
|
@@ -187,28 +249,29 @@ async def edit_slack_message( | |
| """Edit an existing Slack message by appending new text or replacing it. | ||
|
|
||
| Args: | ||
| channel (str): The Slack channel ID. | ||
| ts (str): The timestamp of the message to edit. | ||
| channel_id (str): The Slack channel ID. | ||
| thread_ts (str): The timestamp of the message to edit. | ||
| new_text (str): The new text to append or replace in the message. | ||
| mode (str): The mode of text editing, 'append' (default) or 'replace'. | ||
| delimiter (Union[str, None]): The delimiter to use if appending to an | ||
| existing message. | ||
|
|
||
| Returns: | ||
| httpx.Response: The response from the Slack API. | ||
| """ | ||
| if mode == "append": | ||
| current_text = await fetch_current_message_text(channel_id, thread_ts) | ||
| delimiter = "\n\n" if delimiter is None else delimiter | ||
| updated_text = f"{current_text}{delimiter}{convert_md_links_to_slack(new_text)}" | ||
| message = await SlackMessage.from_message(channel_id, thread_ts) | ||
| message.append(new_text, delimiter=delimiter) | ||
|
||
| elif mode == "replace": | ||
| updated_text = convert_md_links_to_slack(new_text) | ||
| message = SlackMessage(new_text) | ||
| else: | ||
| raise ValueError("Invalid mode. Use 'append' or 'replace'.") | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| response = await client.post( | ||
| "https://slack.com/api/chat.update", | ||
| headers={"Authorization": f"Bearer {settings.slack_api_token}"}, | ||
| json={"channel": channel_id, "ts": thread_ts, "text": updated_text}, | ||
| json=message.json(), | ||
|
||
| ) | ||
|
|
||
| response.raise_for_status() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import os | ||
| import unittest | ||
| from unittest import mock | ||
|
|
||
| @mock.patch.dict(os.environ, {"MARVIN_SLACKBOT_SLACK_API_TOKEN": "xoxb-fake"}) | ||
| class SlackMessageClassTests(unittest.TestCase): | ||
| def test_transforms_code_text(self): | ||
| from slackbot.slack import SlackMessage | ||
|
|
||
| code = """```python | ||
| print(test) | ||
| ```""" | ||
|
|
||
| message = SlackMessage(code) | ||
|
|
||
| expected = """``` | ||
| print(test) | ||
| ```""" | ||
|
|
||
| self.assertEqual(expected, message._get_transformed_text()) | ||
|
|
||
| def test_append(self): | ||
| from slackbot.slack import SlackMessage | ||
|
|
||
| code = "Original message" | ||
|
|
||
| message = SlackMessage(code) | ||
| message.append("Updated text") | ||
|
|
||
| expected = "Original message\n\nUpdated text" | ||
|
|
||
| self.assertEqual(expected, message.json()["text"]) | ||
|
|
||
| def test_correct_attributes(self): | ||
| from slackbot.slack import SlackMessage | ||
| text = "Test message" | ||
| code = """```python | ||
| print(test) | ||
| ```""" | ||
| code_output = """``` | ||
| print(test) | ||
| ```""" | ||
|
|
||
| message = SlackMessage(text) | ||
| attributes = { | ||
| "text": text | ||
| } | ||
| self.assertEqual(attributes, message.json()) | ||
|
|
||
| message = SlackMessage(text, channel_id=1, ts=1234567890) | ||
| attributes = { | ||
| "text": text, | ||
| "channel": 1, | ||
| "ts": 1234567890 | ||
| } | ||
| self.assertEqual(attributes, message.json()) | ||
|
|
||
| message = SlackMessage(text, channel_id=1, thread_ts=1234567890) | ||
| attributes = { | ||
| "text": text, | ||
| "channel": 1, | ||
| "thread_ts": 1234567890 | ||
| } | ||
| self.assertEqual(attributes, message.json()) | ||
|
|
||
| message = SlackMessage(code, channel_id=1, thread_ts=1234567890) | ||
| attributes = { | ||
| "text": code_output, | ||
| "channel": 1, | ||
| "thread_ts": 1234567890 | ||
| } | ||
| self.assertEqual(attributes, message.json()) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern doesn't properly handle the optional language specifier. The
(?:\w+)?makes the language specifier optional but doesn't account for newlines after it. This will fail to match code blocks with language specifiers followed by newlines. Consider usingr"```(?:\w+\n)?(.*?)```"to properly capture language specifiers followed by newlines.