from __future__ import annotations from yuxi.channels.models import ( Attachment, ChannelIdentity, ChannelMessage, ChannelType, ChatType, EventType, MessageType, ) class LINEEventNormalizer: def __init__(self, channel_id: str = "line"): self._channel_id = channel_id def normalize(self, event: dict) -> ChannelMessage: source = event.get("source", {}) source_type = source.get("type", "user") source_id = source.get("userId") or source.get("groupId") or source.get("roomId") or "unknown" chat_type = ChatType.DIRECT if source_type == "user" else ChatType.GROUP chat_id = self._resolve_chat_id(source_type, source_id) event_type = self._resolve_event_type(event.get("type", "")) reply_token = event.get("replyToken", "") message_obj = event.get("message", {}) msg_type_str = message_obj.get("type", "text") raw_event_type = event.get("type", "") if raw_event_type == "postback": content = event.get("postback", {}).get("data", "") msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "follow": content = "用户关注了 Bot" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "join": content = "Bot 加入群组/房间" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "unfollow": content = "用户取消关注了 Bot" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "leave": content = "Bot 离开了群组/房间" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "memberJoined": content = self._build_member_event_content(event, "joined") msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "memberLeft": content = self._build_member_event_content(event, "left") msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "unsend": unsend_msg_id = event.get("unsend", {}).get("messageId", "") content = f"用户撤回了消息 ({unsend_msg_id})" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "beacon": beacon = event.get("beacon", {}) content = f"Beacon 事件: {beacon.get('type', '')} (hwid: {beacon.get('hwid', '')})" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "accountLink": link_data = event.get("link", {}) content = f"账号关联: result={link_data.get('result', '')}" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "things": things_data = event.get("things", {}) content = f"Things 事件: {things_data.get('type', '')} (deviceId: {things_data.get('deviceId', '')})" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "activated": content = "Chat Control: 频道已激活" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "deactivated": content = "Chat Control: 频道已停用" msg_type = MessageType.TEXT attachments = [] elif raw_event_type == "videoPlayComplete": tracking_id = event.get("videoPlayComplete", {}).get("trackingId", "") content = f"视频播放完成 (trackingId: {tracking_id})" msg_type = MessageType.TEXT attachments = [] else: content, msg_type, attachments = self._extract_content(msg_type_str, message_obj) return ChannelMessage( identity=ChannelIdentity( channel_id=self._channel_id, channel_type=ChannelType.LINE, channel_user_id=source_id, channel_chat_id=chat_id, channel_message_id=message_obj.get("id") or event.get("webhookEventId", ""), ), event_type=event_type, message_type=msg_type, chat_type=chat_type, content=content, attachments=attachments, metadata={ "source_type": source_type, "reply_token": reply_token, "raw_event_type": event.get("type", ""), "raw_msg_type": msg_type_str, "timestamp_ms": event.get("timestamp", 0), }, ) @staticmethod def _resolve_chat_id(source_type: str, source_id: str) -> str: prefix_map = {"user": "user_", "group": "group_", "room": "room_"} return f"{prefix_map.get(source_type, 'unknown_')}{source_id}" @staticmethod def _resolve_event_type(raw_type: str) -> EventType: mapping = { "message": EventType.MESSAGE_RECEIVED, "follow": EventType.BOT_ADDED, "unfollow": EventType.BOT_REMOVED, "join": EventType.MEMBER_JOINED, "leave": EventType.MEMBER_LEFT, "memberJoined": EventType.MEMBER_JOINED, "memberLeft": EventType.MEMBER_LEFT, "postback": EventType.CARD_ACTION, "unsend": EventType.MESSAGE_DELETED, "beacon": EventType.SYSTEM_EVENT, "accountLink": EventType.SYSTEM_EVENT, "things": EventType.SYSTEM_EVENT, "activated": EventType.SYSTEM_EVENT, "deactivated": EventType.SYSTEM_EVENT, "videoPlayComplete": EventType.SYSTEM_EVENT, } return mapping.get(raw_type, EventType.MESSAGE_RECEIVED) @staticmethod def _extract_content(msg_type: str, msg: dict) -> tuple[str, MessageType, list[Attachment]]: if msg_type == "text": text = msg.get("text", "") mention_info = LINEEventNormalizer._extract_native_mentions(msg) if mention_info: text = f"{text}\n[Mentions: {mention_info}]" return text, MessageType.TEXT, [] if msg_type == "image": return "", MessageType.IMAGE, [Attachment(type="image", file_id=msg.get("id"))] if msg_type == "video": return "", MessageType.VIDEO, [Attachment(type="video", file_id=msg.get("id"))] if msg_type == "audio": duration = msg.get("duration", 0) return f"Audio ({duration}ms)", MessageType.AUDIO, [Attachment(type="audio", file_id=msg.get("id"))] if msg_type == "file": filename = msg.get("file_name", "unknown") size = msg.get("file_size", 0) return ( f"File: {filename} ({size} bytes)", MessageType.FILE, [Attachment(type="file", file_id=msg.get("id"), filename=filename, size_bytes=size)], ) if msg_type == "location": title = msg.get("title", "") address = msg.get("address", "") return f"Location: {title} ({address})", MessageType.LOCATION, [] if msg_type == "sticker": pkg_id = msg.get("package_id", "") sticker_id = msg.get("sticker_id", "") return f"Sticker (pkg:{pkg_id}, id:{sticker_id})", MessageType.TEXT, [] return str(msg)[:500], MessageType.TEXT, [] @staticmethod def _build_member_event_content(event: dict, action: str) -> str: members = ( event.get("members") or event.get("left", {}).get("members") or event.get("joined", {}).get("members") or [] ) source = event.get("source", {}) group_id = source.get("groupId") or source.get("roomId") or "unknown" user_ids = [m.get("userId", "?") for m in members] if action == "joined": users_str = ", ".join(user_ids) return f"[{group_id}] 新成员加入: {users_str}" else: users_str = ", ".join(user_ids) return f"[{group_id}] 成员离开: {users_str}" @staticmethod def _extract_native_mentions(msg: dict) -> str | None: mention = msg.get("mention") if not mention or not isinstance(mention, dict): return None mentionees = mention.get("mentionees") if not mentionees: return None user_ids = [m.get("userId", "?") for m in mentionees if isinstance(m, dict)] return ", ".join(user_ids) if user_ids else None