Skip to content

Commit c96bcf2

Browse files
authored
Merge pull request #1822 from elementary-data/ele-4067-slack-messaging-integration
Ele 4067 slack messaging integration
2 parents e48a6d3 + 46eef40 commit c96bcf2

File tree

191 files changed

+7488
-256
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

191 files changed

+7488
-256
lines changed

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pytest
22
pytest-parametrization>=2022.2.1
33
pre-commit
44
mypy
5-
5+
deepdiff
66
# MyPy stubs
77
types-requests
88
networkx-stubs

elementary/messages/block_builders.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
LineBlock,
1212
LinesBlock,
1313
LinkBlock,
14+
MentionBlock,
1415
TextBlock,
1516
TextStyle,
1617
)
@@ -41,7 +42,9 @@ def BulletListBlock(
4142
icon_inline: InlineBlock = (
4243
IconBlock(icon=icon) if isinstance(icon, Icon) else TextBlock(text=icon)
4344
)
44-
lines = [LineBlock(inlines=[icon_inline] + line.inlines) for line in lines]
45+
lines = [
46+
LineBlock(inlines=[icon_inline, *line.inlines], sep=line.sep) for line in lines
47+
]
4548
return LinesBlock(lines=lines)
4649

4750

@@ -138,3 +141,7 @@ def TitledParagraphBlock(
138141

139142
def JsonCodeBlock(*, content: Union[str, dict, list], indent: int = 2) -> CodeBlock:
140143
return CodeBlock(text=json.dumps(content, indent=indent))
144+
145+
146+
def MentionLineBlock(*users: str) -> LineBlock:
147+
return LineBlock(inlines=[MentionBlock(user=user) for user in users], sep=", ")

elementary/messages/blocks.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from enum import Enum
2-
from typing import Any, Dict, List, Optional, Union
2+
from typing import Any, Dict, List, Optional, Sequence, Union
33

44
from pydantic import BaseModel
55
from typing_extensions import Literal
@@ -51,7 +51,32 @@ class IconBlock(BaseInlineTextBlock):
5151
icon: Icon
5252

5353

54-
InlineBlock = Union[TextBlock, LinkBlock, IconBlock]
54+
class InlineCodeBlock(BaseInlineTextBlock):
55+
type: Literal["inline_code"] = "inline_code"
56+
code: str
57+
58+
59+
class MentionBlock(BaseInlineTextBlock):
60+
type: Literal["mention"] = "mention"
61+
user: str
62+
63+
64+
class LineBlock(BaseBlock):
65+
type: Literal["line"] = "line"
66+
inlines: Sequence["InlineBlock"]
67+
sep: str = " "
68+
69+
70+
InlineBlock = Union[
71+
TextBlock,
72+
LinkBlock,
73+
IconBlock,
74+
InlineCodeBlock,
75+
MentionBlock,
76+
"LineBlock",
77+
]
78+
79+
LineBlock.update_forward_refs()
5580

5681

5782
class HeaderBlock(BaseBlock):
@@ -68,12 +93,6 @@ class DividerBlock(BaseBlock):
6893
type: Literal["divider"] = "divider"
6994

7095

71-
class LineBlock(BaseBlock):
72-
type: Literal["line"] = "line"
73-
inlines: List[InlineBlock]
74-
sep: str = " "
75-
76-
7796
class BaseLinesBlock(BaseBlock):
7897
lines: List[LineBlock]
7998

elementary/messages/formats/adaptive_cards.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
Icon,
1111
IconBlock,
1212
InlineBlock,
13+
InlineCodeBlock,
1314
LineBlock,
1415
LinesBlock,
1516
LinkBlock,
17+
MentionBlock,
1618
TableBlock,
1719
TextBlock,
1820
TextStyle,
@@ -47,6 +49,12 @@ def format_inline_block(block: InlineBlock) -> str:
4749
return format_text_block(block)
4850
elif isinstance(block, LinkBlock):
4951
return f"[{block.text}]({block.url})"
52+
elif isinstance(block, InlineCodeBlock):
53+
return block.code
54+
elif isinstance(block, MentionBlock):
55+
return block.user
56+
elif isinstance(block, LineBlock):
57+
return format_line_block_text(block)
5058
else:
5159
raise ValueError(f"Unsupported inline block type: {type(block)}")
5260

elementary/messages/formats/block_kit.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
2-
from typing import Any, Dict, List, Optional, Tuple
2+
from typing import Any, Callable, List, Optional, Tuple
33

4+
from pydantic import BaseModel
45
from slack_sdk.models import blocks as slack_blocks
56
from tabulate import tabulate
67

@@ -14,9 +15,11 @@
1415
Icon,
1516
IconBlock,
1617
InlineBlock,
18+
InlineCodeBlock,
1719
LineBlock,
1820
LinesBlock,
1921
LinkBlock,
22+
MentionBlock,
2023
TableBlock,
2124
TextBlock,
2225
TextStyle,
@@ -31,15 +34,26 @@
3134
}
3235

3336

37+
class FormattedBlockKitMessage(BaseModel):
38+
blocks: List[dict]
39+
attachments: List[dict]
40+
41+
42+
ResolveMentionCallback = Callable[[str], Optional[str]]
43+
44+
3445
class BlockKitBuilder:
3546
_SECONDARY_FACT_CHUNK_SIZE = 2
3647
_LONGEST_MARKDOWN_SUFFIX_LEN = 3 # length of markdown's code suffix (```)
3748
_MAX_CELL_LENGTH_BY_COLUMN_COUNT = {4: 11, 3: 14, 2: 22, 1: 40, 0: 40}
3849

39-
def __init__(self) -> None:
50+
def __init__(
51+
self, resolve_mention: Optional[ResolveMentionCallback] = None
52+
) -> None:
4053
self._blocks: List[dict] = []
4154
self._attachment_blocks: List[dict] = []
4255
self._is_divided = False
56+
self._resolve_mention = resolve_mention or (lambda x: None)
4357

4458
def _format_icon(self, icon: Icon) -> str:
4559
return ICON_TO_HTML[icon]
@@ -59,6 +73,16 @@ def _format_inline_block(self, block: InlineBlock) -> str:
5973
return self._format_text_block(block)
6074
elif isinstance(block, LinkBlock):
6175
return f"<{block.url}|{block.text}>"
76+
elif isinstance(block, InlineCodeBlock):
77+
return f"`{block.code}`"
78+
elif isinstance(block, MentionBlock):
79+
resolved_user = self._resolve_mention(block.user)
80+
if resolved_user:
81+
return f"<@{resolved_user}>"
82+
else:
83+
return block.user
84+
elif isinstance(block, LineBlock):
85+
return self._format_line_block_text(block)
6286
else:
6387
raise ValueError(f"Unsupported inline block type: {type(block)}")
6488

@@ -192,12 +216,6 @@ def _add_expandable_block(self, block: ExpandableBlock) -> None:
192216
Expandable blocks are not supported in Slack Block Kit.
193217
However, slack automatically collapses a large section block into an expandable block.
194218
"""
195-
self._add_block(
196-
{
197-
"type": "section",
198-
"text": self._format_markdown_section_text(f"*{block.title}*"),
199-
}
200-
)
201219
self._add_message_blocks(block.body)
202220

203221
def _add_message_block(self, block: MessageBlock) -> None:
@@ -239,25 +257,28 @@ def _get_final_blocks(
239257
else:
240258
return [], self._blocks
241259

242-
def build(self, message: MessageBody) -> Dict[str, Any]:
260+
def build(self, message: MessageBody) -> FormattedBlockKitMessage:
243261
self._blocks = []
244262
self._attachment_blocks = []
245263
self._add_message_blocks(message.blocks)
246264
color_code = COLOR_MAP.get(message.color) if message.color else None
247265
blocks, attachment_blocks = self._get_final_blocks(message.color)
248-
built_message = {
249-
"blocks": blocks,
250-
"attachments": [
266+
built_message = FormattedBlockKitMessage(
267+
blocks=blocks,
268+
attachments=[
251269
{
252270
"blocks": attachment_blocks,
253271
}
254272
],
255-
}
273+
)
256274
if color_code:
257-
built_message["attachments"][0]["color"] = color_code
275+
for attachment in built_message.attachments:
276+
attachment["color"] = color_code
258277
return built_message
259278

260279

261-
def format_block_kit(message: MessageBody) -> Dict[str, Any]:
262-
builder = BlockKitBuilder()
280+
def format_block_kit(
281+
message: MessageBody, resolve_mention: Optional[ResolveMentionCallback] = None
282+
) -> FormattedBlockKitMessage:
283+
builder = BlockKitBuilder(resolve_mention)
263284
return builder.build(message)

elementary/messages/messaging_integrations/README.md

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ If your platform's message format is not yet supported:
4747
- LinesBlock: Plain text content
4848
- FactListBlock: Key-value pairs
4949
- ExpandableBlock: Collapsible sections
50+
- MentionBlock: Mention a user
51+
- TableBlock: Table of data
5052
```
5153
3. Add tests in `tests/unit/messages/formats/`
5254

@@ -92,19 +94,6 @@ Once the message format is ready:
9294
- Make sure to get all required information from users to create your destination type
9395
- See Teams implementation for reference (webhook URL configuration)
9496

95-
## Migration Strategy
96-
97-
The system currently supports both:
98-
99-
- Legacy `BaseIntegration` implementations (e.g., Slack)
100-
- New `BaseMessagingIntegration` implementations (e.g., Teams)
101-
102-
This dual support allows for a gradual migration path where:
103-
104-
1. New integrations are implemented using `BaseMessagingIntegration`
105-
2. Existing integrations can be migrated one at a time
106-
3. The legacy `BaseIntegration` will eventually be deprecated
107-
10897
## Implementing a New Integration
10998

11099
To add a new messaging platform integration:
@@ -120,10 +109,9 @@ To add a new messaging platform integration:
120109

121110
## Current Implementations
122111

123-
- **Teams**: Uses the new `BaseMessagingIntegration` system with webhook support and Adaptive Cards format
124-
- **Slack**: Currently uses the legacy `BaseIntegration` system (planned for migration)
112+
- **Teams**: Webhook support, Adaptive Cards format
113+
- **Slack**: Webhook and token support, Block Kit format
125114

126115
## Future Improvements
127116

128-
1. Complete migration of Slack to `BaseMessagingIntegration`
129-
2. Add support for more messaging platforms
117+
1. Add support for more messaging platforms

elementary/messages/messaging_integrations/base_messaging_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ def reply_to_message(
4545
body: MessageBody,
4646
) -> MessageSendResult[MessageContextType]:
4747
if not self.supports_reply():
48-
raise MessageIntegrationReplyNotSupportedError
48+
raise MessageIntegrationReplyNotSupportedError(type(self).__name__)
4949
raise NotImplementedError

elementary/messages/messaging_integrations/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ class MessagingIntegrationError(Exception):
33

44

55
class MessageIntegrationReplyNotSupportedError(MessagingIntegrationError):
6-
pass
6+
def __init__(self, integration_name: str):
7+
self.integration_name = integration_name
8+
super().__init__(f"{integration_name} does not support replying to messages")

0 commit comments

Comments
 (0)