feat(pheby): accept inbound chat attachments
This commit is contained in:
+82
-2
@@ -61,11 +61,13 @@ class PhebyServer:
|
||||
async def start(self) -> bool:
|
||||
from aiohttp import web as _web # local import keeps import light
|
||||
self.store.hydrate_legacy_meta()
|
||||
app = _web.Application(client_max_size=proto.MAX_WS_MESSAGE_BYTES)
|
||||
app = _web.Application(client_max_size=max(proto.MAX_UPLOAD_BYTES,
|
||||
proto.MAX_WS_MESSAGE_BYTES))
|
||||
app.router.add_get("/health", self._handle_health)
|
||||
app.router.add_get("/ws", self._handle_ws)
|
||||
app.router.add_get("/attachments/{attachment_id}",
|
||||
self._handle_attachment_download)
|
||||
app.router.add_post("/attachments", self._handle_attachment_upload)
|
||||
self._app = app
|
||||
self._runner = web.AppRunner(app, access_log=None)
|
||||
await self._runner.setup()
|
||||
@@ -171,6 +173,57 @@ class PhebyServer:
|
||||
},
|
||||
)
|
||||
|
||||
async def _handle_attachment_upload(
|
||||
self, request: web.Request) -> web.Response:
|
||||
"""Inbound attachment upload: raw body → adapter storage.
|
||||
|
||||
The descriptor is returned only to the uploader. Other clients see
|
||||
the attachment after chat.send successfully claims it.
|
||||
"""
|
||||
if not self._check_http_secret(request):
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_UNAUTHORIZED,
|
||||
"message": "Authentication required"}},
|
||||
status=401)
|
||||
conversation_id = request.query.get("conversation_id", "")
|
||||
if not ConversationRouter.is_valid_conversation_id(conversation_id):
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_BAD_REQUEST,
|
||||
"message": "Invalid conversation_id"}},
|
||||
status=400)
|
||||
declared = request.content_length
|
||||
if declared is not None and declared > proto.MAX_UPLOAD_BYTES:
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_TOO_LARGE,
|
||||
"message": f"Upload exceeds "
|
||||
f"{proto.MAX_UPLOAD_BYTES} bytes"}},
|
||||
status=413)
|
||||
data = await request.content.read(proto.MAX_UPLOAD_BYTES + 1)
|
||||
if len(data) > proto.MAX_UPLOAD_BYTES:
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_TOO_LARGE,
|
||||
"message": f"Upload exceeds "
|
||||
f"{proto.MAX_UPLOAD_BYTES} bytes"}},
|
||||
status=413)
|
||||
if not data:
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_BAD_REQUEST,
|
||||
"message": "Empty upload body"}},
|
||||
status=400)
|
||||
filename = request.query.get("filename") or "file.bin"
|
||||
mime = request.headers.get("Content-Type", "").split(";")[0].strip()
|
||||
desc = await self.store.register_bytes(
|
||||
data, conversation_id=conversation_id, filename=filename,
|
||||
mime_type=mime or None)
|
||||
if desc is None:
|
||||
return web.json_response(
|
||||
{"error": {"code": proto.ERR_INTERNAL,
|
||||
"message": "Attachment registration failed"}},
|
||||
status=500)
|
||||
logger.info("[pheby] attachment upload: id=%s bytes=%d conv=%s",
|
||||
desc["attachment_id"], desc["size"], conversation_id)
|
||||
return web.json_response({"attachment": desc}, status=201)
|
||||
|
||||
# ── WebSocket handler ────────────────────────────────────────────────
|
||||
async def _handle_ws(self, request: web.Request) -> web.WebSocketResponse:
|
||||
# Bind server/adapter identity to THIS task's context. ContextVars set
|
||||
@@ -468,8 +521,35 @@ class PhebyServer:
|
||||
proto.ERR_TOO_LARGE,
|
||||
f"text exceeds {proto.MAX_TEXT_CHARS} chars", request_id))
|
||||
return
|
||||
raw_ids = message.get("attachment_ids")
|
||||
attachment_ids: List[str] = []
|
||||
if raw_ids is not None:
|
||||
if not isinstance(raw_ids, list) or \
|
||||
not all(isinstance(x, str) for x in raw_ids) or \
|
||||
len(raw_ids) > 10 or len(raw_ids) != len(set(raw_ids)):
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_BAD_REQUEST,
|
||||
"attachment_ids must be at most 10 unique ids",
|
||||
request_id))
|
||||
return
|
||||
for aid in raw_ids:
|
||||
desc = self.store.describe(aid)
|
||||
if desc is None or \
|
||||
desc.get("conversation_id") != conversation_id or \
|
||||
desc.get("direction") != "inbound":
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_NOT_FOUND,
|
||||
"Unknown attachment for this conversation", request_id))
|
||||
return
|
||||
if desc.get("message_id"):
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_BAD_REQUEST,
|
||||
"Attachment already belongs to a message", request_id))
|
||||
return
|
||||
attachment_ids.append(aid)
|
||||
await self.bridge.send_chat(
|
||||
self, conversation_id, text, client, request_id)
|
||||
self, conversation_id, text, client, request_id,
|
||||
attachment_ids=attachment_ids)
|
||||
|
||||
async def _handle_run_cancel(self, client, message, request_id):
|
||||
conversation_id = str(message.get("conversation_id", ""))
|
||||
|
||||
Reference in New Issue
Block a user