chorus/bot/chorus_bot/ping.py
pyr0ball 75ee9f8117 fix: prevent Discord embed field 400s from empty/oversized values
C1: build_ping_embed set sender_id field value="" whenever sender_id was
None, which manual_share.py always passes -- Discord rejects empty embed
field values (min length 1), so every manual-share ping failed to send.
Fixed by using a zero-width space sentinel that round-trips back to ""
(raw_content) or None (sender_id) in parse_ping_embed. Added regression
tests asserting no field value is ever empty after build_ping_embed runs.

C2: raw_content over 1024 chars (Discord's embed field cap) caused
inbox.send() to raise before the UID was marked seen, so oversized emails
were retried forever and never ingested. Added a defensive hard truncation
in build_ping_embed itself, plus an explicit truncation in email_ingest.py
before the embed is built (lossy for very long emails, acceptable for MVP).
2026-07-13 15:01:23 -07:00

72 lines
2.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
import discord
MODALITY_LABELS = {
"bh_email": "BH Email",
"personal_email": "Personal Email",
"bh_text": "BH Text",
"personal_text": "Personal Text",
"voice": "Voice",
"fb_messenger": "FB Messenger",
"nd_messenger": "ND Messenger",
}
# Discord's API rejects embed field values of length 0 (min 1, max 1024 chars).
# Use a zero-width space as a round-trip-safe sentinel for "empty" values so we
# never send a literal "" to Discord.
_EMPTY_FIELD_SENTINEL = ""
# Hard cap for any field value, with headroom for a truncation marker so the
# final value never exceeds Discord's 1024-char limit regardless of caller.
_FIELD_VALUE_MAX = 1024
_TRUNCATION_MARKER = "\n\n[...truncated for Discord embed limit...]"
def _safe_field_value(value: str) -> str:
"""Return a Discord-safe embed field value: never empty, never over 1024 chars."""
if not value:
return _EMPTY_FIELD_SENTINEL
if len(value) > _FIELD_VALUE_MAX:
keep = _FIELD_VALUE_MAX - len(_TRUNCATION_MARKER)
return value[:keep] + _TRUNCATION_MARKER
return value
def build_ping_embed(
*, modality: str, sender_id: str | None, snippet: str, raw_content: str,
captured_at: datetime,
) -> discord.Embed:
label = MODALITY_LABELS.get(modality, modality)
embed = discord.Embed(
title=f"{label}" + (f" - {sender_id}" if sender_id else ""),
description=snippet,
)
embed.add_field(name="modality", value=_safe_field_value(modality), inline=True)
embed.add_field(name="sender_id", value=_safe_field_value(sender_id or ""), inline=True)
embed.add_field(
name="captured_at", value=_safe_field_value(captured_at.isoformat()), inline=True,
)
embed.add_field(
name="raw_content", value=_safe_field_value(raw_content), inline=False,
)
return embed
def parse_ping_embed(embed: discord.Embed) -> dict:
values = {field.name: field.value for field in embed.fields}
sender_id = values.get("sender_id")
if sender_id == _EMPTY_FIELD_SENTINEL:
sender_id = None
else:
sender_id = sender_id or None
raw_content = values["raw_content"]
if raw_content == _EMPTY_FIELD_SENTINEL:
raw_content = ""
return {
"modality": values["modality"],
"sender_id": sender_id,
"raw_content": raw_content,
"captured_at": datetime.fromisoformat(values["captured_at"]),
}