# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Describing an envelope's *shape* without logging what anybody wrote. `DOCS-signal-envelopes.md` exists because the wire format is not obvious and guessing at it has cost this repo real bugs -- the sticker read off the wrong nesting level, and 0.0.3's `!gekon` shipping green on an assumption nobody had checked against the live API. The recurring problem is that answering "what does a frame with an attachment actually look like?" needed a code change, a restart, and somebody sending one at the right moment. So the bot can now say it itself, at DEBUG. What it emits is keys and value *types*, never values -- with a small allowlist for the handful of fields that are format, not content: a MIME type and a groupInfo type say nothing about anybody. Message bodies, names, numbers, uuids, quoted text and sticker ids are all reduced to their type. LOG_LEVEL=DEBUG, then: journalctl --user -u signal-bot | grep 'envelope shape' """ from __future__ import annotations import json from typing import Final # Fields whose value is a wire-format fact rather than anything a person said. SAFE_VALUES: Final = frozenset({"contentType", "type"}) def _describe(value: object, key: str = "") -> str: """Render one value as its shape.""" if isinstance(value, dict): inner = ", ".join(f"{k}: {_describe(v, k)}" for k, v in sorted(value.items())) return "{" + inner + "}" if isinstance(value, list): if not value: return "[]" # One entry is enough to show the element shape; a photo album would # otherwise print the same dict twenty times. return f"[{_describe(value[0])} x{len(value)}]" if key in SAFE_VALUES and isinstance(value, str): return repr(value) if value is None: return "null" return type(value).__name__ def shape_of(raw: str | bytes) -> str: """Describe a raw frame as keys and types, or say why it could not be. Args: raw: The JSON frame exactly as received. Returns: A one-line description safe to write to a log. """ try: decoded = json.loads(raw) except json.JSONDecodeError, UnicodeDecodeError: return "" return _describe(decoded)