from __future__ import annotations import json import logging import time from dataclasses import dataclass, field logger = logging.getLogger(__name__) @dataclass class CardInteractionEnvelope: version: str = "ocf1" kind: str = "button" action: str = "" quick_response: str | None = None metadata: dict | None = None context: dict | None = None class CardInteractionResult: def __init__(self, status: str, action: str = "", envelope: CardInteractionEnvelope | None = None): self.status = status self.action = action self.envelope = envelope def build_card_interaction_envelope( action: str, kind: str = "button", user_id: str | None = None, chat_id: str | None = None, ttl_ms: int = 300_000, ) -> CardInteractionEnvelope: context = {} if user_id: context["u"] = user_id if chat_id: context["h"] = chat_id context["e"] = int(time.time() * 1000) + ttl_ms context["t"] = int(time.time() * 1000) return CardInteractionEnvelope( version="ocf1", kind=kind, action=action, context=context, ) def encode_envelope_value(envelope: CardInteractionEnvelope) -> str: parts = [f"oc:{envelope.version}"] parts.append(f"k:{envelope.kind}") parts.append(f"a:{envelope.action}") if envelope.quick_response: parts.append(f"q:{envelope.quick_response}") if envelope.metadata: parts.append(f"m:{json.dumps(envelope.metadata, ensure_ascii=False)}") if envelope.context: parts.append(f"c:{json.dumps(envelope.context, ensure_ascii=False)}") return ";".join(parts) def decode_card_action(value: str, *, user_id: str | None = None, chat_id: str | None = None) -> CardInteractionResult: if not value or not value.startswith("oc:"): if "command" in value or "text" in value: try: data = json.loads(value) return CardInteractionResult(status="legacy", action=data.get("command", data.get("text", ""))) except (json.JSONDecodeError, TypeError): pass return CardInteractionResult(status="invalid") parts = {} for part in value.split(";"): if ":" in part: key, val = part.split(":", 1) parts[key] = val version = parts.get("oc", "") if version != "ocf1": return CardInteractionResult(status="invalid") context_str = parts.get("c", "{}") try: context = json.loads(context_str) except (json.JSONDecodeError, TypeError): return CardInteractionResult(status="invalid") if user_id and context.get("u") and context["u"] != user_id: return CardInteractionResult(status="invalid: wrong_user") if chat_id and context.get("h") and context["h"] != chat_id: return CardInteractionResult(status="invalid: wrong_conversation") expiry = context.get("e", 0) if expiry and int(time.time() * 1000) > expiry: return CardInteractionResult(status="invalid: stale") envelope = CardInteractionEnvelope( version=version, kind=parts.get("k", "button"), action=parts.get("a", ""), quick_response=parts.get("q"), metadata=json.loads(parts.get("m", "{}")) if parts.get("m") else None, context=context, ) return CardInteractionResult(status="structured", action=envelope.action, envelope=envelope) def check_legacy_card_command(card_body: dict) -> bool: elements = card_body.get("elements", []) for elem in elements: if elem.get("tag") in ("button", "overflow", "select_static"): value = elem.get("value", "") if value and not value.startswith("oc:"): if "command" in value or "text" in value: return True return False