Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
20 changes: 19 additions & 1 deletion src/sentry/integrations/msteams/card_builder/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ class ColumnBlock(_ColumnBlockNotRequired):
width: ColumnWidth | str


class CodeBlock(TypedDict):
type: Literal["CodeBlock"]
codeSnippet: str


class ColumnSetBlock(TypedDict):
type: Literal["ColumnSet"]
columns: list[ColumnBlock]
Expand Down Expand Up @@ -147,7 +152,13 @@ class InputChoiceSetBlock(_InputChoiceSetBlockNotRequired):

ItemBlock: TypeAlias = str | TextBlock | ImageBlock
Block: TypeAlias = (
ActionSet | TextBlock | ImageBlock | ColumnSetBlock | ContainerBlock | InputChoiceSetBlock
ActionSet
| TextBlock
| ImageBlock
| ColumnSetBlock
| ContainerBlock
| InputChoiceSetBlock
| CodeBlock
)


Expand All @@ -173,6 +184,13 @@ def create_text_block(text: str | None, **kwargs: Unpack[_TextBlockNotRequired])
}


def create_code_block(text: str) -> CodeBlock:
return {
"type": "CodeBlock",
"codeSnippet": escape_markdown_special_chars(text),
}


def create_logo_block(**kwargs: Unpack[_ImageBlockNotRequired]) -> ImageBlock:
# Default size if no size is given
if not kwargs.get("height"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ def get(self, request: Request) -> Response:
def serialize_rendered_example(rendered_template: NotificationRenderedTemplate) -> dict[str, Any]:
response: dict[str, Any] = {
"subject": rendered_template.subject,
"body": rendered_template.body,
"body": [
{
"type": block.type,
"blocks": [
{"type": text_block.type, "text": text_block.text}
for text_block in block.blocks
],
}
for block in rendered_template.body
],
"actions": [
{"label": action.label, "link": action.link} for action in rendered_template.actions
],
Expand Down
49 changes: 48 additions & 1 deletion src/sentry/notifications/platform/discord/provider.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from sentry.notifications.platform.provider import NotificationProvider, NotificationProviderError
Expand All @@ -8,6 +10,10 @@
PreparedIntegrationNotificationTarget,
)
from sentry.notifications.platform.types import (
NotificationBodyFormattingBlock,
NotificationBodyFormattingBlockType,
NotificationBodyTextBlock,
NotificationBodyTextBlockType,
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
Expand All @@ -18,6 +24,9 @@

if TYPE_CHECKING:
from sentry.integrations.discord.message_builder.base.base import DiscordMessage
from sentry.integrations.discord.message_builder.base.embed.field import (
DiscordMessageEmbedField,
)

# TODO(ecosystem): Proper typing - https://discord.com/developers/docs/resources/message#create-message
type DiscordRenderable = DiscordMessage
Expand Down Expand Up @@ -51,10 +60,12 @@ def render[DataT: NotificationData](
components: list[DiscordMessageComponent] = []
embeds = []

body_blocks = cls.render_body_blocks(rendered_template.body)

embeds.append(
DiscordMessageEmbed(
title=rendered_template.subject,
description=rendered_template.body,
fields=body_blocks,
image=(
DiscordMessageEmbedImage(url=rendered_template.chart.url)
if rendered_template.chart
Expand Down Expand Up @@ -82,6 +93,42 @@ def render[DataT: NotificationData](

return builder.build()

@classmethod
def render_body_blocks(
cls, body: list[NotificationBodyFormattingBlock]
) -> list[DiscordMessageEmbedField]:
from sentry.integrations.discord.message_builder.base.embed.field import (
DiscordMessageEmbedField,
)

fields = []
for block in body:
if block.type == NotificationBodyFormattingBlockType.SECTION:
fields.append(
DiscordMessageEmbedField(
name=block.type.value, value=cls.render_text_blocks(block.blocks)
)
)
elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK:
fields.append(
DiscordMessageEmbedField(
name=block.type.value, value=f"```{cls.render_text_blocks(block.blocks)}```"
)
)
return fields

@classmethod
def render_text_blocks(cls, blocks: list[NotificationBodyTextBlock]) -> str:
texts = []
for block in blocks:
if block.type == NotificationBodyTextBlockType.PLAIN_TEXT:
texts.append(block.text)
elif block.type == NotificationBodyTextBlockType.BOLD_TEXT:
texts.append(f"**{block.text}**")
elif block.type == NotificationBodyTextBlockType.CODE:
texts.append(f"`{block.text}`")
return " ".join(texts)


@provider_registry.register(NotificationProviderKey.DISCORD)
class DiscordNotificationProvider(NotificationProvider[DiscordRenderable]):
Expand Down
63 changes: 60 additions & 3 deletions src/sentry/notifications/platform/email/provider.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from django.core.mail import EmailMultiAlternatives
from django.core.mail.message import make_msgid
from django.utils.safestring import mark_safe

from sentry import options
from sentry.notifications.platform.provider import NotificationProvider
from sentry.notifications.platform.registry import provider_registry
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.target import GenericNotificationTarget
from sentry.notifications.platform.types import (
NotificationBodyFormattingBlock,
NotificationBodyFormattingBlockType,
NotificationBodyTextBlock,
NotificationBodyTextBlockType,
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
Expand All @@ -32,24 +37,29 @@ class EmailRenderer(NotificationRenderer[EmailRenderable]):
def render[DataT: NotificationData](
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
) -> EmailRenderable:
html_body_blocks = cls.render_body_blocks_to_html_string(rendered_template.body)
txt_body_blocks = cls.render_body_blocks_to_txt_string(rendered_template.body)

email_context = {
"subject": rendered_template.subject,
"body": rendered_template.body,
"actions": [(action.label, action.link) for action in rendered_template.actions],
"chart_url": rendered_template.chart.url if rendered_template.chart else None,
"chart_alt_text": rendered_template.chart.alt_text if rendered_template.chart else None,
"footer": rendered_template.footer,
}

html_email_context = {**email_context, "body": html_body_blocks}
txt_email_context = {**email_context, "body": txt_body_blocks}

html_body = inline_css(
render_to_string(
template=(rendered_template.email_html_path or DEFAULT_EMAIL_HTML_PATH),
context=email_context,
context=html_email_context,
)
)
txt_body = render_to_string(
template=(rendered_template.email_text_path or DEFAULT_EMAIL_TEXT_PATH),
context=email_context,
context=txt_email_context,
)
# Required by RFC 2822 (https://www.rfc-editor.org/rfc/rfc2822.html)
headers = {"Message-Id": make_msgid(domain=get_from_email_domain())}
Expand All @@ -68,6 +78,53 @@ def render[DataT: NotificationData](
email.attach_alternative(html_body, "text/html")
return email

@classmethod
def render_body_blocks_to_html_string(cls, body: list[NotificationBodyFormattingBlock]) -> str:
body_blocks = []
for block in body:
if block.type == NotificationBodyFormattingBlockType.SECTION:
body_blocks.append(f"<br>{cls.render_text_blocks_to_html_string(block.blocks)}")
elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK:
body_blocks.append(
f"<br><code>{cls.render_text_blocks_to_html_string(block.blocks)}</code>"
)
# Mark as safe so Django doesn't escape the HTML tags
return mark_safe(" ".join(body_blocks))

@classmethod
def render_text_blocks_to_html_string(cls, blocks: list[NotificationBodyTextBlock]) -> str:
texts = []
for block in blocks:
if block.type == NotificationBodyTextBlockType.PLAIN_TEXT:
texts.append(block.text)
elif block.type == NotificationBodyTextBlockType.BOLD_TEXT:
texts.append(f"<strong>{block.text}</strong>")
elif block.type == NotificationBodyTextBlockType.CODE:
texts.append(f"<code>{block.text}</code>")
return mark_safe(" ".join(texts))

@classmethod
def render_body_blocks_to_txt_string(cls, blocks: list[NotificationBodyFormattingBlock]) -> str:
body_blocks = []
for block in blocks:
if block.type == NotificationBodyFormattingBlockType.SECTION:
body_blocks.append(f"\n{cls.render_text_blocks_to_txt_string(block.blocks)}")
elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK:
body_blocks.append(f"\n```{cls.render_text_blocks_to_txt_string(block.blocks)}```")
return mark_safe(" ".join(body_blocks))

@classmethod
def render_text_blocks_to_txt_string(cls, blocks: list[NotificationBodyTextBlock]) -> str:
texts = []
for block in blocks:
if block.type == NotificationBodyTextBlockType.PLAIN_TEXT:
texts.append(block.text)
elif block.type == NotificationBodyTextBlockType.BOLD_TEXT:
texts.append(f"**{block.text}**")
elif block.type == NotificationBodyTextBlockType.CODE:
texts.append(f"`{block.text}`")
return mark_safe(" ".join(texts))


@provider_registry.register(NotificationProviderKey.EMAIL)
class EmailNotificationProvider(NotificationProvider[EmailRenderable]):
Expand Down
38 changes: 34 additions & 4 deletions src/sentry/notifications/platform/msteams/provider.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from sentry.integrations.msteams.card_builder.block import create_code_block
from sentry.notifications.platform.provider import NotificationProvider, NotificationProviderError
from sentry.notifications.platform.registry import provider_registry
from sentry.notifications.platform.renderer import NotificationRenderer
Expand All @@ -8,6 +11,10 @@
PreparedIntegrationNotificationTarget,
)
from sentry.notifications.platform.types import (
NotificationBodyFormattingBlock,
NotificationBodyFormattingBlockType,
NotificationBodyTextBlock,
NotificationBodyTextBlockType,
NotificationData,
NotificationProviderKey,
NotificationRenderedTemplate,
Expand All @@ -17,7 +24,7 @@
from sentry.organizations.services.organization.model import RpcOrganizationSummary

if TYPE_CHECKING:
from sentry.integrations.msteams.card_builder.block import AdaptiveCard
from sentry.integrations.msteams.card_builder.block import AdaptiveCard, Block

type MSTeamsRenderable = AdaptiveCard

Expand Down Expand Up @@ -47,9 +54,8 @@ def render[DataT: NotificationData](
title_text = create_text_block(
text=rendered_template.subject, size=TextSize.LARGE, weight=TextWeight.BOLDER
)
body_text = create_text_block(text=rendered_template.body)

body_blocks: list[Block] = [title_text, body_text]
body_text = cls.render_body_blocks(rendered_template.body)
body_blocks: list[Block] = [title_text, *body_text]

if len(rendered_template.actions) > 0:
actions: list[Action] = []
Expand Down Expand Up @@ -81,6 +87,30 @@ def render[DataT: NotificationData](
}
return card

@classmethod
def render_body_blocks(cls, body: list[NotificationBodyFormattingBlock]) -> list[Block]:
from sentry.integrations.msteams.card_builder.block import create_text_block

body_blocks: list[Block] = []
for block in body:
if block.type == NotificationBodyFormattingBlockType.SECTION:
body_blocks.append(create_text_block(text=cls.render_text_blocks(block.blocks)))
elif block.type == NotificationBodyFormattingBlockType.CODE_BLOCK:
body_blocks.append(create_code_block(text=cls.render_text_blocks(block.blocks)))
return body_blocks

@classmethod
def render_text_blocks(cls, blocks: list[NotificationBodyTextBlock]) -> str:
texts = []
for block in blocks:
if block.type == NotificationBodyTextBlockType.PLAIN_TEXT:
texts.append(block.text)
elif block.type == NotificationBodyTextBlockType.BOLD_TEXT:
texts.append(f"**{block.text}**")
elif block.type == NotificationBodyTextBlockType.CODE:
texts.append(f"`{block.text}`")
return " ".join(texts)


@provider_registry.register(NotificationProviderKey.MSTEAMS)
class MSTeamsNotificationProvider(NotificationProvider[MSTeamsRenderable]):
Expand Down
Loading
Loading