Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion elementary/messages/block_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
MentionBlock,
TextBlock,
TextStyle,
WhitespaceBlock,
)

SimpleInlineBlock = Union[str, Icon]
Expand All @@ -38,12 +39,23 @@ def BulletListBlock(
*,
icon: Union[Icon, str],
lines: List[LineBlock] = [],
indent: int = 0,
) -> LinesBlock:
whitespaces = [WhitespaceBlock()] * indent
icon_inline: InlineBlock = (
IconBlock(icon=icon) if isinstance(icon, Icon) else TextBlock(text=icon)
)
lines = [
LineBlock(inlines=[icon_inline, *line.inlines], sep=line.sep) for line in lines
LineBlock(
inlines=[
*whitespaces,
icon_inline,
TextBlock(text=" "),
line,
],
sep="",
)
for line in lines
]
return LinesBlock(lines=lines)

Expand Down
37 changes: 37 additions & 0 deletions elementary/messages/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Icon(Enum):
EYE = "eye"
GEAR = "gear"
BELL = "bell"
GEM = "gem"


class TextStyle(Enum):
Expand Down Expand Up @@ -67,12 +68,47 @@ class LineBlock(BaseBlock):
sep: str = " "


class WhitespaceBlock(BaseBlock):
type: Literal["whitespace"] = "whitespace"


class ActionBlock(BaseBlock):
type: Literal["action"] = "action"
action: str
action_id: str


class DropdownOptionBlock(BaseBlock):
type: Literal["dropdown_option"] = "dropdown_option"
text: str
value: str


class DropdownActionBlock(ActionBlock):
action: Literal["dropdown"] = "dropdown"
options: List[DropdownOptionBlock]
placeholder: Optional[str] = None
initial_option: Optional[DropdownOptionBlock] = None


class UserSelectActionBlock(ActionBlock):
action: Literal["users_select"] = "users_select"
placeholder: Optional[str] = None
initial_user: Optional[str] = None


class ActionsBlock(BaseBlock):
type: Literal["actions"] = "actions"
actions: Sequence[ActionBlock]


InlineBlock = Union[
TextBlock,
LinkBlock,
IconBlock,
InlineCodeBlock,
MentionBlock,
WhitespaceBlock,
"LineBlock",
]

Expand Down Expand Up @@ -142,6 +178,7 @@ class ExpandableBlock(BaseBlock):
LinesBlock,
FactListBlock,
TableBlock,
ActionsBlock,
"ExpandableBlock",
]

Expand Down
8 changes: 8 additions & 0 deletions elementary/messages/formats/adaptive_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Dict, List, Optional

from elementary.messages.blocks import (
ActionsBlock,
CodeBlock,
DividerBlock,
ExpandableBlock,
Expand All @@ -18,6 +19,7 @@
TableBlock,
TextBlock,
TextStyle,
WhitespaceBlock,
)
from elementary.messages.formats.html import ICON_TO_HTML
from elementary.messages.message_body import Color, MessageBlock, MessageBody
Expand Down Expand Up @@ -55,6 +57,8 @@ def format_inline_block(block: InlineBlock) -> str:
return block.user
elif isinstance(block, LineBlock):
return format_line_block_text(block)
elif isinstance(block, WhitespaceBlock):
return " "
else:
raise ValueError(f"Unsupported inline block type: {type(block)}")

Expand Down Expand Up @@ -167,6 +171,10 @@ def format_message_block(
return format_expandable_block(block)
elif isinstance(block, TableBlock):
return [format_table_block(block)]
elif isinstance(block, ActionsBlock):
# Not supported in webhooks, so we don't need to format it.
# When we add support for teams apps, we will need to format it.
return []
else:
raise ValueError(f"Unsupported message block type: {type(block)}")

Expand Down
76 changes: 76 additions & 0 deletions elementary/messages/formats/block_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
from tabulate import tabulate

from elementary.messages.blocks import (
ActionBlock,
ActionsBlock,
CodeBlock,
DividerBlock,
DropdownActionBlock,
ExpandableBlock,
FactBlock,
FactListBlock,
Expand All @@ -23,6 +26,8 @@
TableBlock,
TextBlock,
TextStyle,
UserSelectActionBlock,
WhitespaceBlock,
)
from elementary.messages.formats.html import ICON_TO_HTML
from elementary.messages.message_body import Color, MessageBlock, MessageBody
Expand Down Expand Up @@ -83,6 +88,8 @@ def _format_inline_block(self, block: InlineBlock) -> str:
return block.user
elif isinstance(block, LineBlock):
return self._format_line_block_text(block)
elif isinstance(block, WhitespaceBlock):
return " "
else:
raise ValueError(f"Unsupported inline block type: {type(block)}")

Expand Down Expand Up @@ -120,6 +127,60 @@ def _format_markdown_section(self, text: str) -> dict:
"text": self._format_markdown_section_text(text),
}

def _format_action_block(self, block: ActionBlock) -> dict:
if isinstance(block, DropdownActionBlock):
return self._format_dropdown_action_block(block)
elif isinstance(block, UserSelectActionBlock):
return self._format_user_select_action_block(block)
else:
raise ValueError(f"Unsupported action block type: {type(block)}")

