120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
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 |