import json import logging import time from collections import defaultdict from datetime import UTC, datetime from yuxi.channel.message.models import MessageType, PeerInfo, UnifiedMessage from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) class RateLimiter: def __init__(self, max_requests: int = 10, window: int = 10): self._max = max_requests self._window = window self._buckets: dict[str, list[float]] = defaultdict(list) def allow(self, key: str) -> bool: now = time.time() bucket = self._buckets[key] bucket[:] = [t for t in bucket if now - t < self._window] if len(bucket) >= self._max: return False bucket.append(now) return True def _convert_to_unified_event( event: dict, account, ) -> UnifiedMessage | None: cmd = event.get("CallbackCommand", "") msg_body = event.get("MsgBody", []) if not msg_body: return None first_msg = msg_body[0] msg_type = first_msg.get("MsgType", "") msg_content = first_msg.get("MsgContent", {}) content, unified_type = _parse_msg_content(msg_type, msg_content) if not content: return None is_group = "Group" in cmd from_account = event.get("From_Account", "") to_account = event.get("To_Account", "") group_id = event.get("GroupId", "") media_urls = _extract_media_urls(msg_type, msg_content) return UnifiedMessage( msg_id=f"{from_account}_{event.get('MsgSeq', '')}_{event.get('MsgRandom', '')}", channel_type="tencent-im", account_id=account.account_id, content=content, message_type=unified_type, sender=PeerInfo( id=from_account, kind=PeerKind.GROUP if is_group else PeerKind.DIRECT, display_name=from_account, ), timestamp=datetime.fromtimestamp(event.get("MsgTime", 0), tz=UTC), raw_payload=event, body_for_agent=content, media_urls=media_urls, metadata={ "group_id": group_id if is_group else "", "callback_command": cmd, "msg_type": msg_type, "to_account": to_account, }, ) def _parse_msg_content(msg_type: str, msg_content: dict) -> tuple[str, str]: if msg_type == "TIMTextElem": return msg_content.get("Text", ""), MessageType.TEXT if msg_type == "TIMCustomElem": data = msg_content.get("Data", "") content = data if isinstance(data, str) else json.dumps(data) return content, MessageType.TEXT if msg_type == "TIMImageElem": return "[图片]", MessageType.IMAGE if msg_type == "TIMSoundElem": return "[语音]", MessageType.VOICE if msg_type == "TIMVideoFileElem": return "[视频]", MessageType.VIDEO if msg_type == "TIMFileElem": return f"[文件: {msg_content.get('FileName', 'unknown')}]", MessageType.FILE if msg_type == "TIMFaceElem": return f"[表情: {msg_content.get('Index', 0)}]", MessageType.TEXT if msg_type == "TIMLocationElem": lat = msg_content.get("Latitude", 0) lng = msg_content.get("Longitude", 0) desc = msg_content.get("Desc", "") return f"[位置: {desc} ({lat}, {lng})]", MessageType.TEXT if msg_type == "TIMRelayElem": title = msg_content.get("Title", "合并转发消息") relay_count = len(msg_content.get("MsgList", [])) return f"[合并转发: {title} ({relay_count}条)]", MessageType.TEXT return f"[{msg_type}]", MessageType.TEXT def _extract_media_urls(msg_type: str, msg_content: dict) -> list[str]: urls = [] if msg_type == "TIMImageElem": for img_info in msg_content.get("ImageInfoArray", []): if url := img_info.get("URL"): urls.append(url) elif msg_type == "TIMSoundElem": if url := msg_content.get("Url"): urls.append(url) elif msg_type == "TIMVideoFileElem": if url := msg_content.get("VideoUrl"): urls.append(url) if thumb_url := msg_content.get("ThumbUrl"): urls.append(thumb_url) elif msg_type == "TIMFileElem": if url := msg_content.get("Url"): urls.append(url) return urls def resolve_event_type(raw_event: dict) -> str: cmd = raw_event.get("CallbackCommand", "") if "SendMsg" in cmd: return "message" if "WithDraw" in cmd: return "withdraw" if "StateChange" in cmd: return "state" if "Report" in cmd: return "read_report" if "Group" in cmd: return "group_event" return "unknown"