feat(pheby): accept inbound chat attachments
This commit is contained in:
@@ -179,6 +179,7 @@ class AttachmentStore:
|
||||
"kind": kind,
|
||||
"conversation_id": conversation_id,
|
||||
"message_id": message_id,
|
||||
"direction": "outbound",
|
||||
"created_at": protocol.now_iso(),
|
||||
"created_epoch": time.time(),
|
||||
"retention_days": self._retention_days,
|
||||
@@ -197,6 +198,83 @@ class AttachmentStore:
|
||||
attachment_id, kind, size, conversation_id)
|
||||
return self.describe(attachment_id)
|
||||
|
||||
async def register_bytes(
|
||||
self,
|
||||
data: bytes,
|
||||
*,
|
||||
conversation_id: str,
|
||||
filename: str,
|
||||
mime_type: Optional[str] = None,
|
||||
message_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Register an *inbound* upload: raw client bytes → adapter storage.
|
||||
|
||||
Returns the attachment descriptor dict, or ``None`` on failure.
|
||||
"""
|
||||
fname = self._sanitize_filename(filename)
|
||||
attachment_id = protocol.new_id()
|
||||
mime = (mime_type or "").strip()
|
||||
if mime in ("", "application/octet-stream"):
|
||||
mime = guess_mime(fname)
|
||||
if not isinstance(mime, str) or "/" not in mime or len(mime) > 120:
|
||||
mime = "application/octet-stream"
|
||||
is_image = mime.startswith(IMAGE_MIME_PREFIXES) or (
|
||||
Path(fname).suffix.lower() in IMAGE_EXTS)
|
||||
if Path(fname).suffix.lower() in VIDEO_EXTS:
|
||||
kind = "video"
|
||||
elif Path(fname).suffix.lower() in AUDIO_EXTS:
|
||||
kind = "audio"
|
||||
elif is_image:
|
||||
kind = "image"
|
||||
else:
|
||||
kind = "document"
|
||||
size = len(data)
|
||||
|
||||
try:
|
||||
async with self._lock:
|
||||
self._load_index()
|
||||
blob = self._blob_path(attachment_id, fname)
|
||||
blob.parent.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(blob.write_bytes, data)
|
||||
meta: Dict[str, Any] = {
|
||||
"attachment_id": attachment_id,
|
||||
"filename": fname,
|
||||
"mime_type": mime,
|
||||
"size": size,
|
||||
"kind": kind,
|
||||
"conversation_id": conversation_id,
|
||||
"message_id": message_id,
|
||||
"direction": "inbound",
|
||||
"created_at": protocol.now_iso(),
|
||||
"created_epoch": time.time(),
|
||||
"retention_days": self._retention_days,
|
||||
"blob": blob.name,
|
||||
"blob_subdir": blob.parent.name,
|
||||
}
|
||||
self._meta[attachment_id] = meta
|
||||
self._save_index()
|
||||
except Exception:
|
||||
logger.error("[pheby] inbound attachment registration failed",
|
||||
exc_info=True)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
"[pheby] inbound attachment registered: id=%s kind=%s size=%d "
|
||||
"conv=%s", attachment_id, kind, size, conversation_id)
|
||||
return self.describe(attachment_id)
|
||||
|
||||
def anchor_message(self, attachment_id: str, message_id: str) -> None:
|
||||
"""Attach *message_id* to a registered attachment (thread anchoring)."""
|
||||
self._load_index()
|
||||
meta = self._meta.get(attachment_id)
|
||||
if meta is not None:
|
||||
meta["message_id"] = str(message_id)
|
||||
self._save_index()
|
||||
|
||||
def get_blob_path(self, attachment_id: str) -> Optional[Path]:
|
||||
"""Filesystem path for an attachment (server-side use only)."""
|
||||
return self.resolve_blob(attachment_id)
|
||||
|
||||
# ── lookup / download ────────────────────────────────────────────────
|
||||
def describe(self, attachment_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Public descriptor for an attachment (no server paths)."""
|
||||
@@ -210,6 +288,7 @@ class AttachmentStore:
|
||||
"size": int(meta.get("size", 0)),
|
||||
"kind": meta.get("kind", "document"),
|
||||
"inline_image": meta.get("kind") == "image",
|
||||
"direction": meta.get("direction", "outbound"),
|
||||
"conversation_id": meta.get("conversation_id"),
|
||||
"message_id": meta.get("message_id"),
|
||||
"created_at": meta.get("created_at"),
|
||||
@@ -261,7 +340,8 @@ class AttachmentStore:
|
||||
out = []
|
||||
for aid in list(self._meta):
|
||||
desc = self.describe(aid)
|
||||
if desc and desc.get("conversation_id") == conversation_id:
|
||||
if desc and desc.get("conversation_id") == conversation_id and (
|
||||
desc.get("direction") != "inbound" or desc.get("message_id")):
|
||||
out.append(desc)
|
||||
return out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user