117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""Generic fixed-schema text webhook channel adapter."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
from beaver.foundation.events import ChannelIdentity, InboundMessage, OutboundMessage
|
|
from beaver.interfaces.channels.base import ChannelInboundSink
|
|
|
|
|
|
class GenericWebhookAdapter:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
channel_id: str,
|
|
kind: str,
|
|
mode: str,
|
|
account_id: str,
|
|
display_name: str = "",
|
|
inbound_sink: ChannelInboundSink,
|
|
response_timeout_seconds: float = 1800,
|
|
) -> None:
|
|
self.channel_id = channel_id
|
|
self.kind = kind
|
|
self.mode = mode
|
|
self.account_id = account_id
|
|
self.display_name = display_name or channel_id
|
|
self.inbound_sink = inbound_sink
|
|
self.response_timeout_seconds = max(1.0, float(response_timeout_seconds))
|
|
self.started = False
|
|
self._pending: dict[str, asyncio.Future[OutboundMessage]] = {}
|
|
|
|
async def start(self) -> None:
|
|
self.started = True
|
|
|
|
async def stop(self) -> None:
|
|
self.started = False
|
|
for future in list(self._pending.values()):
|
|
if not future.done():
|
|
future.cancel()
|
|
self._pending.clear()
|
|
|
|
async def handle_webhook_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
text = str(payload.get("text") or "").strip()
|
|
peer_id = str(payload.get("peer_id") or "").strip()
|
|
message_id = str(payload.get("message_id") or "").strip()
|
|
thread_id = str(payload.get("thread_id") or "").strip() or None
|
|
peer_type = str(payload.get("peer_type") or "unknown").strip() or "unknown"
|
|
user_id = str(payload.get("user_id") or "").strip() or None
|
|
if not text:
|
|
return {"ok": False, "error": "text is required"}
|
|
if not peer_id:
|
|
return {"ok": False, "error": "peer_id is required"}
|
|
if not message_id:
|
|
return {"ok": False, "error": "message_id is required"}
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id=self.channel_id,
|
|
kind=self.kind,
|
|
account_id=self.account_id,
|
|
peer_id=peer_id,
|
|
thread_id=thread_id,
|
|
peer_type=peer_type,
|
|
user_id=user_id,
|
|
message_id=message_id,
|
|
)
|
|
inbound = InboundMessage(
|
|
channel=self.channel_id,
|
|
content=text,
|
|
user_id=user_id,
|
|
channel_identity=identity,
|
|
metadata={"webhook": {"peer_type": peer_type}},
|
|
)
|
|
future = asyncio.get_running_loop().create_future()
|
|
self._pending[inbound.message_id] = future
|
|
accept = await self.inbound_sink.accept_inbound(inbound)
|
|
if not accept.accepted:
|
|
self._pending.pop(inbound.message_id, None)
|
|
record = accept.record or {}
|
|
return {
|
|
"ok": accept.error is None,
|
|
"duplicate": accept.duplicate,
|
|
"pending": accept.pending,
|
|
"session_id": accept.session_id,
|
|
"status": record.get("status"),
|
|
"run_id": record.get("run_id"),
|
|
"reply": record.get("reply"),
|
|
"error": accept.error or record.get("error"),
|
|
}
|
|
try:
|
|
outbound = await asyncio.wait_for(future, timeout=self.response_timeout_seconds)
|
|
except asyncio.TimeoutError:
|
|
self._pending.pop(inbound.message_id, None)
|
|
return {
|
|
"ok": True,
|
|
"duplicate": False,
|
|
"pending": True,
|
|
"session_id": accept.session_id,
|
|
}
|
|
return {
|
|
"ok": outbound.finish_reason != "error",
|
|
"duplicate": False,
|
|
"pending": False,
|
|
"session_id": outbound.session_id,
|
|
"run_id": outbound.run_id,
|
|
"reply": outbound.content,
|
|
"error": outbound.metadata.get("error"),
|
|
}
|
|
|
|
async def send(self, message: OutboundMessage) -> None:
|
|
future = self._pending.pop(message.message_id, None)
|
|
if future is None or future.done():
|
|
message.metadata["delivery_status"] = "unclaimed"
|
|
return
|
|
future.set_result(message)
|