Ekaropolus 0eb2b393f2
All checks were successful
continuous-integration/drone/push Build is passing
SAMI Functionality add
2025-09-16 16:18:45 -06:00

127 lines
4.9 KiB
Python

# pxy_bots/canonical.py
from typing import Any, Dict, Optional
def _pick_photo(sizes):
# Telegram sends photos as array of sizes; pick the largest
if not sizes:
return None
sizes = sorted(sizes, key=lambda s: (s.get("width", 0) * s.get("height", 0)), reverse=True)
top = sizes[0]
return {
"type": "photo",
"file_id": top.get("file_id"),
"mime": "image/jpeg", # Telegram photos are JPEG
"size_bytes": None, # Telegram doesn't include bytes here; leave None
"width": top.get("width"),
"height": top.get("height"),
}
def _extract_media(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if "photo" in msg:
return _pick_photo(msg.get("photo") or [])
if "voice" in msg:
v = msg["voice"]
return {"type": "voice", "file_id": v.get("file_id"), "mime": v.get("mime_type"), "size_bytes": v.get("file_size"), "duration": v.get("duration")}
if "audio" in msg:
a = msg["audio"]
return {"type": "audio", "file_id": a.get("file_id"), "mime": a.get("mime_type"), "size_bytes": a.get("file_size"), "duration": a.get("duration")}
if "video" in msg:
v = msg["video"]
return {"type": "video", "file_id": v.get("file_id"), "mime": v.get("mime_type"), "size_bytes": v.get("file_size"), "duration": v.get("duration"), "width": v.get("width"), "height": v.get("height")}
if "video_note" in msg:
v = msg["video_note"]
return {"type": "video_note", "file_id": v.get("file_id"), "mime": None, "size_bytes": v.get("file_size"), "duration": v.get("duration"), "length": v.get("length")}
if "animation" in msg:
a = msg["animation"]
return {"type": "animation", "file_id": a.get("file_id"), "mime": a.get("mime_type"), "size_bytes": a.get("file_size")}
if "document" in msg:
d = msg["document"]
return {"type": "document", "file_id": d.get("file_id"), "mime": d.get("mime_type"), "size_bytes": d.get("file_size"), "file_name": d.get("file_name")}
return None
def build_req_v1(update: Dict[str, Any], bot_name: str) -> Dict[str, Any]:
"""
Normalize a Telegram update into our canonical req.v1 envelope.
Pure function. No network, no state.
"""
schema_version = "req.v1"
update_id = update.get("update_id")
# Determine primary container: message, edited_message, callback_query
msg = update.get("message") or update.get("edited_message")
cbq = update.get("callback_query")
# Chat/user basics
if msg:
chat = msg.get("chat") or {}
user = msg.get("from") or {}
message_id = msg.get("message_id")
ts = msg.get("date")
text = msg.get("text")
caption = msg.get("caption")
location = msg.get("location")
media = _extract_media(msg)
trigger = "message"
elif cbq:
m = cbq.get("message") or {}
chat = m.get("chat") or {}
user = cbq.get("from") or {}
message_id = m.get("message_id")
ts = m.get("date") or None
text = None
caption = None
location = None
media = None
trigger = "callback"
else:
# Fallback for other update types we haven't mapped yet
chat = {}
user = update.get("from") or {}
message_id = None
ts = None
text = None
caption = None
location = None
media = None
trigger = "unknown"
# Command name (if text/caption starts with '/')
raw_cmd = None
if text and isinstance(text, str) and text.startswith("/"):
raw_cmd = text.split()[0][1:]
elif caption and isinstance(caption, str) and caption.startswith("/"):
raw_cmd = caption.split()[0][1:]
elif cbq and isinstance(cbq.get("data"), str):
raw_cmd = None # callbacks carry 'action' instead
# Build envelope
env = {
"schema_version": schema_version,
"bot": {"username": bot_name},
"chat": {"id": chat.get("id"), "type": chat.get("type")},
"user": {"id": user.get("id"), "language": user.get("language_code")},
"command": {
"name": raw_cmd,
"version": 1,
"trigger": ("text_command" if raw_cmd and trigger == "message" else ("callback" if trigger == "callback" else trigger)),
},
"input": {
"text": text,
"caption": caption,
"args_raw": text or caption,
"media": media,
"location": ({"lat": location.get("latitude"), "lon": location.get("longitude")} if location else None),
},
"callback": (
{"id": cbq.get("id"), "data": cbq.get("data"), "origin": {"message_id": message_id, "chat_id": chat.get("id")}}
if cbq else None
),
"context": {
"message_id": message_id,
"update_id": update_id,
"ts": ts,
"idempotency_key": f"tg:{message_id}:{user.get('id')}" if message_id and user.get("id") else None,
},
}
return env