Initial commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from opc.channels.provider_base import SocketChannel
|
||||
from opc.core.models import SystemMessage
|
||||
|
||||
|
||||
class WhatsAppChannel(SocketChannel):
|
||||
name = "whatsapp"
|
||||
required_package = "websockets"
|
||||
delivery_mode = "bridge"
|
||||
|
||||
def __init__(self, config: Any, bus: Any):
|
||||
super().__init__(config, bus)
|
||||
self._ws: Any = None
|
||||
self._processed_message_ids: OrderedDict[str, None] = OrderedDict()
|
||||
|
||||
def get_required_config_fields(self) -> list[str]:
|
||||
return ["bridge_url"]
|
||||
|
||||
def normalize_event(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
message = payload.get("message", payload)
|
||||
sender = str(message.get("sender", message.get("from", "")) or "")
|
||||
if not sender:
|
||||
sender = str(payload.get("sender", payload.get("from", "")) or "")
|
||||
pn = str(message.get("pn", "") or "")
|
||||
if not pn:
|
||||
pn = str(payload.get("pn", "") or "")
|
||||
sender_id = pn or sender
|
||||
if "@" in sender_id:
|
||||
sender_id = sender_id.split("@", 1)[0]
|
||||
content = str(message.get("text", message.get("content", message.get("body", ""))) or "")
|
||||
attachments = list(message.get("media", []) or [])
|
||||
if attachments:
|
||||
for path in attachments:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
tag = "image" if mime and mime.startswith("image/") else "file"
|
||||
content = f"{content}\n[{tag}: {path}]".strip() if content else f"[{tag}: {path}]"
|
||||
return {
|
||||
"sender_id": sender_id,
|
||||
"chat_id": str(message.get("chat_id", sender) or ""),
|
||||
"content": content,
|
||||
"thread_id": "",
|
||||
"reply_to": str(message.get("reply_to", message.get("id", "")) or ""),
|
||||
"attachments": attachments,
|
||||
"metadata": {
|
||||
"message_id": str(message.get("id", "") or ""),
|
||||
"is_group": bool(message.get("isGroup", False)),
|
||||
"raw_sender": sender,
|
||||
},
|
||||
}
|
||||
|
||||
async def run_socket_forever(self) -> None:
|
||||
import websockets
|
||||
|
||||
async with websockets.connect(self.config.bridge_url) as ws:
|
||||
self._ws = ws
|
||||
if self.config.bridge_token:
|
||||
await ws.send(json.dumps({"type": "auth", "token": self.config.bridge_token}))
|
||||
async for raw in ws:
|
||||
await self._handle_bridge_message(raw)
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._ws is not None:
|
||||
try:
|
||||
await self._ws.close()
|
||||
except Exception:
|
||||
logger.exception("whatsapp bridge close failed")
|
||||
self._ws = None
|
||||
await super().stop()
|
||||
|
||||
async def send(self, message: SystemMessage) -> None:
|
||||
await super().send(message)
|
||||
if self._ws is None:
|
||||
logger.warning("whatsapp bridge not connected")
|
||||
return
|
||||
metadata = dict(message.metadata or {})
|
||||
payload = {
|
||||
"type": "send",
|
||||
"to": str(metadata.get("chat_id") or message.session_id or ""),
|
||||
"text": message.content,
|
||||
"attachments": list(metadata.get("attachments", []) or []),
|
||||
}
|
||||
await self._ws.send(json.dumps(payload, ensure_ascii=False))
|
||||
|
||||
async def _handle_bridge_message(self, raw: str) -> None:
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("invalid whatsapp bridge payload: {}", raw[:100])
|
||||
return
|
||||
msg_type = str(payload.get("type", "") or "")
|
||||
if msg_type != "message":
|
||||
if msg_type == "error":
|
||||
self.set_last_error(str(payload.get("error", "bridge error")))
|
||||
return
|
||||
message_id = str(payload.get("id", "") or payload.get("message", {}).get("id", "") or "")
|
||||
if message_id:
|
||||
if message_id in self._processed_message_ids:
|
||||
return
|
||||
self._processed_message_ids[message_id] = None
|
||||
while len(self._processed_message_ids) > 1000:
|
||||
self._processed_message_ids.popitem(last=False)
|
||||
normalized = self.normalize_event(payload)
|
||||
await self.publish_normalized(normalized)
|
||||
Reference in New Issue
Block a user