File size: 5,635 Bytes
6155b26 754345f 6155b26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import json
import re
import httpx
from agent.messaging.base import (
NotificationError,
NotificationProvider,
RetryableNotificationError,
)
from agent.messaging.models import (
NotificationRequest,
NotificationResult,
SlackDestinationConfig,
)
_SEVERITY_PREFIX = {
"info": "[INFO]",
"success": "[SUCCESS]",
"warning": "[WARNING]",
"error": "[ERROR]",
}
def _format_slack_mrkdwn(content: str) -> str:
"""Convert common Markdown constructs to Slack's mrkdwn syntax."""
if not content:
return content
placeholders: dict[str, str] = {}
placeholder_index = 0
def placeholder(value: str) -> str:
nonlocal placeholder_index
key = f"\x00SLACK{placeholder_index}\x00"
placeholder_index += 1
placeholders[key] = value
return key
text = content
# Protect code before any formatting conversion. Slack's mrkdwn ignores
# formatting inside backticks, so these regions should stay byte-for-byte.
text = re.sub(
r"(```(?:[^\n]*\n)?[\s\S]*?```)",
lambda match: placeholder(match.group(0)),
text,
)
text = re.sub(r"(`[^`\n]+`)", lambda match: placeholder(match.group(0)), text)
def convert_markdown_link(match: re.Match[str]) -> str:
label = match.group(1)
url = match.group(2).strip()
if url.startswith("<") and url.endswith(">"):
url = url[1:-1].strip()
return placeholder(f"<{url}|{label}>")
text = re.sub(
r"\[([^\]]+)\]\(([^()]*(?:\([^()]*\)[^()]*)*)\)",
convert_markdown_link,
text,
)
# Preserve existing Slack entities and manual mrkdwn links before escaping.
text = re.sub(
r"(<(?:[@#!]|(?:https?|mailto|tel):)[^>\n]+>)",
lambda match: placeholder(match.group(1)),
text,
)
text = re.sub(
r"^(>+\s)",
lambda match: placeholder(match.group(0)),
text,
flags=re.MULTILINE,
)
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
def convert_header(match: re.Match[str]) -> str:
header = match.group(1).strip()
header = re.sub(r"\*\*(.+?)\*\*", r"\1", header)
return placeholder(f"*{header}*")
text = re.sub(r"^#{1,6}\s+(.+)$", convert_header, text, flags=re.MULTILINE)
text = re.sub(
r"\*\*\*(.+?)\*\*\*",
lambda match: placeholder(f"*_{match.group(1)}_*"),
text,
)
text = re.sub(
r"\*\*(.+?)\*\*",
lambda match: placeholder(f"*{match.group(1)}*"),
text,
)
text = re.sub(
r"(?<!\*)\*([^*\n]+)\*(?!\*)",
lambda match: placeholder(f"_{match.group(1)}_"),
text,
)
text = re.sub(
r"~~(.+?)~~",
lambda match: placeholder(f"~{match.group(1)}~"),
text,
)
for key in reversed(placeholders):
text = text.replace(key, placeholders[key])
return text
def _format_text(request: NotificationRequest) -> str:
lines: list[str] = []
prefix = _SEVERITY_PREFIX[request.severity]
if request.title:
lines.append(f"{prefix} {request.title}")
else:
lines.append(prefix)
lines.append(request.message)
for key, value in request.metadata.items():
lines.append(f"{key}: {value}")
return _format_slack_mrkdwn("\n".join(lines))
class SlackProvider(NotificationProvider):
provider_name = "slack"
async def send(
self,
client: httpx.AsyncClient,
destination_name: str,
destination: SlackDestinationConfig,
request: NotificationRequest,
) -> NotificationResult:
payload = {
"channel": destination.channel,
"text": _format_text(request),
"mrkdwn": True,
"unfurl_links": False,
"unfurl_media": False,
}
if destination.username:
payload["username"] = destination.username
if destination.icon_emoji:
payload["icon_emoji"] = destination.icon_emoji
try:
response = await client.post(
"https://slack.com/api/chat.postMessage",
headers={
"Authorization": f"Bearer {destination.token}",
"Content-Type": "application/json; charset=utf-8",
},
content=json.dumps(payload),
)
except httpx.TimeoutException as exc:
raise RetryableNotificationError("Slack request timed out") from exc
except httpx.TransportError as exc:
raise RetryableNotificationError("Slack transport error") from exc
if response.status_code == 429 or response.status_code >= 500:
raise RetryableNotificationError(f"Slack HTTP {response.status_code}")
if response.status_code >= 400:
raise NotificationError(f"Slack HTTP {response.status_code}")
try:
data = response.json()
except ValueError as exc:
raise RetryableNotificationError("Slack returned invalid JSON") from exc
if not data.get("ok"):
error = str(data.get("error") or "unknown_error")
if error == "ratelimited":
raise RetryableNotificationError(error)
raise NotificationError(error)
return NotificationResult(
destination=destination_name,
ok=True,
provider=self.provider_name,
external_id=str(data.get("ts") or ""),
error=None,
)
|