aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrygve Aaberge <trygveaa@gmail.com>2023-07-06 22:13:35 +0200
committerTrygve Aaberge <trygveaa@gmail.com>2023-07-07 01:48:31 +0200
commit74da30342e8d328fc085d2158e37afda3b908f2c (patch)
tree6c3dd5fd96d56a6cb7653e665ba4c87ee202078b
parent857a42dee093ef87723d0e214a29bc346a29a595 (diff)
downloadwee-slack-74da30342e8d328fc085d2158e37afda3b908f2c.tar.gz
Support and use rich_text blocks for message content
Instead of just using the text field of a message, use the rich_text blocks to format the message. The main reason for doing this was that messages that include images suddenly started just having "This message contains interactive elements." in the text field. There are some other benefits as well: - It fixes some bugs with multiple formatting styles not being applied correctly. - It fixes links in code blocks not being rendered verbatim (but with text and url, like outside code blocks). - It fixes code blocks sometimes having newlines after/before ``` and sometimes not, depending on how the message was sent. It looks like messages with images are now back to having the proper text in the text field (for new messages), so this change isn't necessary to fix that after all (except for for messages in the history sent in the last couple of days), but since it fixes other issues as well, it's best to keep it. There is one bug left when inline code blocks contains links. Then there will be ` characters before and after the link, which shouldn't be there, but I'll leave fixing this for later. Fixes #354, fixes #550, fixes #893
-rw-r--r--_pytest/test_render_blocks.py247
-rw-r--r--wee_slack.py160
2 files changed, 397 insertions, 10 deletions
diff --git a/_pytest/test_render_blocks.py b/_pytest/test_render_blocks.py
new file mode 100644
index 0000000..258fc49
--- /dev/null
+++ b/_pytest/test_render_blocks.py
@@ -0,0 +1,247 @@
+from __future__ import print_function, unicode_literals
+
+import pytest
+
+import wee_slack
+
+
+@pytest.mark.parametrize(
+ "case",
+ [
+ {
+ "blocks": [
+ {
+ "type": "rich_text",
+ "block_id": "dhGA",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {"type": "text", "text": "normal "},
+ {
+ "type": "text",
+ "text": "bold",
+ "style": {"bold": True},
+ },
+ {"type": "text", "text": " "},
+ {
+ "type": "text",
+ "text": "italic",
+ "style": {"italic": True},
+ },
+ {"type": "text", "text": " "},
+ {
+ "type": "text",
+ "text": "strikethrough",
+ "style": {"strike": True},
+ },
+ {"type": "text", "text": " "},
+ {
+ "type": "text",
+ "text": "bold-italic-strikethrough",
+ "style": {
+ "bold": True,
+ "italic": True,
+ "strike": True,
+ },
+ },
+ {"type": "text", "text": " "},
+ {
+ "type": "link",
+ "url": "https://vg.no",
+ "text": "link",
+ },
+ {"type": "text", "text": "\n"},
+ ],
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "number"}],
+ },
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "list"}],
+ },
+ ],
+ "style": "ordered",
+ "indent": 0,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_quote",
+ "elements": [
+ {"type": "text", "text": "some quote\nmore quote"}
+ ],
+ },
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {
+ "type": "text",
+ "text": "inline code",
+ "style": {"code": True},
+ },
+ {"type": "text", "text": "\n"},
+ ],
+ },
+ {
+ "type": "rich_text_preformatted",
+ "elements": [
+ {"type": "text", "text": "block code\nmore code"}
+ ],
+ "border": 0,
+ },
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {"type": "user", "user_id": "U407ABLLW"},
+ {"type": "text", "text": ": "},
+ {
+ "type": "emoji",
+ "name": "open_mouth",
+ "unicode": "1f62e",
+ },
+ ],
+ },
+ ],
+ }
+ ],
+ "rendered": [
+ "normal <[color bold]>*bold*<[color -bold]> <[color italic]>_italic_<[color -italic]> ~strikethrough~ <[color bold]><[color italic]>*_~bold-italic-strikethrough~_*<[color -bold]><[color -italic]> https://vg.no (link)",
+ "1. number",
+ "2. list",
+ "> some quote",
+ "> more quote",
+ "`inline code`",
+ "```\nblock code\nmore code\n```",
+ "@alice: :open_mouth:",
+ ],
+ },
+ {
+ "blocks": [
+ {
+ "type": "rich_text",
+ "block_id": "D=pkP",
+ "elements": [
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "number"}],
+ }
+ ],
+ "style": "ordered",
+ "indent": 0,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "list"}],
+ }
+ ],
+ "style": "ordered",
+ "indent": 1,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "third"}],
+ }
+ ],
+ "style": "ordered",
+ "indent": 2,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {"type": "text", "text": "end number list"}
+ ],
+ }
+ ],
+ "style": "ordered",
+ "indent": 0,
+ "offset": 1,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "bullet"}],
+ }
+ ],
+ "style": "bullet",
+ "indent": 0,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "list"}],
+ }
+ ],
+ "style": "bullet",
+ "indent": 1,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "third"}],
+ }
+ ],
+ "style": "bullet",
+ "indent": 2,
+ "border": 0,
+ },
+ {
+ "type": "rich_text_list",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {"type": "text", "text": "end bullet list"}
+ ],
+ }
+ ],
+ "style": "bullet",
+ "indent": 0,
+ "border": 0,
+ },
+ ],
+ }
+ ],
+ "rendered": [
+ "1. number",
+ " a. list",
+ " i. third",
+ "2. end number list",
+ "• bullet",
+ " ◦ list",
+ " ▪︎ third",
+ "• end bullet list",
+ ],
+ },
+ ],
+)
+def test_render_blocks(case):
+ assert wee_slack.unfurl_blocks(case["blocks"]) == case["rendered"]
diff --git a/wee_slack.py b/wee_slack.py
index ff794f9..c87ea78 100644
--- a/wee_slack.py
+++ b/wee_slack.py
@@ -3394,14 +3394,7 @@ class SlackMessage(object):
blocks = self.message_json.get("blocks", [])
blocks_rendered = "\n".join(unfurl_blocks(blocks))
- has_rich_text = any(block["type"] == "rich_text" for block in blocks)
- if has_rich_text:
- text = self.message_json.get("text", "")
- if blocks_rendered:
- if text:
- text += "\n"
- text += blocks_rendered
- elif blocks_rendered:
+ if blocks_rendered:
text = blocks_rendered
else:
text = self.message_json.get("text", "")
@@ -4605,7 +4598,47 @@ def unfurl_blocks(blocks):
block_text.append(unfurl_block_element(block["title"]))
block_text.append(unfurl_block_element(block))
elif block["type"] == "rich_text":
- continue
+ for element in block.get("elements", []):
+ if element["type"] == "rich_text_section":
+ rendered = unfurl_rich_text_section(element)
+ if rendered:
+ block_text.append(rendered)
+ elif element["type"] == "rich_text_list":
+ rendered = [
+ "{}{} {}".format(
+ " " * element.get("indent", 0),
+ block_list_prefix(
+ element, element.get("offset", 0) + i
+ ),
+ unfurl_rich_text_section(e),
+ )
+ for i, e in enumerate(element["elements"])
+ ]
+ block_text.extend(rendered)
+ elif element["type"] == "rich_text_quote":
+ lines = [
+ "> {}".format(line)
+ for e in element["elements"]
+ for line in unfurl_block_element(e).split("\n")
+ ]
+ block_text.extend(lines)
+ elif element["type"] == "rich_text_preformatted":
+ texts = [
+ e.get("text", e.get("url", "")) for e in element["elements"]
+ ]
+ if texts:
+ block_text.append("```\n{}\n```".format("".join(texts)))
+ else:
+ text = '<<Unsupported rich_text type "{}">>'.format(
+ element["type"]
+ )
+ block_text.append(colorize_string(config.color_deleted, text))
+ dbg(
+ "Unsupported rich_text element: '{}'".format(
+ json.dumps(element)
+ ),
+ level=4,
+ )
else:
block_text.append(
colorize_string(
@@ -4613,7 +4646,7 @@ def unfurl_blocks(blocks):
'<<Unsupported block type "{}">>'.format(block["type"]),
)
)
- dbg('Unsupported block: "{}"'.format(json.dumps(block)), level=4)
+ dbg("Unsupported block: '{}'".format(json.dumps(block)), level=4)
except Exception as e:
dbg(
"Failed to unfurl block ({}): {}".format(repr(e), json.dumps(block)),
@@ -4622,9 +4655,92 @@ def unfurl_blocks(blocks):
return block_text
+def convert_int_to_letter(num):
+ letter = ""
+ while num > 0:
+ num -= 1
+ letter = chr((num % 26) + 97) + letter
+ num //= 26
+ return letter
+
+
+def convert_int_to_roman(num):
+ roman_numerals = {
+ 1000: "m",
+ 900: "cm",
+ 500: "d",
+ 400: "cd",
+ 100: "c",
+ 90: "xc",
+ 50: "l",
+ 40: "xl",
+ 10: "x",
+ 9: "ix",
+ 5: "v",
+ 4: "iv",
+ 1: "i",
+ }
+ roman_numeral = ""
+ for value, symbol in roman_numerals.items():
+ while num >= value:
+ roman_numeral += symbol
+ num -= value
+ return roman_numeral
+
+
+def block_list_prefix(element, index):
+ if element["style"] == "ordered":
+ if element["indent"] == 0 or element["indent"] == 3:
+ return "{}.".format(index + 1)
+ elif element["indent"] == 1 or element["indent"] == 4:
+ return "{}.".format(convert_int_to_letter(index + 1))
+ else:
+ return "{}.".format(convert_int_to_roman(index + 1))
+ else:
+ if element["indent"] == 0 or element["indent"] == 3:
+ return "•"
+ elif element["indent"] == 1 or element["indent"] == 4:
+ return "◦"
+ else:
+ return "▪︎"
+
+
+def unfurl_rich_text_section(block):
+ text = "".join(
+ unfurl_block_element(sub_element) for sub_element in block["elements"]
+ )
+ if text.endswith("\n"):
+ return text[:-1]
+ else:
+ return text
+
+
def unfurl_block_element(text):
if text["type"] == "mrkdwn":
return render_formatting(text["text"])
+ elif text["type"] == "text":
+ colors = []
+ characters = []
+ if text.get("style", {}).get("bold"):
+ colors.append(config.render_bold_as)
+ characters.append("*")
+ if text.get("style", {}).get("italic"):
+ colors.append(config.render_italic_as)
+ characters.append("_")
+ if text.get("style", {}).get("strike"):
+ characters.append("~")
+ if text.get("style", {}).get("code"):
+ characters.append("`")
+
+ colors_start = "".join(w.color(color) for color in colors)
+ colors_end = "".join(w.color("-" + color) for color in colors)
+ return (
+ colors_start
+ + "".join(characters)
+ + text["text"]
+ + "".join(reversed(characters))
+ + colors_end
+ )
elif text["type"] == "plain_text":
return text["text"]
elif text["type"] == "image":
@@ -4632,6 +4748,30 @@ def unfurl_block_element(text):
return "{} ({})".format(text["image_url"], text["alt_text"])
else:
return text["image_url"]
+ elif text["type"] == "link":
+ if text.get("text"):
+ if text.get("style", {}).get("code"):
+ return text["text"]
+ else:
+ return "{} ({})".format(text["url"], text["text"])
+ else:
+ return text["url"]
+ elif text["type"] == "emoji":
+ return replace_string_with_emoji(":{}:".format(text["name"]))
+ elif text["type"] == "user":
+ return resolve_ref("@{}".format(text["user_id"]))
+ elif text["type"] == "usergroup":
+ return resolve_ref("!subteam^{}".format(text["usergroup_id"]))
+ elif text["type"] == "broadcast":
+ return resolve_ref("@{}".format(text["range"]))
+ elif text["type"] == "channel":
+ return resolve_ref("#{}".format(text["channel_id"]))
+ else:
+ dbg("Unsupported block element: '{}'".format(json.dumps(text)), level=4)
+ return colorize_string(
+ config.color_deleted,
+ '<<Unsupported block element type "{}">>'.format(text["type"]),
+ )
def unfurl_refs(text):