108 lines
4.6 KiB
Python
108 lines
4.6 KiB
Python
"""纯 Python 的文本会话解析;不依赖 Windows 或原生 DLL。"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
WXID = re.compile(r"^[A-Za-z0-9_.@-]{1,127}$")
|
||
SYSTEM_PEERS = frozenset({"weixin", "fmessage", "newsapp", "floatbottle"})
|
||
GROUP_CONTROL_COMMANDS = frozenset({"/help", "成语接龙", "合并接龙", "退出接龙"})
|
||
FOUR_CHINESE = re.compile(r"^[\u3400-\u4dbf\u4e00-\u9fff]{4}$")
|
||
|
||
|
||
def own_group_command(text):
|
||
value = text.strip()
|
||
return bool(value in GROUP_CONTROL_COMMANDS or FOUR_CHINESE.fullmatch(value) or
|
||
value.split(maxsplit=1)[:1] in (["/绑定"], ["/总结"], ["/总结群聊"], ["/切换人格"], ["/清空"]))
|
||
|
||
|
||
def validate_target(target):
|
||
if not isinstance(target, str) or not WXID.fullmatch(target):
|
||
raise ValueError("目标应为微信 ID 或群 ID,长度最多 127 个 ASCII 字符")
|
||
return target
|
||
|
||
|
||
def validate_mentions(room, members):
|
||
validate_target(room)
|
||
if not room.endswith("@chatroom"):
|
||
raise ValueError("艾特消息的目标必须是群 ID")
|
||
if not isinstance(members, (list, tuple)) or not 1 <= len(members) <= 8:
|
||
raise ValueError("at_list 应包含 1–8 个成员微信 ID")
|
||
from .account import validate_account
|
||
result = tuple(validate_account(member) for member in members)
|
||
if len(set(result)) != len(result):
|
||
raise ValueError("at_list 不能重复")
|
||
return result
|
||
|
||
|
||
class TextRouter:
|
||
"""先识别本账号,再区分会话与作者,避免回复自己同步过来的消息。"""
|
||
|
||
def __init__(self, *, include_self_groups=False):
|
||
self.include_self_groups = include_self_groups
|
||
self.self_wxid = ""
|
||
self.unidentified = 0
|
||
self.unparsed_groups = 0
|
||
self.ignored_self = 0
|
||
|
||
def set_account(self, account):
|
||
from .account import validate_account
|
||
validate_account(account)
|
||
if self.self_wxid and self.self_wxid != account:
|
||
raise RuntimeError("当前登录账号发生变化,需要关闭旧连接后重新识别")
|
||
self.self_wxid = account
|
||
|
||
def convert(self, sender, receiver, text, *, sequence=0, thread_id=0):
|
||
validate_target(sender)
|
||
validate_target(receiver)
|
||
filehelper = "filehelper" in (sender, receiver)
|
||
incoming_group = sender.endswith("@chatroom")
|
||
# 文件传输助手的另一端,以及收到群消息的接收方,都是本账号。
|
||
identified = (receiver if sender == "filehelper" else sender) if filehelper else (
|
||
receiver if incoming_group else "")
|
||
if identified:
|
||
validate_target(identified)
|
||
if identified == "filehelper" or identified.endswith("@chatroom"):
|
||
return None
|
||
self.set_account(identified)
|
||
if not self.self_wxid:
|
||
self.unidentified += 1
|
||
return None
|
||
|
||
is_self = sender == self.self_wxid
|
||
if filehelper:
|
||
chat, author, room, body = "filehelper", sender, "", text
|
||
elif incoming_group:
|
||
if receiver != self.self_wxid:
|
||
return None
|
||
author, separator, body = text.partition(":\n")
|
||
# Sync AddMsg 的群文本包含“成员 ID:\n正文”;不猜测缺失的作者。
|
||
if not separator or not WXID.fullmatch(author) or author.endswith("@chatroom"):
|
||
self.unparsed_groups += 1
|
||
return None
|
||
chat = room = sender
|
||
is_self = author == self.self_wxid
|
||
elif receiver.endswith("@chatroom"):
|
||
if not is_self:
|
||
return None
|
||
chat = room = receiver
|
||
author, body = sender, text
|
||
elif receiver == self.self_wxid:
|
||
chat, author, room, body = sender, sender, "", text
|
||
elif is_self:
|
||
chat, author, room, body = receiver, sender, "", text
|
||
else:
|
||
return None
|
||
own_group_input = bool(room and own_group_command(body))
|
||
if not filehelper and is_self and not own_group_input and not (room and self.include_self_groups):
|
||
self.ignored_self += 1
|
||
return None
|
||
if chat.startswith("gh_") or chat in SYSTEM_PEERS:
|
||
return None
|
||
return {"msg": body, "content": body, "from_wxid": author if room else chat,
|
||
"room_wxid": room, "to_wxid": receiver, "wx_type": 1,
|
||
"chat_id": chat, "sender_id": author, "is_self": is_self,
|
||
"self_wxid": self.self_wxid, "native_sender": sender,
|
||
"native_receiver": receiver, "sequence": sequence,
|
||
"dispatch_to_plugins": not (room and is_self and not own_group_input),
|
||
"native_thread_id": thread_id}
|