def _format_dropdown_action_block(self, block: DropdownActionBlock) -> dict:
formatted_block = {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": block.placeholder,
"emoji": True,
},
"options": [
{
"text": {
"type": "plain_text",
"text": option.text,
"emoji": True,
},
"value": option.value,
}
for option in block.options
],
}
if block.initial_option:
formatted_block["initial_option"] = {
"text": {
"type": "plain_text",
"text": block.initial_option.text,
"emoji": True,
},
"value": block.initial_option.value,
}
return formatted_block

def _format_user_select_action_block(self, block: UserSelectActionBlock) -> dict:
formatted_block = {
"type": "users_select",
"placeholder": {
"type": "plain_text",
"text": block.placeholder,
"emoji": True,
},
}
if block.initial_user:
resolved_user = self._resolve_mention(block.initial_user)
if resolved_user:
formatted_block["initial_user"] = resolved_user
return formatted_block

def _add_block(self, block: dict) -> None:
if not self._is_divided:
self._blocks.append(block)
Expand Down Expand Up @@ -211,6 +272,16 @@ def _add_table_block(self, block: TableBlock) -> None:
table_text = tabulate(new_rows, headers=new_headers, tablefmt="simple")
self._add_block(self._format_markdown_section(f"```{table_text}```"))

def _add_actions_block(self, block: ActionsBlock) -> None:
self._add_block(
{
"type": "actions",
"elements": [
self._format_action_block(action) for action in block.actions
],
}
)

def _add_expandable_block(self, block: ExpandableBlock) -> None:
"""
Expandable blocks are not supported in Slack Block Kit.
Expand All @@ -233,6 +304,8 @@ def _add_message_block(self, block: MessageBlock) -> None:
self._add_expandable_block(block)
elif isinstance(block, TableBlock):
self._add_table_block(block)
elif isinstance(block, ActionsBlock):
self._add_actions_block(block)
else:
raise ValueError(f"Unsupported message block type: {type(block)}")

Expand Down Expand Up @@ -263,6 +336,9 @@ def build(self, message: MessageBody) -> FormattedBlockKitMessage:
self._add_message_blocks(message.blocks)
color_code = COLOR_MAP.get(message.color) if message.color else None
blocks, attachment_blocks = self._get_final_blocks(message.color)
if message.id and blocks:
# The only place in a slack message where we can set a custom id is in blocks, so we set the id of the first block
blocks[0]["block_id"] = message.id
built_message = FormattedBlockKitMessage(
blocks=blocks,
attachments=[
Expand Down
1 change: 1 addition & 0 deletions elementary/messages/formats/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Icon.EYE: "👁️",
Icon.GEAR: "⚙️",
Icon.BELL: "🔔",
Icon.GEM: "💎",
}

for icon in Icon:
Expand Down
5 changes: 4 additions & 1 deletion elementary/messages/message_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic import BaseModel

from elementary.messages.blocks import (
ActionsBlock,
CodeBlock,
DividerBlock,
ExpandableBlock,
Expand All @@ -26,14 +27,16 @@ class Color(Enum):
DividerBlock,
LinesBlock,
FactListBlock,
ExpandableBlock,
TableBlock,
ActionsBlock,
ExpandableBlock,
]


class MessageBody(BaseModel):
blocks: List[MessageBlock]
color: Optional[Color] = None
id: Optional[str] = None


MessageBody.update_forward_refs()
1 change: 1 addition & 0 deletions elementary/messages/messaging_integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ If your platform's message format is not yet supported:
- ExpandableBlock: Collapsible sections
- MentionBlock: Mention a user
- TableBlock: Table of data
- WhitespaceBlock: Whitespace for indentation
```
3. Add tests in `tests/unit/messages/formats/`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def send_message(
def supports_reply(self) -> bool:
raise NotImplementedError

def supports_actions(self) -> bool:
return False

def reply_to_message(
self,
destination: DestinationType,
Expand Down
3 changes: 3 additions & 0 deletions elementary/messages/messaging_integrations/slack_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def from_token(
def supports_reply(self) -> bool:
return True

def supports_actions(self) -> bool:
return True

def send_message(
self, destination: Channel, body: MessageBody
) -> MessageSendResult[SlackWebMessageContext]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"items": [
{
"type": "TextBlock",
"text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14",
"text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14 GEM \ud83d\udc8e",
"wrap": true
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "This should not be indented",
"wrap": true
},
{
"type": "TextBlock",
"text": "  This should be indented",
"wrap": true
}
]
}
],
"version": "1.5"
}
23 changes: 23 additions & 0 deletions tests/unit/messages/formats/base_test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TableBlock,
TextBlock,
TextStyle,
WhitespaceBlock,
)
from elementary.messages.message_body import Color, MessageBody

Expand Down Expand Up @@ -378,3 +379,25 @@ def test_format_table_block(self, text_length: int, column_count: int):
)
result = self.format(message_body)
self.assert_expected_value(result, expected_file_path)

def test_format_message_body_whitespace_block(self):
message_body = MessageBody(
blocks=[
LinesBlock(
lines=[
LineBlock(
inlines=[TextBlock(text="This should not be indented")]
),
LineBlock(
inlines=[
WhitespaceBlock(),
TextBlock(text="This should be indented"),
]
),
]
)
]
)
expected_file_path = self.get_expected_file_path("whitespace_block")
result = self.format(message_body)
self.assert_expected_value(result, expected_file_path)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "section",
"text": {
"type": "mrkdwn",
"text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14"
"text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14 GEM \ud83d\udc8e"
}
}
],
Expand Down
Loading
Loading