Skip to content
Merged
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
62 changes: 59 additions & 3 deletions elementary/messages/messaging_integrations/teams_webhook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any, Optional
from typing import Any, Dict, List, Optional

import requests
from ratelimit import limits, sleep_and_retry
Expand All @@ -25,6 +26,7 @@

Channel: TypeAlias = Optional[str]
ONE_SECOND = 1
TEAMS_PAYLOAD_SIZE_LIMIT = 27 * 1024


class TeamsWebhookHttpError(MessagingIntegrationError):
Expand All @@ -36,8 +38,8 @@ def __init__(self, response: requests.Response):
)


def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
payload = {
def _build_payload(card: dict) -> dict:
return {
"type": "message",
"attachments": [
{
Expand All @@ -48,6 +50,60 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
],
}


def _truncation_notice_item() -> Dict[str, Any]:
return {
"type": "TextBlock",
"text": "_... Content truncated due to message size limits._",
"wrap": True,
"isSubtle": True,
}


def _minimal_card(card: dict) -> dict:
return {
**card,
"body": [
{
"type": "TextBlock",
"text": "Alert content too large to display in Teams.",
"wrap": True,
"weight": "bolder",
}
],
}


def _truncate_card(card: dict) -> dict:
body: List[Dict[str, Any]] = list(card.get("body", []))
if not body:
return card

while len(body) > 1:
payload = _build_payload({**card, "body": body + [_truncation_notice_item()]})
if len(json.dumps(payload)) <= TEAMS_PAYLOAD_SIZE_LIMIT:
break
body.pop()

truncated = {**card, "body": body + [_truncation_notice_item()]}
if len(json.dumps(_build_payload(truncated))) > TEAMS_PAYLOAD_SIZE_LIMIT:
return _minimal_card(card)
return truncated
Comment on lines +63 to +91
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Minimal fallback can still exceed limit if non-body fields are large.

_minimal_card() preserves **card, so large top-level fields (for example actions) can keep the payload oversized. In that case, send_adaptive_card() can still post a >27KiB payload and hit 413 again.

Suggested hard-limit fallback
 def _minimal_card(card: dict) -> dict:
-    return {
-        **card,
-        "body": [
-            {
-                "type": "TextBlock",
-                "text": "Alert content too large to display in Teams.",
-                "wrap": True,
-                "weight": "bolder",
-            }
-        ],
-    }
+    # Keep only required/adaptive-card identity fields to guarantee small payload.
+    minimal = {
+        "type": card.get("type", "AdaptiveCard"),
+        "version": card.get("version", "1.4"),
+        "body": [
+            {
+                "type": "TextBlock",
+                "text": "Alert content too large to display in Teams.",
+                "wrap": True,
+                "weight": "bolder",
+            }
+        ],
+    }
+    if "$schema" in card:
+        minimal["$schema"] = card["$schema"]
+    return minimal
 def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
@@
     if len(payload_json) > TEAMS_PAYLOAD_SIZE_LIMIT:
@@
         card = _truncate_card(card)
         payload = _build_payload(card)
+        if len(json.dumps(payload)) > TEAMS_PAYLOAD_SIZE_LIMIT:
+            raise MessagingIntegrationError(
+                "Teams webhook payload still exceeds size limit after truncation."
+            )

Also applies to: 94-106

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 83-83: Consider [*body, _truncation_notice_item()] instead of concatenation

Replace with [*body, _truncation_notice_item()]

(RUF005)


[warning] 88-88: Consider [*body, _truncation_notice_item()] instead of concatenation

Replace with [*body, _truncation_notice_item()]

(RUF005)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/messaging_integrations/teams_webhook.py` around lines 63
- 91, The current _minimal_card preserves all top-level fields from card so
large non-body fields (e.g., actions) can still make the payload exceed
TEAMS_PAYLOAD_SIZE_LIMIT; change _minimal_card to construct a strict minimal
payload (only keys required by _build_payload / Teams adaptive card: type, body
with the truncation TextBlock, and any mandatory schema/version keys) instead of
spreading **card, and update _truncate_card (and the similar fallback at lines
94–106) to validate the final payload size by calling _build_payload and, if
still too large, return this strict minimal payload; reference _minimal_card,
_truncate_card, _build_payload, _truncation_notice_item and
TEAMS_PAYLOAD_SIZE_LIMIT when making the change.



def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
payload = _build_payload(card)
payload_json = json.dumps(payload)
if len(payload_json) > TEAMS_PAYLOAD_SIZE_LIMIT:
logger.warning(
"Teams webhook payload size (%d bytes) exceeds limit (%d bytes), "
"truncating card body",
len(payload_json),
TEAMS_PAYLOAD_SIZE_LIMIT,
)
card = _truncate_card(card)
payload = _build_payload(card)

response = requests.post(
webhook_url,
json=payload,
Expand Down
Loading