diff options
author | Trygve Aaberge <trygveaa@gmail.com> | 2023-07-06 22:13:35 +0200 |
---|---|---|
committer | Trygve Aaberge <trygveaa@gmail.com> | 2023-07-07 01:48:31 +0200 |
commit | 74da30342e8d328fc085d2158e37afda3b908f2c (patch) | |
tree | 6c3dd5fd96d56a6cb7653e665ba4c87ee202078b | |
parent | 857a42dee093ef87723d0e214a29bc346a29a595 (diff) | |
download | wee-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.py | 247 | ||||
-rw-r--r-- | wee_slack.py | 160 |
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): |