import logging from datetime import datetime, UTC from yuxi.channel.message.models import ( MessageType, PeerInfo, UnifiedMessage, ) from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) class MessengerMonitor: def parse_event_to_unified( self, event: dict, account_id: str, page_id: str = "", ) -> UnifiedMessage | None: sender_psid = event.get("sender", {}).get("id", "") timestamp_ms = event.get("timestamp", 0) if not sender_psid: logger.warning("messenger event missing sender PSID") return None if "message" in event: message = event.get("message", {}) if message.get("is_echo"): return self._parse_echo_event(event, sender_psid, account_id, page_id, timestamp_ms) return self._parse_message_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "postback" in event: return self._parse_postback_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "referral" in event: return self._parse_referral_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "optin" in event: return self._parse_optin_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "delivery" in event: return self._parse_delivery_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "read" in event: return self._parse_read_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "account_linking" in event: return self._parse_account_linking_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "pass_thread_control" in event or "take_thread_control" in event or "request_thread_control" in event: return self._parse_handover_event(event, sender_psid, account_id, page_id, timestamp_ms) elif "policy_enforcement" in event: return self._parse_policy_event(event, sender_psid, account_id, page_id, timestamp_ms) else: logger.debug(f"messenger unhandled event type: keys={list(event.keys())}") return None def _parse_message_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage | None: message = event.get("message", {}) msg_id = f"messenger:{message.get('mid', '')}" attachments = message.get("attachments", []) if attachments: return self._parse_attachment_event(attachments, msg_id, psid, account_id, page_id, timestamp_ms, event) quick_reply = message.get("quick_reply", {}) if quick_reply: return self._parse_quick_reply(quick_reply, msg_id, psid, account_id, page_id, timestamp_ms, event) text = message.get("text", "") return self._build_unified( msg_id=msg_id, psid=psid, account_id=account_id, content=text, msg_type=MessageType.TEXT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id}, ) def _parse_attachment_event( self, attachments: list, msg_id: str, psid: str, account_id: str, page_id: str, timestamp_ms: int, raw: dict, ) -> UnifiedMessage | None: attachment = attachments[0] att_type = attachment.get("type", "") payload = attachment.get("payload", {}) type_map = { "image": (MessageType.IMAGE, "[图片]"), "video": (MessageType.FILE, "[视频]"), "audio": (MessageType.VOICE, "[语音消息]"), "file": (MessageType.FILE, "[文件]"), "location": (MessageType.TEXT, None), } if att_type == "location": coordinates = payload.get("coordinates", {}) lat = coordinates.get("lat") lon = coordinates.get("long") title = payload.get("title", "") content = f"[位置] {title}" if title else "[位置]" return self._build_unified( msg_id=msg_id, psid=psid, account_id=account_id, content=content, msg_type=MessageType.TEXT, timestamp_ms=timestamp_ms, raw=raw, metadata={"page_id": page_id}, location_lat=lat, location_lon=lon, ) msg_type, fallback_text = type_map.get(att_type, (MessageType.FILE, f"[{att_type}]")) media_url = payload.get("url", "") media_urls = [media_url] if media_url else [] return self._build_unified( msg_id=msg_id, psid=psid, account_id=account_id, content=fallback_text or att_type, msg_type=msg_type, media_urls=media_urls, timestamp_ms=timestamp_ms, raw=raw, metadata={"page_id": page_id, "attachment_type": att_type}, ) def _parse_quick_reply( self, quick_reply: dict, msg_id: str, psid: str, account_id: str, page_id: str, timestamp_ms: int, raw: dict, ) -> UnifiedMessage: payload = quick_reply.get("payload", "") return self._build_unified( msg_id=msg_id, psid=psid, account_id=account_id, content=payload, msg_type=MessageType.TEXT, timestamp_ms=timestamp_ms, raw=raw, metadata={"page_id": page_id, "is_quick_reply": True}, ) def _parse_postback_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: postback = event.get("postback", {}) payload = postback.get("payload", "") title = postback.get("title", "") content = payload or title return self._build_unified( msg_id=f"messenger:pb:{payload}", psid=psid, account_id=account_id, content=content, msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "postback", "title": title}, ) def _parse_referral_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: referral = event.get("referral", {}) ref = referral.get("ref", "") return self._build_unified( msg_id=f"messenger:ref:{ref}", psid=psid, account_id=account_id, content=f"[referral] {ref}", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "referral", "ref": ref}, ) def _parse_optin_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: optin = event.get("optin", {}) return self._build_unified( msg_id=f"messenger:optin:{optin.get('one_time_notif_token', '')}", psid=psid, account_id=account_id, content="[optin]", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "optin", "optin_payload": optin.get("payload")}, ) def _parse_delivery_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: delivery = event.get("delivery", {}) mids = delivery.get("mids", []) mid = mids[0] if mids else "" return self._build_unified( msg_id=f"messenger:delivery:{mid}", psid=psid, account_id=account_id, content="[delivery]", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "delivery", "mids": mids}, ) def _parse_read_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: read = event.get("read", {}) return self._build_unified( msg_id=f"messenger:read:{read.get('watermark', '')}", psid=psid, account_id=account_id, content="[read]", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "read", "watermark": read.get("watermark")}, ) def _parse_echo_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: message = event.get("message", {}) mid = message.get("mid", "") return self._build_unified( msg_id=f"messenger:echo:{mid}", psid=psid, account_id=account_id, content=message.get("text", "[echo]"), msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={"page_id": page_id, "event_type": "echo", "mid": mid}, ) def _parse_account_linking_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: linking = event.get("account_linking", {}) status = linking.get("status", "") return self._build_unified( msg_id=f"messenger:linking:{status}", psid=psid, account_id=account_id, content=f"[account_linking] {status}", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={ "page_id": page_id, "event_type": "account_linking", "status": status, "authorization_code": linking.get("authorization_code"), }, ) def _parse_handover_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: if "pass_thread_control" in event: handover = event.get("pass_thread_control", {}) event_type = "pass_thread_control" elif "take_thread_control" in event: handover = event.get("take_thread_control", {}) event_type = "take_thread_control" else: handover = event.get("request_thread_control", {}) event_type = "request_thread_control" return self._build_unified( msg_id=f"messenger:{event_type}:{handover.get('metadata', '')}", psid=psid, account_id=account_id, content=f"[{event_type}]", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={ "page_id": page_id, "event_type": event_type, "new_owner_app_id": handover.get("new_owner_app_id"), "previous_owner_app_id": handover.get("previous_owner_app_id"), "metadata": handover.get("metadata"), }, ) def _parse_policy_event( self, event: dict, psid: str, account_id: str, page_id: str, timestamp_ms: int ) -> UnifiedMessage: policy = event.get("policy_enforcement", {}) action = policy.get("action", "") reason = policy.get("reason", "") return self._build_unified( msg_id=f"messenger:policy:{action}", psid=psid, account_id=account_id, content=f"[policy_enforcement] {action}: {reason}", msg_type=MessageType.EVENT, timestamp_ms=timestamp_ms, raw=event, metadata={ "page_id": page_id, "event_type": "policy_enforcement", "action": action, "reason": reason, }, ) @staticmethod def _build_unified( msg_id: str, psid: str, account_id: str, content: str, msg_type: MessageType = MessageType.TEXT, media_urls: list[str] | None = None, timestamp_ms: int = 0, raw: dict | None = None, metadata: dict | None = None, location_lat: float | None = None, location_lon: float | None = None, ) -> UnifiedMessage: timestamp = None if timestamp_ms: try: timestamp = datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC) except (OSError, ValueError): pass sender = PeerInfo( kind=PeerKind.DIRECT, id=psid, display_name=None, is_bot=False, is_self=False, ) return UnifiedMessage( msg_id=msg_id, channel_type="messenger", account_id=account_id, content=content, sender=sender, message_type=msg_type, media_urls=media_urls or [], timestamp=timestamp, raw_payload=raw or {}, metadata=metadata or {}, reply_to_id=None, location_lat=location_lat, location_lon=location_lon, )