diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 87febae7..96ecfb9e 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -43,6 +43,12 @@ from .block_elements import StaticSelectElement from .block_elements import UserMultiSelectElement from .block_elements import UserSelectElement +from .block_elements import RichTextElement +from .block_elements import RichTextElementParts +from .block_elements import RichTextListElement +from .block_elements import RichTextPreformattedElement +from .block_elements import RichTextQuoteElement +from .block_elements import RichTextSectionElement from .blocks import ActionsBlock from .blocks import Block from .blocks import CallBlock @@ -54,6 +60,7 @@ from .blocks import InputBlock from .blocks import SectionBlock from .blocks import VideoBlock +from .blocks import RichTextBlock __all__ = [ "ButtonStyles", @@ -93,6 +100,12 @@ "StaticSelectElement", "UserMultiSelectElement", "UserSelectElement", + "RichTextElement", + "RichTextElementParts", + "RichTextListElement", + "RichTextPreformattedElement", + "RichTextQuoteElement", + "RichTextSectionElement", "ActionsBlock", "Block", "CallBlock", @@ -104,4 +117,5 @@ "InputBlock", "SectionBlock", "VideoBlock", + "RichTextBlock", ] diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py index 8b9ac4ed..bf2518f0 100644 --- a/slack_sdk/models/blocks/block_elements.py +++ b/slack_sdk/models/blocks/block_elements.py @@ -1834,3 +1834,309 @@ def __init__( self.workflow = workflow self.style = style self.accessibility_label = accessibility_label + + +# ------------------------------------------------- +# Rich text elements +# ------------------------------------------------- + + +class RichTextElement(BlockElement): + pass + + +class RichTextListElement(RichTextElement): + type = "rich_text_list" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"elements", "style", "indent", "offset", "border"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + style: Optional[str] = None, # bullet, ordered + indent: Optional[int] = None, + offset: Optional[int] = None, + border: Optional[int] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = elements + self.style = style + self.indent = indent + self.offset = offset + self.border = border + + +class RichTextPreformattedElement(RichTextElement): + type = "rich_text_preformatted" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"elements", "border"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + border: Optional[int] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = elements + self.border = border + + +class RichTextQuoteElement(RichTextElement): + type = "rich_text_quote" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = elements + + +class RichTextSectionElement(RichTextElement): + type = "rich_text_section" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = elements + + +class RichTextElementParts: + class TextStyle: + def __init__( + self, + *, + bold: Optional[bool] = None, + italic: Optional[bool] = None, + strike: Optional[bool] = None, + code: Optional[bool] = None, + ): + self.bold = bold + self.italic = italic + self.strike = strike + self.code = code + + def to_dict(self, *args) -> dict: + result = { + "bold": self.bold, + "italic": self.italic, + "strike": self.strike, + "code": self.code, + } + return {k: v for k, v in result.items() if v is not None} + + class Text(RichTextElement): + type = "text" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"text", "style"}) + + def __init__( + self, + *, + text: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.text = text + self.style = style + + class Channel(RichTextElement): + type = "channel" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"channel_id", "style"}) + + def __init__( + self, + *, + channel_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.channel_id = channel_id + self.style = style + + class User(RichTextElement): + type = "user" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"user_id", "style"}) + + def __init__( + self, + *, + user_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.user_id = user_id + self.style = style + + class Emoji(RichTextElement): + type = "emoji" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"name", "skin_tone", "unicode", "style"}) + + def __init__( + self, + *, + name: str, + skin_tone: Optional[int] = None, + unicode: Optional[str] = None, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.name = name + self.skin_tone = skin_tone + self.unicode = unicode + self.style = style + + class Link(RichTextElement): + type = "link" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"url", "text", "style"}) + + def __init__( + self, + *, + url: str, + text: Optional[str] = None, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.url = url + self.text = text + self.style = style + + class Team(RichTextElement): + type = "team" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"team_id", "style"}) + + def __init__( + self, + *, + team_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.team_id = team_id + self.style = style + + class UserGroup(RichTextElement): + type = "usergroup" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"usergroup_id", "style"}) + + def __init__( + self, + *, + usergroup_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.usergroup_id = usergroup_id + self.style = style + + class Date(RichTextElement): + type = "date" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"timestamp"}) + + def __init__( + self, + *, + timestamp: str, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.timestamp = timestamp + + class Broadcast(RichTextElement): + type = "broadcast" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"range"}) + + def __init__( + self, + *, + range: str, # channel, here, .. + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.range = range + + class Color(RichTextElement): + type = "color" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"value"}) + + def __init__( + self, + *, + value: str, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.value = value diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 590f8ae2..5bd0171d 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -11,7 +11,7 @@ from .basic_components import MarkdownTextObject from .basic_components import PlainTextObject from .basic_components import TextObject -from .block_elements import BlockElement +from .block_elements import BlockElement, RichTextElement from .block_elements import ImageElement from .block_elements import InputInteractiveElement from .block_elements import InteractiveElement @@ -604,3 +604,34 @@ def _validate_title_length(self): @JsonValidator(f"author_name attribute cannot exceed {author_name_max_length} characters") def _validate_author_name_length(self): return self.author_name is None or len(self.author_name) < self.author_name_max_length + + +class RichTextBlock(Block): + type = "rich_text" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + block_id: Optional[str] = None, + **others: dict, + ): + """A block that is used to hold interactive elements. + https://api.slack.com/reference/block-kit/blocks#rich_text + + Args: + elements (required): An array of rich text objects - + rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted + block_id: A unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message or view and each iteration of a message or view. + If a message or view is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 5461ba8d..4ea1158e 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -22,6 +22,12 @@ HeaderBlock, VideoBlock, Option, + RichTextBlock, + RichTextSectionElement, + RichTextListElement, + RichTextQuoteElement, + RichTextPreformattedElement, + RichTextElementParts, ) from . import STRING_3001_CHARS @@ -825,3 +831,219 @@ def test_title_length_200(self): } with self.assertRaises(SlackObjectFormationError): VideoBlock(**input).validate_json() + + +# ---------------------------------------------- +# RichTextBlock +# ---------------------------------------------- + + +class RichTextBlockTests(unittest.TestCase): + def test_document(self): + inputs = [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"type": "text", "text": "Hello there, I am a basic rich text block!"}], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am a bold rich text block!", "style": {"bold": True}}, + ], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am an italic rich text block!", "style": {"italic": True}}, + ], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am a strikethrough rich text block!", "style": {"strike": True}}, + ], + } + ], + }, + ] + for input in inputs: + self.assertDictEqual(input, RichTextBlock(**input).to_dict()) + + def test_complex(self): + self.maxDiff = None + dict_block = { + "type": "rich_text", + "block_id": "3Uk3Q", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hey!", "style": {"bold": True}}, + {"type": "text", "text": " this is "}, + {"type": "text", "text": "very", "style": {"strike": True}}, + {"type": "text", "text": " rich text "}, + {"type": "text", "text": "block", "style": {"code": True}}, + {"type": "text", "text": " "}, + {"type": "text", "text": "test", "style": {"italic": True}}, + {"type": "link", "url": "https://slack.com", "text": "Slack website!"}, + ], + }, + { + "type": "rich_text_list", + "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": "a"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "b"}]}, + ], + "style": "ordered", + "indent": 0, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "bb"}]}], + "style": "ordered", + "indent": 1, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "BBB"}]}], + "style": "ordered", + "indent": 2, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "c"}]}], + "style": "ordered", + "indent": 0, + "offset": 2, + "border": 0, + }, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]}, + { + "type": "rich_text_list", + "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + ], + "style": "bullet", + "indent": 0, + "border": 0, + }, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]}, + {"type": "rich_text_quote", "elements": [{"type": "text", "text": "this is very important"}]}, + { + "type": "rich_text_preformatted", + "elements": [{"type": "text", "text": 'print("Hello world")'}], + "border": 0, + }, + { + "type": "rich_text_section", + "elements": [ + {"type": "user", "user_id": "WJC6QG0MS"}, + {"type": "text", "text": " "}, + {"type": "usergroup", "usergroup_id": "S01BL602YLU"}, + {"type": "text", "text": " "}, + {"type": "channel", "channel_id": "C02GD0YEHDJ"}, + ], + }, + ], + } + self.assertDictEqual(dict_block, RichTextBlock(**dict_block).to_dict()) + + _ = RichTextElementParts + class_block = RichTextBlock( + block_id="3Uk3Q", + elements=[ + RichTextSectionElement( + elements=[ + _.Text(text="Hey!", style=_.TextStyle(bold=True)), + _.Text(text=" this is "), + _.Text(text="very", style=_.TextStyle(strike=True)), + _.Text(text=" rich text "), + _.Text(text="block", style=_.TextStyle(code=True)), + _.Text(text=" "), + _.Text(text="test", style=_.TextStyle(italic=True)), + _.Link(text="Slack website!", url="https://slack.com"), + ] + ), + RichTextListElement( + elements=[ + RichTextSectionElement(elements=[_.Text(text="a")]), + RichTextSectionElement(elements=[_.Text(text="b")]), + ], + style="ordered", + indent=0, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="bb")])], + style="ordered", + indent=1, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="BBB")])], + style="ordered", + indent=2, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="c")])], + style="ordered", + indent=0, + offset=2, + border=0, + ), + RichTextSectionElement(elements=[_.Text(text="\n")]), + RichTextListElement( + elements=[ + RichTextSectionElement(elements=[_.Text(text="todo")]), + RichTextSectionElement(elements=[_.Text(text="todo")]), + RichTextSectionElement(elements=[_.Text(text="todo")]), + ], + style="bullet", + indent=0, + border=0, + ), + RichTextSectionElement(elements=[_.Text(text="\n")]), + RichTextQuoteElement(elements=[_.Text(text="this is very important")]), + RichTextPreformattedElement( + elements=[_.Text(text='print("Hello world")')], + border=0, + ), + RichTextSectionElement( + elements=[ + _.User(user_id="WJC6QG0MS"), + _.Text(text=" "), + _.UserGroup(usergroup_id="S01BL602YLU"), + _.Text(text=" "), + _.Channel(channel_id="C02GD0YEHDJ"), + ] + ), + ], + ) + self.assertDictEqual(dict_block, class_block.to_dict())