ForcePilot/backend/package/yuxi/channel/extensions/tencent_im/webhook.py

140 lines
4.6 KiB
Python
Raw Normal View History

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"