From c4b5f7fbaee14145be3720ba0fb79a2581c181a1 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 16 Mar 2026 11:05:45 +0800 Subject: [PATCH] refactor: remove unavailable channels --- .github/ISSUE_TEMPLATE/1.bug.yml | 2 - .gitignore | 2 - app.py | 7 - channel/channel_factory.py | 14 +- channel/chat_message.py | 2 +- channel/wechat/wcf_channel.py | 179 -------- channel/wechat/wcf_message.py | 58 --- channel/wechat/wechat_channel.py | 309 ------------- channel/wechat/wechat_message.py | 124 ------ channel/wechat/wechaty_channel.py | 129 ------ channel/wechat/wechaty_message.py | 89 ---- channel/wechatmp/README.md | 2 +- channel/wework/run.py | 17 - channel/wework/wework_channel.py | 326 -------------- channel/wework/wework_message.py | 227 ---------- config.py | 8 +- lib/itchat/LICENSE | 9 - lib/itchat/__init__.py | 96 ----- lib/itchat/async_components/__init__.py | 12 - lib/itchat/async_components/contact.py | 488 --------------------- lib/itchat/async_components/hotreload.py | 102 ----- lib/itchat/async_components/login.py | 422 ------------------ lib/itchat/async_components/messages.py | 527 ---------------------- lib/itchat/async_components/register.py | 106 ----- lib/itchat/components/__init__.py | 12 - lib/itchat/components/contact.py | 519 ---------------------- lib/itchat/components/hotreload.py | 102 ----- lib/itchat/components/login.py | 418 ------------------ lib/itchat/components/messages.py | 528 ----------------------- lib/itchat/components/register.py | 106 ----- lib/itchat/config.py | 17 - lib/itchat/content.py | 14 - lib/itchat/core.py | 456 -------------------- lib/itchat/log.py | 36 -- lib/itchat/returnvalues.py | 67 --- lib/itchat/storage/__init__.py | 117 ----- lib/itchat/storage/messagequeue.py | 32 -- lib/itchat/storage/templates.py | 318 -------------- lib/itchat/utils.py | 163 ------- plugins/README.md | 2 +- plugins/godcmd/godcmd.py | 4 +- plugins/keyword/keyword.py | 1 - plugins/tool/tool.py | 5 - requirements.txt | 5 - voice/audio_convert.py | 2 +- 45 files changed, 8 insertions(+), 6173 deletions(-) delete mode 100644 channel/wechat/wcf_channel.py delete mode 100644 channel/wechat/wcf_message.py delete mode 100644 channel/wechat/wechat_channel.py delete mode 100644 channel/wechat/wechat_message.py delete mode 100644 channel/wechat/wechaty_channel.py delete mode 100644 channel/wechat/wechaty_message.py delete mode 100644 channel/wework/run.py delete mode 100644 channel/wework/wework_channel.py delete mode 100644 channel/wework/wework_message.py delete mode 100644 lib/itchat/LICENSE delete mode 100644 lib/itchat/__init__.py delete mode 100644 lib/itchat/async_components/__init__.py delete mode 100644 lib/itchat/async_components/contact.py delete mode 100644 lib/itchat/async_components/hotreload.py delete mode 100644 lib/itchat/async_components/login.py delete mode 100644 lib/itchat/async_components/messages.py delete mode 100644 lib/itchat/async_components/register.py delete mode 100644 lib/itchat/components/__init__.py delete mode 100644 lib/itchat/components/contact.py delete mode 100644 lib/itchat/components/hotreload.py delete mode 100644 lib/itchat/components/login.py delete mode 100644 lib/itchat/components/messages.py delete mode 100644 lib/itchat/components/register.py delete mode 100644 lib/itchat/config.py delete mode 100644 lib/itchat/content.py delete mode 100644 lib/itchat/core.py delete mode 100644 lib/itchat/log.py delete mode 100644 lib/itchat/returnvalues.py delete mode 100644 lib/itchat/storage/__init__.py delete mode 100644 lib/itchat/storage/messagequeue.py delete mode 100644 lib/itchat/storage/templates.py delete mode 100644 lib/itchat/utils.py diff --git a/.github/ISSUE_TEMPLATE/1.bug.yml b/.github/ISSUE_TEMPLATE/1.bug.yml index 2f762c0..a4f1092 100644 --- a/.github/ISSUE_TEMPLATE/1.bug.yml +++ b/.github/ISSUE_TEMPLATE/1.bug.yml @@ -79,8 +79,6 @@ body: description: | 请确保你正确配置了该`channel`所需的配置项,所有可选的配置项都写在了[该文件中](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py),请将所需配置项填写在根目录下的`config.json`文件中。 options: - - wx(个人微信, itchat) - - wxy(个人微信, wechaty) - wechatmp(公众号, 订阅号) - wechatmp_service(公众号, 服务号) - terminal diff --git a/.gitignore b/.gitignore index db3ba55..e217c97 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .vscode .venv .vs -.wechaty/ __pycache__/ venv* *.pyc @@ -13,7 +12,6 @@ QR.png nohup.out tmp plugins.json -itchat.pkl *.log logs/ workspace diff --git a/app.py b/app.py index 073aae3..f7b7eed 100644 --- a/app.py +++ b/app.py @@ -221,14 +221,10 @@ def _clear_singleton_cache(channel_name: str): a new instance can be created with updated config. """ cls_map = { - "wx": "channel.wechat.wechat_channel.WechatChannel", - "wxy": "channel.wechat.wechaty_channel.WechatyChannel", - "wcf": "channel.wechat.wcf_channel.WechatfChannel", "web": "channel.web.web_channel.WebChannel", "wechatmp": "channel.wechatmp.wechatmp_channel.WechatMPChannel", "wechatmp_service": "channel.wechatmp.wechatmp_channel.WechatMPChannel", "wechatcom_app": "channel.wechatcom.wechatcomapp_channel.WechatComAppChannel", - "wework": "channel.wework.wework_channel.WeworkChannel", const.FEISHU: "channel.feishu.feishu_channel.FeiShuChanel", const.DINGTALK: "channel.dingtalk.dingtalk_channel.DingTalkChanel", } @@ -288,9 +284,6 @@ def run(): if not channel_names: channel_names = ["web"] - if "wxy" in channel_names: - os.environ["WECHATY_LOG"] = "warn" - # Auto-start web console unless explicitly disabled web_console_enabled = conf().get("web_console", True) if web_console_enabled and "web" not in channel_names: diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 50e1756..7c6e30f 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -12,16 +12,7 @@ def create_channel(channel_type) -> Channel: :return: channel instance """ ch = Channel() - if channel_type == "wx": - from channel.wechat.wechat_channel import WechatChannel - ch = WechatChannel() - elif channel_type == "wxy": - from channel.wechat.wechaty_channel import WechatyChannel - ch = WechatyChannel() - elif channel_type == "wcf": - from channel.wechat.wcf_channel import WechatfChannel - ch = WechatfChannel() - elif channel_type == "terminal": + if channel_type == "terminal": from channel.terminal.terminal_channel import TerminalChannel ch = TerminalChannel() elif channel_type == 'web': @@ -36,9 +27,6 @@ def create_channel(channel_type) -> Channel: elif channel_type == "wechatcom_app": from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel ch = WechatComAppChannel() - elif channel_type == "wework": - from channel.wework.wework_channel import WeworkChannel - ch = WeworkChannel() elif channel_type == const.FEISHU: from channel.feishu.feishu_channel import FeiShuChanel ch = FeiShuChanel() diff --git a/channel/chat_message.py b/channel/chat_message.py index ac0e5c2..d7ea52d 100644 --- a/channel/chat_message.py +++ b/channel/chat_message.py @@ -1,5 +1,5 @@ """ -本类表示聊天消息,用于对itchat和wechaty的消息进行统一的封装。 +Unified chat message class for different channel implementations. 填好必填项(群聊6个,非群聊8个),即可接入ChatChannel,并支持插件,参考TerminalChannel diff --git a/channel/wechat/wcf_channel.py b/channel/wechat/wcf_channel.py deleted file mode 100644 index af5468b..0000000 --- a/channel/wechat/wcf_channel.py +++ /dev/null @@ -1,179 +0,0 @@ -# encoding:utf-8 - -""" -wechat channel -""" - -import io -import json -import os -import threading -import time -from queue import Empty -from typing import Any - -from bridge.context import * -from bridge.reply import * -from channel.chat_channel import ChatChannel -from channel.wechat.wcf_message import WechatfMessage -from common.log import logger -from common.singleton import singleton -from common.utils import * -from config import conf, get_appdata_dir -from wcferry import Wcf, WxMsg - - -@singleton -class WechatfChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [] - - def __init__(self): - super().__init__() - self.NOT_SUPPORT_REPLYTYPE = [] - # 使用字典存储最近消息,用于去重 - self.received_msgs = {} - # 初始化wcferry客户端 - self.wcf = Wcf() - self.wxid = None # 登录后会被设置为当前登录用户的wxid - - def startup(self): - """ - 启动通道 - """ - try: - # wcferry会自动唤起微信并登录 - self.wxid = self.wcf.get_self_wxid() - self.name = self.wcf.get_user_info().get("name") - logger.info(f"微信登录成功,当前用户ID: {self.wxid}, 用户名:{self.name}") - self.contact_cache = ContactCache(self.wcf) - self.contact_cache.update() - # 启动消息接收 - self.wcf.enable_receiving_msg() - # 创建消息处理线程 - t = threading.Thread(target=self._process_messages, name="WeChatThread", daemon=True) - t.start() - - - except Exception as e: - logger.error(f"微信通道启动失败: {e}") - raise e - - def _process_messages(self): - """ - 处理消息队列 - """ - while True: - try: - msg = self.wcf.get_msg() - if msg: - self._handle_message(msg) - except Empty: - continue - except Exception as e: - logger.error(f"处理消息失败: {e}") - continue - - def _handle_message(self, msg: WxMsg): - """ - 处理单条消息 - """ - try: - # 构造消息对象 - cmsg = WechatfMessage(self, msg) - # 消息去重 - if cmsg.msg_id in self.received_msgs: - return - self.received_msgs[cmsg.msg_id] = time.time() - # 清理过期消息ID - self._clean_expired_msgs() - - logger.debug(f"收到消息: {msg}") - context = self._compose_context(cmsg.ctype, cmsg.content, - isgroup=cmsg.is_group, - msg=cmsg) - if context: - self.produce(context) - except Exception as e: - logger.error(f"处理消息失败: {e}") - - def _clean_expired_msgs(self, expire_time: float = 60): - """ - 清理过期的消息ID - """ - now = time.time() - for msg_id in list(self.received_msgs.keys()): - if now - self.received_msgs[msg_id] > expire_time: - del self.received_msgs[msg_id] - - def send(self, reply: Reply, context: Context): - """ - 发送消息 - """ - receiver = context["receiver"] - if not receiver: - logger.error("receiver is empty") - return - - try: - if reply.type == ReplyType.TEXT: - # 处理@信息 - at_list = [] - if context.get("isgroup"): - if context["msg"].actual_user_id: - at_list = [context["msg"].actual_user_id] - at_str = ",".join(at_list) if at_list else "" - self.wcf.send_text(reply.content, receiver, at_str) - - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - self.wcf.send_text(reply.content, receiver) - else: - logger.error(f"暂不支持的消息类型: {reply.type}") - - except Exception as e: - logger.error(f"发送消息失败: {e}") - - def close(self): - """ - 关闭通道 - """ - try: - self.wcf.cleanup() - except Exception as e: - logger.error(f"关闭通道失败: {e}") - - -class ContactCache: - def __init__(self, wcf): - """ - wcf: 一个 wcfferry.client.Wcf 实例 - """ - self.wcf = wcf - self._contact_map = {} # 形如 {wxid: {完整联系人信息}} - - def update(self): - """ - 更新缓存:调用 get_contacts(), - 再把 wcf.contacts 构建成 {wxid: {完整信息}} 的字典 - """ - self.wcf.get_contacts() - self._contact_map.clear() - for item in self.wcf.contacts: - wxid = item.get('wxid') - if wxid: # 确保有 wxid 字段 - self._contact_map[wxid] = item - - def get_contact(self, wxid: str) -> dict: - """ - 返回该 wxid 对应的完整联系人 dict, - 如果没找到就返回 None - """ - return self._contact_map.get(wxid) - - def get_name_by_wxid(self, wxid: str) -> str: - """ - 通过wxid,获取成员/群名称 - """ - contact = self.get_contact(wxid) - if contact: - return contact.get('name', '') - return '' \ No newline at end of file diff --git a/channel/wechat/wcf_message.py b/channel/wechat/wcf_message.py deleted file mode 100644 index 827a578..0000000 --- a/channel/wechat/wcf_message.py +++ /dev/null @@ -1,58 +0,0 @@ -# encoding:utf-8 - -""" -wechat channel message -""" - -from bridge.context import ContextType -from channel.chat_message import ChatMessage -from common.log import logger -from wcferry import WxMsg - - -class WechatfMessage(ChatMessage): - """ - 微信消息封装类 - """ - - def __init__(self, channel, wcf_msg: WxMsg, is_group=False): - """ - 初始化消息对象 - :param wcf_msg: wcferry消息对象 - :param is_group: 是否是群消息 - """ - super().__init__(wcf_msg) - self.msg_id = wcf_msg.id - self.create_time = wcf_msg.ts # 使用消息时间戳 - self.is_group = is_group or wcf_msg._is_group - self.wxid = channel.wxid - self.name = channel.name - - # 解析消息类型 - if wcf_msg.is_text(): - self.ctype = ContextType.TEXT - self.content = wcf_msg.content - else: - raise NotImplementedError(f"Unsupported message type: {wcf_msg.type}") - - # 设置发送者和接收者信息 - self.from_user_id = self.wxid if wcf_msg.sender == self.wxid else wcf_msg.sender - self.from_user_nickname = self.name if wcf_msg.sender == self.wxid else channel.contact_cache.get_name_by_wxid(wcf_msg.sender) - self.to_user_id = self.wxid - self.to_user_nickname = self.name - self.other_user_id = wcf_msg.sender - self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender) - - # 群消息特殊处理 - if self.is_group: - self.other_user_id = wcf_msg.roomid - self.other_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.roomid) - self.actual_user_id = wcf_msg.sender - self.actual_user_nickname = channel.wcf.get_alias_in_chatroom(wcf_msg.sender, wcf_msg.roomid) - if not self.actual_user_nickname: # 群聊获取不到企微号成员昵称,这里尝试从联系人缓存去获取 - self.actual_user_nickname = channel.contact_cache.get_name_by_wxid(wcf_msg.sender) - self.room_id = wcf_msg.roomid - self.is_at = wcf_msg.is_at(self.wxid) # 是否被@当前登录用户 - - # 判断是否是自己发送的消息 - self.my_msg = wcf_msg.from_self() diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py deleted file mode 100644 index 19db71d..0000000 --- a/channel/wechat/wechat_channel.py +++ /dev/null @@ -1,309 +0,0 @@ -# encoding:utf-8 - -""" -wechat channel -""" - -import io -import json -import os -import threading -import time -import requests - -from bridge.context import * -from bridge.reply import * -from channel.chat_channel import ChatChannel -from channel import chat_channel -from channel.wechat.wechat_message import * -from common.expired_dict import ExpiredDict -from common.log import logger -from common.singleton import singleton -from common.time_check import time_checker -from common.utils import convert_webp_to_png, remove_markdown_symbol -from config import conf, get_appdata_dir -from lib import itchat -from lib.itchat.content import * - - -@itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING]) -def handler_single_msg(msg): - try: - cmsg = WechatMessage(msg, False) - except NotImplementedError as e: - logger.debug("[WX]single message {} skipped: {}".format(msg["MsgId"], e)) - return None - WechatChannel().handle_single(cmsg) - return None - - -@itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING], isGroupChat=True) -def handler_group_msg(msg): - try: - cmsg = WechatMessage(msg, True) - except NotImplementedError as e: - logger.debug("[WX]group message {} skipped: {}".format(msg["MsgId"], e)) - return None - WechatChannel().handle_group(cmsg) - return None - - -def _check(func): - def wrapper(self, cmsg: ChatMessage): - msgId = cmsg.msg_id - if msgId in self.receivedMsgs: - logger.info("Wechat message {} already received, ignore".format(msgId)) - return - self.receivedMsgs[msgId] = True - create_time = cmsg.create_time # 消息时间戳 - if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息 - logger.debug("[WX]history message {} skipped".format(msgId)) - return - if cmsg.my_msg and not cmsg.is_group: - logger.debug("[WX]my message {} skipped".format(msgId)) - return - return func(self, cmsg) - - return wrapper - - -# 可用的二维码生成接口 -# https://api.qrserver.com/v1/create-qr-code/?size=400×400&data=https://www.abc.com -# https://api.isoyu.com/qr/?m=1&e=L&p=20&url=https://www.abc.com -def qrCallback(uuid, status, qrcode): - # logger.debug("qrCallback: {} {}".format(uuid,status)) - if status == "0": - try: - from PIL import Image - - img = Image.open(io.BytesIO(qrcode)) - _thread = threading.Thread(target=img.show, args=("QRCode",)) - _thread.setDaemon(True) - _thread.start() - except Exception as e: - pass - - import qrcode - - url = f"https://login.weixin.qq.com/l/{uuid}" - - qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url) - qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url) - qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url) - qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url) - print("You can also scan QRCode in any website below:") - print(qr_api3) - print(qr_api4) - print(qr_api2) - print(qr_api1) - _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1]) - qr = qrcode.QRCode(border=1) - qr.add_data(url) - qr.make(fit=True) - try: - qr.print_ascii(invert=True) - except UnicodeEncodeError: - print("ASCII QR code printing failed due to encoding issues.") - - -@singleton -class WechatChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [] - - def __init__(self): - super().__init__() - self.receivedMsgs = ExpiredDict(conf().get("expires_in_seconds", 3600)) - self.auto_login_times = 0 - - def startup(self): - try: - time.sleep(3) - logger.error("""[WechatChannel] 当前channel暂不可用,目前支持的channel有: - 1. terminal: 终端 - 2. wechatmp: 个人公众号 - 3. wechatmp_service: 企业公众号 - 4. wechatcom_app: 企微自建应用 - 5. dingtalk: 钉钉 - 6. feishu: 飞书 - 7. web: 网页 - 8. wcf: wechat (需Windows环境,参考 https://github.com/zhayujie/chatgpt-on-wechat/pull/2562 ) - 可修改 config.json 配置文件的 channel_type 字段进行切换""") - - # itchat.instance.receivingRetryCount = 600 # 修改断线超时时间 - # # login by scan QRCode - # hotReload = conf().get("hot_reload", False) - # status_path = os.path.join(get_appdata_dir(), "itchat.pkl") - # itchat.auto_login( - # enableCmdQR=2, - # hotReload=hotReload, - # statusStorageDir=status_path, - # qrCallback=qrCallback, - # exitCallback=self.exitCallback, - # loginCallback=self.loginCallback - # ) - # self.user_id = itchat.instance.storageClass.userName - # self.name = itchat.instance.storageClass.nickName - # logger.info("Wechat login success, user_id: {}, nickname: {}".format(self.user_id, self.name)) - # # start message listener - # itchat.run() - except Exception as e: - logger.exception(e) - - def exitCallback(self): - try: - from common.cloud_client import chat_client - if chat_client.client_id and conf().get("use_linkai"): - _send_logout() - time.sleep(2) - self.auto_login_times += 1 - if self.auto_login_times < 100: - chat_channel.handler_pool._shutdown = False - self.startup() - except Exception as e: - pass - - def loginCallback(self): - logger.debug("Login success") - _send_login_success() - - # handle_* 系列函数处理收到的消息后构造Context,然后传入produce函数中处理Context和发送回复 - # Context包含了消息的所有信息,包括以下属性 - # type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE - # content 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是IMAGE_CREATE类型,content就是图片生成命令 - # kwargs 附加参数字典,包含以下的key: - # session_id: 会话id - # isgroup: 是否是群聊 - # receiver: 需要回复的对象 - # msg: ChatMessage消息对象 - # origin_ctype: 原始消息类型,语音转文字后,私聊时如果匹配前缀失败,会根据初始消息是否是语音来放宽触发规则 - # desire_rtype: 希望回复类型,默认是文本回复,设置为ReplyType.VOICE是语音回复 - @time_checker - @_check - def handle_single(self, cmsg: ChatMessage): - # filter system message - if cmsg.other_user_id in ["weixin"]: - return - if cmsg.ctype == ContextType.VOICE: - if conf().get("speech_recognition") != True: - return - logger.debug("[WX]receive voice msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.IMAGE: - logger.debug("[WX]receive image msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.PATPAT: - logger.debug("[WX]receive patpat msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.TEXT: - logger.debug("[WX]receive text msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg)) - else: - logger.debug("[WX]receive msg: {}, cmsg={}".format(cmsg.content, cmsg)) - context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg) - if context: - self.produce(context) - - @time_checker - @_check - def handle_group(self, cmsg: ChatMessage): - if cmsg.ctype == ContextType.VOICE: - if conf().get("group_speech_recognition") != True: - return - logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.IMAGE: - logger.debug("[WX]receive image for group msg: {}".format(cmsg.content)) - elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND, ContextType.EXIT_GROUP]: - logger.debug("[WX]receive note msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.TEXT: - # logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg)) - pass - elif cmsg.ctype == ContextType.FILE: - logger.debug(f"[WX]receive attachment msg, file_name={cmsg.content}") - else: - logger.debug("[WX]receive group msg: {}".format(cmsg.content)) - context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg, no_need_at=conf().get("no_need_at", False)) - if context: - self.produce(context) - - # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息 - def send(self, reply: Reply, context: Context): - receiver = context["receiver"] - if reply.type == ReplyType.TEXT: - reply.content = remove_markdown_symbol(reply.content) - itchat.send(reply.content, toUserName=receiver) - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - reply.content = remove_markdown_symbol(reply.content) - itchat.send(reply.content, toUserName=receiver) - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.VOICE: - itchat.send_file(reply.content, toUserName=receiver) - logger.info("[WX] sendFile={}, receiver={}".format(reply.content, receiver)) - elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 - img_url = reply.content - logger.debug(f"[WX] start download image, img_url={img_url}") - pic_res = requests.get(img_url, stream=True) - image_storage = io.BytesIO() - size = 0 - for block in pic_res.iter_content(1024): - size += len(block) - image_storage.write(block) - logger.info(f"[WX] download image success, size={size}, img_url={img_url}") - image_storage.seek(0) - if ".webp" in img_url: - try: - image_storage = convert_webp_to_png(image_storage) - except Exception as e: - logger.error(f"Failed to convert image: {e}") - return - itchat.send_image(image_storage, toUserName=receiver) - logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver)) - elif reply.type == ReplyType.IMAGE: # 从文件读取图片 - image_storage = reply.content - image_storage.seek(0) - itchat.send_image(image_storage, toUserName=receiver) - logger.info("[WX] sendImage, receiver={}".format(receiver)) - elif reply.type == ReplyType.FILE: # 新增文件回复类型 - file_storage = reply.content - itchat.send_file(file_storage, toUserName=receiver) - logger.info("[WX] sendFile, receiver={}".format(receiver)) - elif reply.type == ReplyType.VIDEO: # 新增视频回复类型 - video_storage = reply.content - itchat.send_video(video_storage, toUserName=receiver) - logger.info("[WX] sendFile, receiver={}".format(receiver)) - elif reply.type == ReplyType.VIDEO_URL: # 新增视频URL回复类型 - video_url = reply.content - logger.debug(f"[WX] start download video, video_url={video_url}") - video_res = requests.get(video_url, stream=True) - video_storage = io.BytesIO() - size = 0 - for block in video_res.iter_content(1024): - size += len(block) - video_storage.write(block) - logger.info(f"[WX] download video success, size={size}, video_url={video_url}") - video_storage.seek(0) - itchat.send_video(video_storage, toUserName=receiver) - logger.info("[WX] sendVideo url={}, receiver={}".format(video_url, receiver)) - -def _send_login_success(): - try: - from common.cloud_client import chat_client - if chat_client.client_id: - chat_client.send_login_success() - except Exception as e: - pass - - -def _send_logout(): - try: - from common.cloud_client import chat_client - if chat_client.client_id: - chat_client.send_logout() - except Exception as e: - pass - - -def _send_qr_code(qrcode_list: list): - try: - from common.cloud_client import chat_client - if chat_client.client_id: - chat_client.send_qrcode(qrcode_list) - except Exception as e: - pass - diff --git a/channel/wechat/wechat_message.py b/channel/wechat/wechat_message.py deleted file mode 100644 index 5c8065c..0000000 --- a/channel/wechat/wechat_message.py +++ /dev/null @@ -1,124 +0,0 @@ -import re - -from bridge.context import ContextType -from channel.chat_message import ChatMessage -from common.log import logger -from common.tmp_dir import TmpDir -from lib import itchat -from lib.itchat.content import * - -class WechatMessage(ChatMessage): - def __init__(self, itchat_msg, is_group=False): - super().__init__(itchat_msg) - self.msg_id = itchat_msg["MsgId"] - self.create_time = itchat_msg["CreateTime"] - self.is_group = is_group - - notes_join_group = ["加入群聊", "加入了群聊", "invited", "joined"] # 可通过添加对应语言的加入群聊通知中的关键词适配更多 - notes_bot_join_group = ["邀请你", "invited you", "You've joined", "你通过扫描"] - notes_exit_group = ["移出了群聊", "removed"] # 可通过添加对应语言的踢出群聊通知中的关键词适配更多 - notes_patpat = ["拍了拍我", "tickled my", "tickled me"] # 可通过添加对应语言的拍一拍通知中的关键词适配更多 - - if itchat_msg["Type"] == TEXT: - self.ctype = ContextType.TEXT - self.content = itchat_msg["Text"] - elif itchat_msg["Type"] == VOICE: - self.ctype = ContextType.VOICE - self.content = TmpDir().path() + itchat_msg["FileName"] # content直接存临时目录路径 - self._prepare_fn = lambda: itchat_msg.download(self.content) - elif itchat_msg["Type"] == PICTURE and itchat_msg["MsgType"] == 3: - self.ctype = ContextType.IMAGE - self.content = TmpDir().path() + itchat_msg["FileName"] # content直接存临时目录路径 - self._prepare_fn = lambda: itchat_msg.download(self.content) - elif itchat_msg["Type"] == NOTE and itchat_msg["MsgType"] == 10000: - if is_group: - if any(note_bot_join_group in itchat_msg["Content"] for note_bot_join_group in notes_bot_join_group): # 邀请机器人加入群聊 - logger.warn("机器人加入群聊消息,不处理~") - pass - elif any(note_join_group in itchat_msg["Content"] for note_join_group in notes_join_group): # 若有任何在notes_join_group列表中的字符串出现在NOTE中 - # 这里只能得到nickname, actual_user_id还是机器人的id - if "加入群聊" not in itchat_msg["Content"]: - self.ctype = ContextType.JOIN_GROUP - self.content = itchat_msg["Content"] - if "invited" in itchat_msg["Content"]: # 匹配英文信息 - self.actual_user_nickname = re.findall(r'invited\s+(.+?)\s+to\s+the\s+group\s+chat', itchat_msg["Content"])[0] - elif "joined" in itchat_msg["Content"]: # 匹配通过二维码加入的英文信息 - self.actual_user_nickname = re.findall(r'"(.*?)" joined the group chat via the QR Code shared by', itchat_msg["Content"])[0] - elif "加入了群聊" in itchat_msg["Content"]: - self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[-1] - elif "加入群聊" in itchat_msg["Content"]: - self.ctype = ContextType.JOIN_GROUP - self.content = itchat_msg["Content"] - self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0] - - elif any(note_exit_group in itchat_msg["Content"] for note_exit_group in notes_exit_group): # 若有任何在notes_exit_group列表中的字符串出现在NOTE中 - self.ctype = ContextType.EXIT_GROUP - self.content = itchat_msg["Content"] - self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0] - - elif any(note_patpat in itchat_msg["Content"] for note_patpat in notes_patpat): # 若有任何在notes_patpat列表中的字符串出现在NOTE中: - self.ctype = ContextType.PATPAT - self.content = itchat_msg["Content"] - if "拍了拍我" in itchat_msg["Content"]: # 识别中文 - self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0] - elif "tickled my" in itchat_msg["Content"] or "tickled me" in itchat_msg["Content"]: - self.actual_user_nickname = re.findall(r'^(.*?)(?:tickled my|tickled me)', itchat_msg["Content"])[0] - else: - raise NotImplementedError("Unsupported note message: " + itchat_msg["Content"]) - - elif "你已添加了" in itchat_msg["Content"]: #通过好友请求 - self.ctype = ContextType.ACCEPT_FRIEND - self.content = itchat_msg["Content"] - elif any(note_patpat in itchat_msg["Content"] for note_patpat in notes_patpat): # 若有任何在notes_patpat列表中的字符串出现在NOTE中: - self.ctype = ContextType.PATPAT - self.content = itchat_msg["Content"] - else: - raise NotImplementedError("Unsupported note message: " + itchat_msg["Content"]) - elif itchat_msg["Type"] == ATTACHMENT: - self.ctype = ContextType.FILE - self.content = TmpDir().path() + itchat_msg["FileName"] # content直接存临时目录路径 - self._prepare_fn = lambda: itchat_msg.download(self.content) - elif itchat_msg["Type"] == SHARING: - self.ctype = ContextType.SHARING - self.content = itchat_msg.get("Url") - - else: - raise NotImplementedError("Unsupported message type: Type:{} MsgType:{}".format(itchat_msg["Type"], itchat_msg["MsgType"])) - - self.from_user_id = itchat_msg["FromUserName"] - self.to_user_id = itchat_msg["ToUserName"] - - user_id = itchat.instance.storageClass.userName - nickname = itchat.instance.storageClass.nickName - - # 虽然from_user_id和to_user_id用的少,但是为了保持一致性,还是要填充一下 - # 以下很繁琐,一句话总结:能填的都填了。 - if self.from_user_id == user_id: - self.from_user_nickname = nickname - if self.to_user_id == user_id: - self.to_user_nickname = nickname - try: # 陌生人时候, User字段可能不存在 - # my_msg 为True是表示是自己发送的消息 - self.my_msg = itchat_msg["ToUserName"] == itchat_msg["User"]["UserName"] and \ - itchat_msg["ToUserName"] != itchat_msg["FromUserName"] - self.other_user_id = itchat_msg["User"]["UserName"] - self.other_user_nickname = itchat_msg["User"]["NickName"] - if self.other_user_id == self.from_user_id: - self.from_user_nickname = self.other_user_nickname - if self.other_user_id == self.to_user_id: - self.to_user_nickname = self.other_user_nickname - if itchat_msg["User"].get("Self"): - # 自身的展示名,当设置了群昵称时,该字段表示群昵称 - self.self_display_name = itchat_msg["User"].get("Self").get("DisplayName") - except KeyError as e: # 处理偶尔没有对方信息的情况 - logger.warn("[WX]get other_user_id failed: " + str(e)) - if self.from_user_id == user_id: - self.other_user_id = self.to_user_id - else: - self.other_user_id = self.from_user_id - - if self.is_group: - self.is_at = itchat_msg["IsAt"] - self.actual_user_id = itchat_msg["ActualUserName"] - if self.ctype not in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.EXIT_GROUP]: - self.actual_user_nickname = itchat_msg["ActualNickName"] diff --git a/channel/wechat/wechaty_channel.py b/channel/wechat/wechaty_channel.py deleted file mode 100644 index 051a9cf..0000000 --- a/channel/wechat/wechaty_channel.py +++ /dev/null @@ -1,129 +0,0 @@ -# encoding:utf-8 - -""" -wechaty channel -Python Wechaty - https://github.com/wechaty/python-wechaty -""" -import asyncio -import base64 -import os -import time - -from wechaty import Contact, Wechaty -from wechaty.user import Message -from wechaty_puppet import FileBox - -from bridge.context import * -from bridge.context import Context -from bridge.reply import * -from channel.chat_channel import ChatChannel -from channel.wechat.wechaty_message import WechatyMessage -from common.log import logger -from common.singleton import singleton -from config import conf - -try: - from voice.audio_convert import any_to_sil -except Exception as e: - pass - - -@singleton -class WechatyChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [] - - def __init__(self): - super().__init__() - - def startup(self): - config = conf() - token = config.get("wechaty_puppet_service_token") - os.environ["WECHATY_PUPPET_SERVICE_TOKEN"] = token - asyncio.run(self.main()) - - async def main(self): - loop = asyncio.get_event_loop() - # 将asyncio的loop传入处理线程 - self.handler_pool._initializer = lambda: asyncio.set_event_loop(loop) - self.bot = Wechaty() - self.bot.on("login", self.on_login) - self.bot.on("message", self.on_message) - await self.bot.start() - - async def on_login(self, contact: Contact): - self.user_id = contact.contact_id - self.name = contact.name - logger.info("[WX] login user={}".format(contact)) - - # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息 - def send(self, reply: Reply, context: Context): - receiver_id = context["receiver"] - loop = asyncio.get_event_loop() - if context["isgroup"]: - receiver = asyncio.run_coroutine_threadsafe(self.bot.Room.find(receiver_id), loop).result() - else: - receiver = asyncio.run_coroutine_threadsafe(self.bot.Contact.find(receiver_id), loop).result() - msg = None - if reply.type == ReplyType.TEXT: - msg = reply.content - asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result() - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - msg = reply.content - asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result() - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.VOICE: - voiceLength = None - file_path = reply.content - sil_file = os.path.splitext(file_path)[0] + ".sil" - voiceLength = int(any_to_sil(file_path, sil_file)) - if voiceLength >= 60000: - voiceLength = 60000 - logger.info("[WX] voice too long, length={}, set to 60s".format(voiceLength)) - # 发送语音 - t = int(time.time()) - msg = FileBox.from_file(sil_file, name=str(t) + ".sil") - if voiceLength is not None: - msg.metadata["voiceLength"] = voiceLength - asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result() - try: - os.remove(file_path) - if sil_file != file_path: - os.remove(sil_file) - except Exception as e: - pass - logger.info("[WX] sendVoice={}, receiver={}".format(reply.content, receiver)) - elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 - img_url = reply.content - t = int(time.time()) - msg = FileBox.from_url(url=img_url, name=str(t) + ".png") - asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result() - logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver)) - elif reply.type == ReplyType.IMAGE: # 从文件读取图片 - image_storage = reply.content - image_storage.seek(0) - t = int(time.time()) - msg = FileBox.from_base64(base64.b64encode(image_storage.read()), str(t) + ".png") - asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result() - logger.info("[WX] sendImage, receiver={}".format(receiver)) - - async def on_message(self, msg: Message): - """ - listen for message event - """ - try: - cmsg = await WechatyMessage(msg) - except NotImplementedError as e: - logger.debug("[WX] {}".format(e)) - return - except Exception as e: - logger.exception("[WX] {}".format(e)) - return - logger.debug("[WX] message:{}".format(cmsg)) - room = msg.room() # 获取消息来自的群聊. 如果消息不是来自群聊, 则返回None - isgroup = room is not None - ctype = cmsg.ctype - context = self._compose_context(ctype, cmsg.content, isgroup=isgroup, msg=cmsg) - if context: - logger.info("[WX] receiveMsg={}, context={}".format(cmsg, context)) - self.produce(context) diff --git a/channel/wechat/wechaty_message.py b/channel/wechat/wechaty_message.py deleted file mode 100644 index cdb41dd..0000000 --- a/channel/wechat/wechaty_message.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -import re - -from wechaty import MessageType -from wechaty.user import Message - -from bridge.context import ContextType -from channel.chat_message import ChatMessage -from common.log import logger -from common.tmp_dir import TmpDir - - -class aobject(object): - """Inheriting this class allows you to define an async __init__. - - So you can create objects by doing something like `await MyClass(params)` - """ - - async def __new__(cls, *a, **kw): - instance = super().__new__(cls) - await instance.__init__(*a, **kw) - return instance - - async def __init__(self): - pass - - -class WechatyMessage(ChatMessage, aobject): - async def __init__(self, wechaty_msg: Message): - super().__init__(wechaty_msg) - - room = wechaty_msg.room() - - self.msg_id = wechaty_msg.message_id - self.create_time = wechaty_msg.payload.timestamp - self.is_group = room is not None - - if wechaty_msg.type() == MessageType.MESSAGE_TYPE_TEXT: - self.ctype = ContextType.TEXT - self.content = wechaty_msg.text() - elif wechaty_msg.type() == MessageType.MESSAGE_TYPE_AUDIO: - self.ctype = ContextType.VOICE - voice_file = await wechaty_msg.to_file_box() - self.content = TmpDir().path() + voice_file.name # content直接存临时目录路径 - - def func(): - loop = asyncio.get_event_loop() - asyncio.run_coroutine_threadsafe(voice_file.to_file(self.content), loop).result() - - self._prepare_fn = func - - else: - raise NotImplementedError("Unsupported message type: {}".format(wechaty_msg.type())) - - from_contact = wechaty_msg.talker() # 获取消息的发送者 - self.from_user_id = from_contact.contact_id - self.from_user_nickname = from_contact.name - - # group中的from和to,wechaty跟itchat含义不一样 - # wecahty: from是消息实际发送者, to:所在群 - # itchat: 如果是你发送群消息,from和to是你自己和所在群,如果是别人发群消息,from和to是所在群和你自己 - # 但这个差别不影响逻辑,group中只使用到:1.用from来判断是否是自己发的,2.actual_user_id来判断实际发送用户 - - if self.is_group: - self.to_user_id = room.room_id - self.to_user_nickname = await room.topic() - else: - to_contact = wechaty_msg.to() - self.to_user_id = to_contact.contact_id - self.to_user_nickname = to_contact.name - - if self.is_group or wechaty_msg.is_self(): # 如果是群消息,other_user设置为群,如果是私聊消息,而且自己发的,就设置成对方。 - self.other_user_id = self.to_user_id - self.other_user_nickname = self.to_user_nickname - else: - self.other_user_id = self.from_user_id - self.other_user_nickname = self.from_user_nickname - - if self.is_group: # wechaty群聊中,实际发送用户就是from_user - self.is_at = await wechaty_msg.mention_self() - if not self.is_at: # 有时候复制粘贴的消息,不算做@,但是内容里面会有@xxx,这里做一下兼容 - name = wechaty_msg.wechaty.user_self().name - pattern = f"@{re.escape(name)}(\u2005|\u0020)" - if re.search(pattern, self.content): - logger.debug(f"wechaty message {self.msg_id} include at") - self.is_at = True - - self.actual_user_id = self.from_user_id - self.actual_user_nickname = self.from_user_nickname diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md index 8d753d8..bdae91b 100644 --- a/channel/wechatmp/README.md +++ b/channel/wechatmp/README.md @@ -1,6 +1,6 @@ # 微信公众号channel -鉴于个人微信号在服务器上通过itchat登录有封号风险,这里新增了微信公众号channel,提供无风险的服务。 +微信公众号channel,提供稳定的服务。 目前支持订阅号和服务号两种类型的公众号,它们都支持文本交互,语音和图片输入。其中个人主体的微信订阅号由于无法通过微信认证,存在回复时间限制,每天的图片和声音回复次数也有限制。 ## 使用方法(订阅号,服务号类似) diff --git a/channel/wework/run.py b/channel/wework/run.py deleted file mode 100644 index 1e7d5b3..0000000 --- a/channel/wework/run.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -import time -os.environ['ntwork_LOG'] = "ERROR" -import ntwork - -wework = ntwork.WeWork() - - -def forever(): - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - ntwork.exit_() - os._exit(0) - - diff --git a/channel/wework/wework_channel.py b/channel/wework/wework_channel.py deleted file mode 100644 index 2e898e4..0000000 --- a/channel/wework/wework_channel.py +++ /dev/null @@ -1,326 +0,0 @@ -import io -import os -import random -import tempfile -import threading -os.environ['ntwork_LOG'] = "ERROR" -import ntwork -import requests -import uuid - -from bridge.context import * -from bridge.reply import * -from channel.chat_channel import ChatChannel -from channel.wework.wework_message import * -from channel.wework.wework_message import WeworkMessage -from common.singleton import singleton -from common.log import logger -from common.time_check import time_checker -from common.utils import compress_imgfile, fsize -from config import conf -from channel.wework.run import wework -from channel.wework import run - - -def get_wxid_by_name(room_members, group_wxid, name): - if group_wxid in room_members: - for member in room_members[group_wxid]['member_list']: - if member['room_nickname'] == name or member['username'] == name: - return member['user_id'] - return None # 如果没有找到对应的group_wxid或name,则返回None - - -def download_and_compress_image(url, filename, quality=30): - # 确定保存图片的目录 - directory = os.path.join(os.getcwd(), "tmp") - # 如果目录不存在,则创建目录 - if not os.path.exists(directory): - os.makedirs(directory) - - # 下载图片 - pic_res = requests.get(url, stream=True) - image_storage = io.BytesIO() - for block in pic_res.iter_content(1024): - image_storage.write(block) - - # 检查图片大小并可能进行压缩 - sz = fsize(image_storage) - if sz >= 10 * 1024 * 1024: # 如果图片大于 10 MB - logger.info("[wework] image too large, ready to compress, sz={}".format(sz)) - image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) - logger.info("[wework] image compressed, sz={}".format(fsize(image_storage))) - - # 将内存缓冲区的指针重置到起始位置 - image_storage.seek(0) - - # 读取并保存图片 - from PIL import Image - image = Image.open(image_storage) - image_path = os.path.join(directory, f"{filename}.png") - image.save(image_path, "png") - - return image_path - - -def download_video(url, filename): - # 确定保存视频的目录 - directory = os.path.join(os.getcwd(), "tmp") - # 如果目录不存在,则创建目录 - if not os.path.exists(directory): - os.makedirs(directory) - - # 下载视频 - response = requests.get(url, stream=True) - total_size = 0 - - video_path = os.path.join(directory, f"{filename}.mp4") - - with open(video_path, 'wb') as f: - for block in response.iter_content(1024): - total_size += len(block) - - # 如果视频的总大小超过30MB (30 * 1024 * 1024 bytes),则停止下载并返回 - if total_size > 30 * 1024 * 1024: - logger.info("[WX] Video is larger than 30MB, skipping...") - return None - - f.write(block) - - return video_path - - -def create_message(wework_instance, message, is_group): - logger.debug(f"正在为{'群聊' if is_group else '单聊'}创建 WeworkMessage") - cmsg = WeworkMessage(message, wework=wework_instance, is_group=is_group) - logger.debug(f"cmsg:{cmsg}") - return cmsg - - -def handle_message(cmsg, is_group): - logger.debug(f"准备用 WeworkChannel 处理{'群聊' if is_group else '单聊'}消息") - if is_group: - WeworkChannel().handle_group(cmsg) - else: - WeworkChannel().handle_single(cmsg) - logger.debug(f"已用 WeworkChannel 处理完{'群聊' if is_group else '单聊'}消息") - - -def _check(func): - def wrapper(self, cmsg: ChatMessage): - msgId = cmsg.msg_id - create_time = cmsg.create_time # 消息时间戳 - if create_time is None: - return func(self, cmsg) - if int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息 - logger.debug("[WX]history message {} skipped".format(msgId)) - return - return func(self, cmsg) - - return wrapper - - -@wework.msg_register( - [ntwork.MT_RECV_TEXT_MSG, ntwork.MT_RECV_IMAGE_MSG, 11072, ntwork.MT_RECV_LINK_CARD_MSG,ntwork.MT_RECV_FILE_MSG, ntwork.MT_RECV_VOICE_MSG]) -def all_msg_handler(wework_instance: ntwork.WeWork, message): - logger.debug(f"收到消息: {message}") - if 'data' in message: - # 首先查找conversation_id,如果没有找到,则查找room_conversation_id - conversation_id = message['data'].get('conversation_id', message['data'].get('room_conversation_id')) - if conversation_id is not None: - is_group = "R:" in conversation_id - try: - cmsg = create_message(wework_instance=wework_instance, message=message, is_group=is_group) - except NotImplementedError as e: - logger.error(f"[WX]{message.get('MsgId', 'unknown')} 跳过: {e}") - return None - delay = random.randint(1, 2) - timer = threading.Timer(delay, handle_message, args=(cmsg, is_group)) - timer.start() - else: - logger.debug("消息数据中无 conversation_id") - return None - return None - - -def accept_friend_with_retries(wework_instance, user_id, corp_id): - result = wework_instance.accept_friend(user_id, corp_id) - logger.debug(f'result:{result}') - - -# @wework.msg_register(ntwork.MT_RECV_FRIEND_MSG) -# def friend(wework_instance: ntwork.WeWork, message): -# data = message["data"] -# user_id = data["user_id"] -# corp_id = data["corp_id"] -# logger.info(f"接收到好友请求,消息内容:{data}") -# delay = random.randint(1, 180) -# threading.Timer(delay, accept_friend_with_retries, args=(wework_instance, user_id, corp_id)).start() -# -# return None - - -def get_with_retry(get_func, max_retries=5, delay=5): - retries = 0 - result = None - while retries < max_retries: - result = get_func() - if result: - break - logger.warning(f"获取数据失败,重试第{retries + 1}次······") - retries += 1 - time.sleep(delay) # 等待一段时间后重试 - return result - - -@singleton -class WeworkChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [] - - def __init__(self): - super().__init__() - - def startup(self): - smart = conf().get("wework_smart", True) - wework.open(smart) - logger.info("等待登录······") - wework.wait_login() - login_info = wework.get_login_info() - self.user_id = login_info['user_id'] - self.name = login_info['nickname'] - logger.info(f"登录信息:>>>user_id:{self.user_id}>>>>>>>>name:{self.name}") - logger.info("静默延迟60s,等待客户端刷新数据,请勿进行任何操作······") - time.sleep(60) - contacts = get_with_retry(wework.get_external_contacts) - rooms = get_with_retry(wework.get_rooms) - directory = os.path.join(os.getcwd(), "tmp") - if not contacts or not rooms: - logger.error("获取contacts或rooms失败,程序退出") - ntwork.exit_() - os.exit(0) - if not os.path.exists(directory): - os.makedirs(directory) - # 将contacts保存到json文件中 - with open(os.path.join(directory, 'wework_contacts.json'), 'w', encoding='utf-8') as f: - json.dump(contacts, f, ensure_ascii=False, indent=4) - with open(os.path.join(directory, 'wework_rooms.json'), 'w', encoding='utf-8') as f: - json.dump(rooms, f, ensure_ascii=False, indent=4) - # 创建一个空字典来保存结果 - result = {} - - # 遍历列表中的每个字典 - for room in rooms['room_list']: - # 获取聊天室ID - room_wxid = room['conversation_id'] - - # 获取聊天室成员 - room_members = wework.get_room_members(room_wxid) - - # 将聊天室成员保存到结果字典中 - result[room_wxid] = room_members - - # 将结果保存到json文件中 - with open(os.path.join(directory, 'wework_room_members.json'), 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=4) - logger.info("wework程序初始化完成········") - run.forever() - - @time_checker - @_check - def handle_single(self, cmsg: ChatMessage): - if cmsg.from_user_id == cmsg.to_user_id: - # ignore self reply - return - if cmsg.ctype == ContextType.VOICE: - if not conf().get("speech_recognition"): - return - logger.debug("[WX]receive voice msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.IMAGE: - logger.debug("[WX]receive image msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.PATPAT: - logger.debug("[WX]receive patpat msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.TEXT: - logger.debug("[WX]receive text msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg)) - else: - logger.debug("[WX]receive msg: {}, cmsg={}".format(cmsg.content, cmsg)) - context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg) - if context: - self.produce(context) - - @time_checker - @_check - def handle_group(self, cmsg: ChatMessage): - if cmsg.ctype == ContextType.VOICE: - if not conf().get("speech_recognition"): - return - logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.IMAGE: - logger.debug("[WX]receive image for group msg: {}".format(cmsg.content)) - elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT]: - logger.debug("[WX]receive note msg: {}".format(cmsg.content)) - elif cmsg.ctype == ContextType.TEXT: - pass - else: - logger.debug("[WX]receive group msg: {}".format(cmsg.content)) - context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg) - if context: - self.produce(context) - - # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息 - def send(self, reply: Reply, context: Context): - logger.debug(f"context: {context}") - receiver = context["receiver"] - actual_user_id = context["msg"].actual_user_id - if reply.type == ReplyType.TEXT or reply.type == ReplyType.TEXT_: - match = re.search(r"^@(.*?)\n", reply.content) - logger.debug(f"match: {match}") - if match: - new_content = re.sub(r"^@(.*?)\n", "\n", reply.content) - at_list = [actual_user_id] - logger.debug(f"new_content: {new_content}") - wework.send_room_at_msg(receiver, new_content, at_list) - else: - wework.send_text(receiver, reply.content) - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - wework.send_text(receiver, reply.content) - logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver)) - elif reply.type == ReplyType.IMAGE: # 从文件读取图片 - image_storage = reply.content - image_storage.seek(0) - # Read data from image_storage - data = image_storage.read() - # Create a temporary file - with tempfile.NamedTemporaryFile(delete=False) as temp: - temp_path = temp.name - temp.write(data) - # Send the image - wework.send_image(receiver, temp_path) - logger.info("[WX] sendImage, receiver={}".format(receiver)) - # Remove the temporary file - os.remove(temp_path) - elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 - img_url = reply.content - filename = str(uuid.uuid4()) - - # 调用你的函数,下载图片并保存为本地文件 - image_path = download_and_compress_image(img_url, filename) - - wework.send_image(receiver, file_path=image_path) - logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver)) - elif reply.type == ReplyType.VIDEO_URL: - video_url = reply.content - filename = str(uuid.uuid4()) - video_path = download_video(video_url, filename) - - if video_path is None: - # 如果视频太大,下载可能会被跳过,此时 video_path 将为 None - wework.send_text(receiver, "抱歉,视频太大了!!!") - else: - wework.send_video(receiver, video_path) - logger.info("[WX] sendVideo, receiver={}".format(receiver)) - elif reply.type == ReplyType.VOICE: - current_dir = os.getcwd() - voice_file = reply.content.split("/")[-1] - reply.content = os.path.join(current_dir, "tmp", voice_file) - wework.send_file(receiver, reply.content) - logger.info("[WX] sendFile={}, receiver={}".format(reply.content, receiver)) diff --git a/channel/wework/wework_message.py b/channel/wework/wework_message.py deleted file mode 100644 index 0d9e96e..0000000 --- a/channel/wework/wework_message.py +++ /dev/null @@ -1,227 +0,0 @@ -import datetime -import json -import os -import re -import time -import pilk - -from bridge.context import ContextType -from channel.chat_message import ChatMessage -from common.log import logger -from ntwork.const import send_type - - -def get_with_retry(get_func, max_retries=5, delay=5): - retries = 0 - result = None - while retries < max_retries: - result = get_func() - if result: - break - logger.warning(f"获取数据失败,重试第{retries + 1}次······") - retries += 1 - time.sleep(delay) # 等待一段时间后重试 - return result - - -def get_room_info(wework, conversation_id): - logger.debug(f"传入的 conversation_id: {conversation_id}") - rooms = wework.get_rooms() - if not rooms or 'room_list' not in rooms: - logger.error(f"获取群聊信息失败: {rooms}") - return None - time.sleep(1) - logger.debug(f"获取到的群聊信息: {rooms}") - for room in rooms['room_list']: - if room['conversation_id'] == conversation_id: - return room - return None - - -def cdn_download(wework, message, file_name): - data = message["data"] - aes_key = data["cdn"]["aes_key"] - file_size = data["cdn"]["size"] - - # 获取当前工作目录,然后与文件名拼接得到保存路径 - current_dir = os.getcwd() - save_path = os.path.join(current_dir, "tmp", file_name) - - # 下载保存图片到本地 - if "url" in data["cdn"].keys() and "auth_key" in data["cdn"].keys(): - url = data["cdn"]["url"] - auth_key = data["cdn"]["auth_key"] - # result = wework.wx_cdn_download(url, auth_key, aes_key, file_size, save_path) # ntwork库本身接口有问题,缺失了aes_key这个参数 - """ - 下载wx类型的cdn文件,以https开头 - """ - data = { - 'url': url, - 'auth_key': auth_key, - 'aes_key': aes_key, - 'size': file_size, - 'save_path': save_path - } - result = wework._WeWork__send_sync(send_type.MT_WXCDN_DOWNLOAD_MSG, data) # 直接用wx_cdn_download的接口内部实现来调用 - elif "file_id" in data["cdn"].keys(): - if message["type"] == 11042: - file_type = 2 - elif message["type"] == 11045: - file_type = 5 - file_id = data["cdn"]["file_id"] - result = wework.c2c_cdn_download(file_id, aes_key, file_size, file_type, save_path) - else: - logger.error(f"something is wrong, data: {data}") - return - - # 输出下载结果 - logger.debug(f"result: {result}") - - -def c2c_download_and_convert(wework, message, file_name): - data = message["data"] - aes_key = data["cdn"]["aes_key"] - file_size = data["cdn"]["size"] - file_type = 5 - file_id = data["cdn"]["file_id"] - - current_dir = os.getcwd() - save_path = os.path.join(current_dir, "tmp", file_name) - result = wework.c2c_cdn_download(file_id, aes_key, file_size, file_type, save_path) - logger.debug(result) - - # 在下载完SILK文件之后,立即将其转换为WAV文件 - base_name, _ = os.path.splitext(save_path) - wav_file = base_name + ".wav" - pilk.silk_to_wav(save_path, wav_file, rate=24000) - - # 删除SILK文件 - try: - os.remove(save_path) - except Exception as e: - pass - - -class WeworkMessage(ChatMessage): - def __init__(self, wework_msg, wework, is_group=False): - try: - super().__init__(wework_msg) - self.msg_id = wework_msg['data'].get('conversation_id', wework_msg['data'].get('room_conversation_id')) - # 使用.get()防止 'send_time' 键不存在时抛出错误 - self.create_time = wework_msg['data'].get("send_time") - self.is_group = is_group - self.wework = wework - - if wework_msg["type"] == 11041: # 文本消息类型 - if any(substring in wework_msg['data']['content'] for substring in ("该消息类型暂不能展示", "不支持的消息类型")): - return - self.ctype = ContextType.TEXT - self.content = wework_msg['data']['content'] - elif wework_msg["type"] == 11044: # 语音消息类型,需要缓存文件 - file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + ".silk" - base_name, _ = os.path.splitext(file_name) - file_name_2 = base_name + ".wav" - current_dir = os.getcwd() - self.ctype = ContextType.VOICE - self.content = os.path.join(current_dir, "tmp", file_name_2) - self._prepare_fn = lambda: c2c_download_and_convert(wework, wework_msg, file_name) - elif wework_msg["type"] == 11042: # 图片消息类型,需要下载文件 - file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + ".jpg" - current_dir = os.getcwd() - self.ctype = ContextType.IMAGE - self.content = os.path.join(current_dir, "tmp", file_name) - self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name) - elif wework_msg["type"] == 11045: # 文件消息 - print("文件消息") - print(wework_msg) - file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - file_name = file_name + wework_msg['data']['cdn']['file_name'] - current_dir = os.getcwd() - self.ctype = ContextType.FILE - self.content = os.path.join(current_dir, "tmp", file_name) - self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name) - elif wework_msg["type"] == 11047: # 链接消息 - self.ctype = ContextType.SHARING - self.content = wework_msg['data']['url'] - elif wework_msg["type"] == 11072: # 新成员入群通知 - self.ctype = ContextType.JOIN_GROUP - member_list = wework_msg['data']['member_list'] - self.actual_user_nickname = member_list[0]['name'] - self.actual_user_id = member_list[0]['user_id'] - self.content = f"{self.actual_user_nickname}加入了群聊!" - directory = os.path.join(os.getcwd(), "tmp") - rooms = get_with_retry(wework.get_rooms) - if not rooms: - logger.error("更新群信息失败···") - else: - result = {} - for room in rooms['room_list']: - # 获取聊天室ID - room_wxid = room['conversation_id'] - - # 获取聊天室成员 - room_members = wework.get_room_members(room_wxid) - - # 将聊天室成员保存到结果字典中 - result[room_wxid] = room_members - with open(os.path.join(directory, 'wework_room_members.json'), 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=4) - logger.info("有新成员加入,已自动更新群成员列表缓存!") - else: - raise NotImplementedError( - "Unsupported message type: Type:{} MsgType:{}".format(wework_msg["type"], wework_msg["MsgType"])) - - data = wework_msg['data'] - login_info = self.wework.get_login_info() - logger.debug(f"login_info: {login_info}") - nickname = f"{login_info['username']}({login_info['nickname']})" if login_info['nickname'] else login_info['username'] - user_id = login_info['user_id'] - - sender_id = data.get('sender') - conversation_id = data.get('conversation_id') - sender_name = data.get("sender_name") - - self.from_user_id = user_id if sender_id == user_id else conversation_id - self.from_user_nickname = nickname if sender_id == user_id else sender_name - self.to_user_id = user_id - self.to_user_nickname = nickname - self.other_user_nickname = sender_name - self.other_user_id = conversation_id - - if self.is_group: - conversation_id = data.get('conversation_id') or data.get('room_conversation_id') - self.other_user_id = conversation_id - if conversation_id: - room_info = get_room_info(wework=wework, conversation_id=conversation_id) - self.other_user_nickname = room_info.get('nickname', None) if room_info else None - self.from_user_nickname = room_info.get('nickname', None) if room_info else None - at_list = data.get('at_list', []) - tmp_list = [] - for at in at_list: - tmp_list.append(at['nickname']) - at_list = tmp_list - logger.debug(f"at_list: {at_list}") - logger.debug(f"nickname: {nickname}") - self.is_at = False - if nickname in at_list or login_info['nickname'] in at_list or login_info['username'] in at_list: - self.is_at = True - self.at_list = at_list - - # 检查消息内容是否包含@用户名。处理复制粘贴的消息,这类消息可能不会触发@通知,但内容中可能包含 "@用户名"。 - content = data.get('content', '') - name = nickname - pattern = f"@{re.escape(name)}(\u2005|\u0020)" - if re.search(pattern, content): - logger.debug(f"Wechaty message {self.msg_id} includes at") - self.is_at = True - - if not self.actual_user_id: - self.actual_user_id = data.get("sender") - self.actual_user_nickname = sender_name if self.ctype != ContextType.JOIN_GROUP else self.actual_user_nickname - else: - logger.error("群聊消息中没有找到 conversation_id 或 room_conversation_id") - - logger.debug(f"WeworkMessage has been successfully instantiated with message id: {self.msg_id}") - except Exception as e: - logger.error(f"在 WeworkMessage 的初始化过程中出现错误:{e}") - raise e diff --git a/config.py b/config.py index c808aab..0bba5ba 100644 --- a/config.py +++ b/config.py @@ -95,8 +95,6 @@ available_setting = { "dashscope_api_key": "", # Google Gemini Api Key "gemini_api_key": "", - # wework的通用配置 - "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 # 语音设置 "speech_recognition": True, # 是否开启语音识别 "group_speech_recognition": False, # 是否开启群组语音识别 @@ -118,7 +116,7 @@ available_setting = { # elevenlabs 语音api配置 "xi_api_key": "", # 获取ap的方法可以参考https://docs.elevenlabs.io/api-reference/quick-start/authentication "xi_voice_id": "", # ElevenLabs提供了9种英式、美式等英语发音id,分别是“Adam/Antoni/Arnold/Bella/Domi/Elli/Josh/Rachel/Sam” - # 服务时间限制,目前支持itchat + # 服务时间限制 "chat_time_module": False, # 是否开启服务时间限制 "chat_start_time": "00:00", # 服务开始时间 "chat_stop_time": "24:00", # 服务结束时间 @@ -127,10 +125,6 @@ available_setting = { # baidu翻译api的配置 "baidu_translate_app_id": "", # 百度翻译api的appid "baidu_translate_app_key": "", # 百度翻译api的秘钥 - # itchat的配置 - "hot_reload": False, # 是否开启热重载 - # wechaty的配置 - "wechaty_puppet_service_token": "", # wechaty的token # wechatmp的配置 "wechatmp_token": "", # 微信公众平台的Token "wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443 diff --git a/lib/itchat/LICENSE b/lib/itchat/LICENSE deleted file mode 100644 index ba1a0e2..0000000 --- a/lib/itchat/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -**The MIT License (MIT)** - -Copyright (c) 2017 LittleCoder ([littlecodersh@Github](https://github.com/littlecodersh)) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/itchat/__init__.py b/lib/itchat/__init__.py deleted file mode 100644 index cccbdef..0000000 --- a/lib/itchat/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -from .core import Core -from .config import VERSION, ASYNC_COMPONENTS -from .log import set_logging - -if ASYNC_COMPONENTS: - from .async_components import load_components -else: - from .components import load_components - - -__version__ = VERSION - - -instanceList = [] - -def load_async_itchat() -> Core: - """load async-based itchat instance - - Returns: - Core: the abstract interface of itchat - """ - from .async_components import load_components - load_components(Core) - return Core() - - -def load_sync_itchat() -> Core: - """load sync-based itchat instance - - Returns: - Core: the abstract interface of itchat - """ - from .components import load_components - load_components(Core) - return Core() - - -if ASYNC_COMPONENTS: - instance = load_async_itchat() -else: - instance = load_sync_itchat() - - -instanceList = [instance] - -# I really want to use sys.modules[__name__] = originInstance -# but it makes auto-fill a real mess, so forgive me for my following ** -# actually it toke me less than 30 seconds, god bless Uganda - -# components.login -login = instance.login -get_QRuuid = instance.get_QRuuid -get_QR = instance.get_QR -check_login = instance.check_login -web_init = instance.web_init -show_mobile_login = instance.show_mobile_login -start_receiving = instance.start_receiving -get_msg = instance.get_msg -logout = instance.logout -# components.contact -update_chatroom = instance.update_chatroom -update_friend = instance.update_friend -get_contact = instance.get_contact -get_friends = instance.get_friends -get_chatrooms = instance.get_chatrooms -get_mps = instance.get_mps -set_alias = instance.set_alias -set_pinned = instance.set_pinned -accept_friend = instance.accept_friend -get_head_img = instance.get_head_img -create_chatroom = instance.create_chatroom -set_chatroom_name = instance.set_chatroom_name -delete_member_from_chatroom = instance.delete_member_from_chatroom -add_member_into_chatroom = instance.add_member_into_chatroom -# components.messages -send_raw_msg = instance.send_raw_msg -send_msg = instance.send_msg -upload_file = instance.upload_file -send_file = instance.send_file -send_image = instance.send_image -send_video = instance.send_video -send = instance.send -revoke = instance.revoke -# components.hotreload -dump_login_status = instance.dump_login_status -load_login_status = instance.load_login_status -# components.register -auto_login = instance.auto_login -configured_reply = instance.configured_reply -msg_register = instance.msg_register -run = instance.run -# other functions -search_friends = instance.search_friends -search_chatrooms = instance.search_chatrooms -search_mps = instance.search_mps -set_logging = set_logging diff --git a/lib/itchat/async_components/__init__.py b/lib/itchat/async_components/__init__.py deleted file mode 100644 index 0fc321c..0000000 --- a/lib/itchat/async_components/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .contact import load_contact -from .hotreload import load_hotreload -from .login import load_login -from .messages import load_messages -from .register import load_register - -def load_components(core): - load_contact(core) - load_hotreload(core) - load_login(core) - load_messages(core) - load_register(core) diff --git a/lib/itchat/async_components/contact.py b/lib/itchat/async_components/contact.py deleted file mode 100644 index cd8446b..0000000 --- a/lib/itchat/async_components/contact.py +++ /dev/null @@ -1,488 +0,0 @@ -import time, re, io -import json, copy -import logging - -from .. import config, utils -from ..components.contact import accept_friend -from ..returnvalues import ReturnValue -from ..storage import contact_change -from ..utils import update_info_dict - -logger = logging.getLogger('itchat') - -def load_contact(core): - core.update_chatroom = update_chatroom - core.update_friend = update_friend - core.get_contact = get_contact - core.get_friends = get_friends - core.get_chatrooms = get_chatrooms - core.get_mps = get_mps - core.set_alias = set_alias - core.set_pinned = set_pinned - core.accept_friend = accept_friend - core.get_head_img = get_head_img - core.create_chatroom = create_chatroom - core.set_chatroom_name = set_chatroom_name - core.delete_member_from_chatroom = delete_member_from_chatroom - core.add_member_into_chatroom = add_member_into_chatroom - -def update_chatroom(self, userName, detailedMember=False): - if not isinstance(userName, list): - userName = [userName] - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(userName), - 'List': [{ - 'UserName': u, - 'ChatRoomId': '', } for u in userName], } - chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace')).get('ContactList') - if not chatroomList: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No chatroom found', - 'Ret': -1001, }}) - - if detailedMember: - def get_detailed_member_info(encryChatroomId, memberList): - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(memberList), - 'List': [{ - 'UserName': member['UserName'], - 'EncryChatRoomId': encryChatroomId} \ - for member in memberList], } - return json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace'))['ContactList'] - MAX_GET_NUMBER = 50 - for chatroom in chatroomList: - totalMemberList = [] - for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)): - memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER] - totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList) - chatroom['MemberList'] = totalMemberList - - update_local_chatrooms(self, chatroomList) - r = [self.storageClass.search_chatrooms(userName=c['UserName']) - for c in chatroomList] - return r if 1 < len(r) else r[0] - -def update_friend(self, userName): - if not isinstance(userName, list): - userName = [userName] - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(userName), - 'List': [{ - 'UserName': u, - 'EncryChatRoomId': '', } for u in userName], } - friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace')).get('ContactList') - - update_local_friends(self, friendList) - r = [self.storageClass.search_friends(userName=f['UserName']) - for f in friendList] - return r if len(r) != 1 else r[0] - -@contact_change -def update_local_chatrooms(core, l): - ''' - get a list of chatrooms for updating local chatrooms - return a list of given chatrooms with updated info - ''' - for chatroom in l: - # format new chatrooms - utils.emoji_formatter(chatroom, 'NickName') - for member in chatroom['MemberList']: - if 'NickName' in member: - utils.emoji_formatter(member, 'NickName') - if 'DisplayName' in member: - utils.emoji_formatter(member, 'DisplayName') - if 'RemarkName' in member: - utils.emoji_formatter(member, 'RemarkName') - # update it to old chatrooms - oldChatroom = utils.search_dict_list( - core.chatroomList, 'UserName', chatroom['UserName']) - if oldChatroom: - update_info_dict(oldChatroom, chatroom) - # - update other values - memberList = chatroom.get('MemberList', []) - oldMemberList = oldChatroom['MemberList'] - if memberList: - for member in memberList: - oldMember = utils.search_dict_list( - oldMemberList, 'UserName', member['UserName']) - if oldMember: - update_info_dict(oldMember, member) - else: - oldMemberList.append(member) - else: - core.chatroomList.append(chatroom) - oldChatroom = utils.search_dict_list( - core.chatroomList, 'UserName', chatroom['UserName']) - # delete useless members - if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \ - chatroom['MemberList']: - existsUserNames = [member['UserName'] for member in chatroom['MemberList']] - delList = [] - for i, member in enumerate(oldChatroom['MemberList']): - if member['UserName'] not in existsUserNames: - delList.append(i) - delList.sort(reverse=True) - for i in delList: - del oldChatroom['MemberList'][i] - # - update OwnerUin - if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'): - owner = utils.search_dict_list(oldChatroom['MemberList'], - 'UserName', oldChatroom['ChatRoomOwner']) - oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0) - # - update IsAdmin - if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0: - oldChatroom['IsAdmin'] = \ - oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin']) - else: - oldChatroom['IsAdmin'] = None - # - update Self - newSelf = utils.search_dict_list(oldChatroom['MemberList'], - 'UserName', core.storageClass.userName) - oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User']) - return { - 'Type' : 'System', - 'Text' : [chatroom['UserName'] for chatroom in l], - 'SystemInfo' : 'chatrooms', - 'FromUserName' : core.storageClass.userName, - 'ToUserName' : core.storageClass.userName, } - -@contact_change -def update_local_friends(core, l): - ''' - get a list of friends or mps for updating local contact - ''' - fullList = core.memberList + core.mpList - for friend in l: - if 'NickName' in friend: - utils.emoji_formatter(friend, 'NickName') - if 'DisplayName' in friend: - utils.emoji_formatter(friend, 'DisplayName') - if 'RemarkName' in friend: - utils.emoji_formatter(friend, 'RemarkName') - oldInfoDict = utils.search_dict_list( - fullList, 'UserName', friend['UserName']) - if oldInfoDict is None: - oldInfoDict = copy.deepcopy(friend) - if oldInfoDict['VerifyFlag'] & 8 == 0: - core.memberList.append(oldInfoDict) - else: - core.mpList.append(oldInfoDict) - else: - update_info_dict(oldInfoDict, friend) - -@contact_change -def update_local_uin(core, msg): - ''' - content contains uins and StatusNotifyUserName contains username - they are in same order, so what I do is to pair them together - - I caught an exception in this method while not knowing why - but don't worry, it won't cause any problem - ''' - uins = re.search('([^<]*?)<', msg['Content']) - usernameChangedList = [] - r = { - 'Type': 'System', - 'Text': usernameChangedList, - 'SystemInfo': 'uins', } - if uins: - uins = uins.group(1).split(',') - usernames = msg['StatusNotifyUserName'].split(',') - if 0 < len(uins) == len(usernames): - for uin, username in zip(uins, usernames): - if not '@' in username: continue - fullContact = core.memberList + core.chatroomList + core.mpList - userDicts = utils.search_dict_list(fullContact, - 'UserName', username) - if userDicts: - if userDicts.get('Uin', 0) == 0: - userDicts['Uin'] = uin - usernameChangedList.append(username) - logger.debug('Uin fetched: %s, %s' % (username, uin)) - else: - if userDicts['Uin'] != uin: - logger.debug('Uin changed: %s, %s' % ( - userDicts['Uin'], uin)) - else: - if '@@' in username: - core.storageClass.updateLock.release() - update_chatroom(core, username) - core.storageClass.updateLock.acquire() - newChatroomDict = utils.search_dict_list( - core.chatroomList, 'UserName', username) - if newChatroomDict is None: - newChatroomDict = utils.struct_friend_info({ - 'UserName': username, - 'Uin': uin, - 'Self': copy.deepcopy(core.loginInfo['User'])}) - core.chatroomList.append(newChatroomDict) - else: - newChatroomDict['Uin'] = uin - elif '@' in username: - core.storageClass.updateLock.release() - update_friend(core, username) - core.storageClass.updateLock.acquire() - newFriendDict = utils.search_dict_list( - core.memberList, 'UserName', username) - if newFriendDict is None: - newFriendDict = utils.struct_friend_info({ - 'UserName': username, - 'Uin': uin, }) - core.memberList.append(newFriendDict) - else: - newFriendDict['Uin'] = uin - usernameChangedList.append(username) - logger.debug('Uin fetched: %s, %s' % (username, uin)) - else: - logger.debug('Wrong length of uins & usernames: %s, %s' % ( - len(uins), len(usernames))) - else: - logger.debug('No uins in 51 message') - logger.debug(msg['Content']) - return r - -def get_contact(self, update=False): - if not update: - return utils.contact_deep_copy(self, self.chatroomList) - def _get_contact(seq=0): - url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'], - int(time.time()), seq, self.loginInfo['skey']) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - try: - r = self.s.get(url, headers=headers) - except Exception: - logger.info('Failed to fetch contact, that may because of the amount of your chatrooms') - for chatroom in self.get_chatrooms(): - self.update_chatroom(chatroom['UserName'], detailedMember=True) - return 0, [] - j = json.loads(r.content.decode('utf-8', 'replace')) - return j.get('Seq', 0), j.get('MemberList') - seq, memberList = 0, [] - while 1: - seq, batchMemberList = _get_contact(seq) - memberList.extend(batchMemberList) - if seq == 0: - break - chatroomList, otherList = [], [] - for m in memberList: - if m['Sex'] != 0: - otherList.append(m) - elif '@@' in m['UserName']: - chatroomList.append(m) - elif '@' in m['UserName']: - # mp will be dealt in update_local_friends as well - otherList.append(m) - if chatroomList: - update_local_chatrooms(self, chatroomList) - if otherList: - update_local_friends(self, otherList) - return utils.contact_deep_copy(self, chatroomList) - -def get_friends(self, update=False): - if update: - self.get_contact(update=True) - return utils.contact_deep_copy(self, self.memberList) - -def get_chatrooms(self, update=False, contactOnly=False): - if contactOnly: - return self.get_contact(update=True) - else: - if update: - self.get_contact(True) - return utils.contact_deep_copy(self, self.chatroomList) - -def get_mps(self, update=False): - if update: self.get_contact(update=True) - return utils.contact_deep_copy(self, self.mpList) - -def set_alias(self, userName, alias): - oldFriendInfo = utils.search_dict_list( - self.memberList, 'UserName', userName) - if oldFriendInfo is None: - return ReturnValue({'BaseResponse': { - 'Ret': -1001, }}) - url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % ( - self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket']) - data = { - 'UserName' : userName, - 'CmdId' : 2, - 'RemarkName' : alias, - 'BaseRequest' : self.loginInfo['BaseRequest'], } - headers = { 'User-Agent' : config.USER_AGENT} - r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'), - headers=headers) - r = ReturnValue(rawResponse=r) - if r: - oldFriendInfo['RemarkName'] = alias - return r - -def set_pinned(self, userName, isPinned=True): - url = '%s/webwxoplog?pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'UserName' : userName, - 'CmdId' : 3, - 'OP' : int(isPinned), - 'BaseRequest' : self.loginInfo['BaseRequest'], } - headers = { 'User-Agent' : config.USER_AGENT} - r = self.s.post(url, json=data, headers=headers) - return ReturnValue(rawResponse=r) - -def accept_friend(self, userName, v4= '', autoUpdate=True): - url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}" - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Opcode': 3, # 3 - 'VerifyUserListSize': 1, - 'VerifyUserList': [{ - 'Value': userName, - 'VerifyUserTicket': v4, }], - 'VerifyContent': '', - 'SceneListCount': 1, - 'SceneList': [33], - 'skey': self.loginInfo['skey'], } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace')) - if autoUpdate: - self.update_friend(userName) - return ReturnValue(rawResponse=r) - -def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): - ''' get head image - * if you want to get chatroom header: only set chatroomUserName - * if you want to get friend header: only set userName - * if you want to get chatroom member header: set both - ''' - params = { - 'userName': userName or chatroomUserName or self.storageClass.userName, - 'skey': self.loginInfo['skey'], - 'type': 'big', } - url = '%s/webwxgeticon' % self.loginInfo['url'] - if chatroomUserName is None: - infoDict = self.storageClass.search_friends(userName=userName) - if infoDict is None: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No friend found', - 'Ret': -1001, }}) - else: - if userName is None: - url = '%s/webwxgetheadimg' % self.loginInfo['url'] - else: - chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName) - if chatroomUserName is None: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No chatroom found', - 'Ret': -1001, }}) - if 'EncryChatRoomId' in chatroom: - params['chatroomid'] = chatroom['EncryChatRoomId'] - params['chatroomid'] = params.get('chatroomid') or chatroom['UserName'] - headers = { 'User-Agent' : config.USER_AGENT} - r = self.s.get(url, params=params, stream=True, headers=headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if picDir is None: - return tempStorage.getvalue() - with open(picDir, 'wb') as f: - f.write(tempStorage.getvalue()) - tempStorage.seek(0) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }, - 'PostFix': utils.get_image_postfix(tempStorage.read(20)), }) - -def create_chatroom(self, memberList, topic=''): - url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time())) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'MemberCount': len(memberList.split(',')), - 'MemberList': [{'UserName': member} for member in memberList.split(',')], - 'Topic': topic, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) - return ReturnValue(rawResponse=r) - -def set_chatroom_name(self, chatroomUserName, name): - url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'ChatRoomName': chatroomUserName, - 'NewTopic': name, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) - return ReturnValue(rawResponse=r) - -def delete_member_from_chatroom(self, chatroomUserName, memberList): - url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'ChatRoomName': chatroomUserName, - 'DelMemberList': ','.join([member['UserName'] for member in memberList]), } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT} - r = self.s.post(url, data=json.dumps(data),headers=headers) - return ReturnValue(rawResponse=r) - -def add_member_into_chatroom(self, chatroomUserName, memberList, - useInvitation=False): - ''' add or invite member into chatroom - * there are two ways to get members into chatroom: invite or directly add - * but for chatrooms with more than 40 users, you can only use invite - * but don't worry we will auto-force userInvitation for you when necessary - ''' - if not useInvitation: - chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName) - if not chatroom: chatroom = self.update_chatroom(chatroomUserName) - if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']: - useInvitation = True - if useInvitation: - fun, memberKeyName = 'invitemember', 'InviteMemberList' - else: - fun, memberKeyName = 'addmember', 'AddMemberList' - url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % ( - self.loginInfo['url'], fun, self.loginInfo['pass_ticket']) - params = { - 'BaseRequest' : self.loginInfo['BaseRequest'], - 'ChatRoomName' : chatroomUserName, - memberKeyName : memberList, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT} - r = self.s.post(url, data=json.dumps(params),headers=headers) - return ReturnValue(rawResponse=r) diff --git a/lib/itchat/async_components/hotreload.py b/lib/itchat/async_components/hotreload.py deleted file mode 100644 index 1bc0e42..0000000 --- a/lib/itchat/async_components/hotreload.py +++ /dev/null @@ -1,102 +0,0 @@ -import pickle, os -import logging - -import requests # type: ignore - -from ..config import VERSION -from ..returnvalues import ReturnValue -from ..storage import templates -from .contact import update_local_chatrooms, update_local_friends -from .messages import produce_msg - -logger = logging.getLogger('itchat') - -def load_hotreload(core): - core.dump_login_status = dump_login_status - core.load_login_status = load_login_status - -async def dump_login_status(self, fileDir=None): - fileDir = fileDir or self.hotReloadDir - try: - with open(fileDir, 'w') as f: - f.write('itchat - DELETE THIS') - os.remove(fileDir) - except Exception: - raise Exception('Incorrect fileDir') - status = { - 'version' : VERSION, - 'loginInfo' : self.loginInfo, - 'cookies' : self.s.cookies.get_dict(), - 'storage' : self.storageClass.dumps()} - with open(fileDir, 'wb') as f: - pickle.dump(status, f) - logger.debug('Dump login status for hot reload successfully.') - -async def load_login_status(self, fileDir, - loginCallback=None, exitCallback=None): - try: - with open(fileDir, 'rb') as f: - j = pickle.load(f) - except Exception as e: - logger.debug('No such file, loading login status failed.') - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No such file, loading login status failed.', - 'Ret': -1002, }}) - - if j.get('version', '') != VERSION: - logger.debug(('you have updated itchat from %s to %s, ' + - 'so cached status is ignored') % ( - j.get('version', 'old version'), VERSION)) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'cached status ignored because of version', - 'Ret': -1005, }}) - self.loginInfo = j['loginInfo'] - self.loginInfo['User'] = templates.User(self.loginInfo['User']) - self.loginInfo['User'].core = self - self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies']) - self.storageClass.loads(j['storage']) - try: - msgList, contactList = self.get_msg() - except Exception: - msgList = contactList = None - if (msgList or contactList) is None: - self.logout() - await load_last_login_status(self.s, j['cookies']) - logger.debug('server refused, loading login status failed.') - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'server refused, loading login status failed.', - 'Ret': -1003, }}) - else: - if contactList: - for contact in contactList: - if '@@' in contact['UserName']: - update_local_chatrooms(self, [contact]) - else: - update_local_friends(self, [contact]) - if msgList: - msgList = produce_msg(self, msgList) - for msg in msgList: self.msgList.put(msg) - await self.start_receiving(exitCallback) - logger.debug('loading login status succeeded.') - if hasattr(loginCallback, '__call__'): - await loginCallback(self.storageClass.userName) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'loading login status succeeded.', - 'Ret': 0, }}) - -async def load_last_login_status(session, cookiesDict): - try: - session.cookies = requests.utils.cookiejar_from_dict({ - 'webwxuvid': cookiesDict['webwxuvid'], - 'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'], - 'login_frequency': '2', - 'last_wxuin': cookiesDict['wxuin'], - 'wxloadtime': cookiesDict['wxloadtime'] + '_expired', - 'wxpluginkey': cookiesDict['wxloadtime'], - 'wxuin': cookiesDict['wxuin'], - 'mm_lang': 'zh_CN', - 'MM_WX_NOTIFY_STATE': '1', - 'MM_WX_SOUND_STATE': '1', }) - except Exception: - logger.info('Load status for push login failed, we may have experienced a cookies change.') - logger.info('If you are using the newest version of itchat, you may report a bug.') diff --git a/lib/itchat/async_components/login.py b/lib/itchat/async_components/login.py deleted file mode 100644 index 38c82bd..0000000 --- a/lib/itchat/async_components/login.py +++ /dev/null @@ -1,422 +0,0 @@ -import asyncio -import os, time, re, io -import threading -import json -import random -import traceback -import logging -try: - from httplib import BadStatusLine -except ImportError: - from http.client import BadStatusLine - -import requests # type: ignore -from pyqrcode import QRCode - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage.templates import wrap_user_dict -from .contact import update_local_chatrooms, update_local_friends -from .messages import produce_msg - -logger = logging.getLogger('itchat') - - -def load_login(core): - core.login = login - core.get_QRuuid = get_QRuuid - core.get_QR = get_QR - core.check_login = check_login - core.web_init = web_init - core.show_mobile_login = show_mobile_login - core.start_receiving = start_receiving - core.get_msg = get_msg - core.logout = logout - -async def login(self, enableCmdQR=False, picDir=None, qrCallback=None, EventScanPayload=None,ScanStatus=None,event_stream=None, - loginCallback=None, exitCallback=None): - if self.alive or self.isLogging: - logger.warning('itchat has already logged in.') - return - self.isLogging = True - - while self.isLogging: - uuid = await push_login(self) - if uuid: - payload = EventScanPayload( - status=ScanStatus.Waiting, - qrcode=f"qrcode/https://login.weixin.qq.com/l/{uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - else: - logger.info('Getting uuid of QR code.') - self.get_QRuuid() - payload = EventScanPayload( - status=ScanStatus.Waiting, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - print(f"https://wechaty.js.org/qrcode/https://login.weixin.qq.com/l/{self.uuid}") - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - # logger.info('Please scan the QR code to log in.') - isLoggedIn = False - while not isLoggedIn: - status = await self.check_login() - # if hasattr(qrCallback, '__call__'): - # await qrCallback(uuid=self.uuid, status=status, qrcode=self.qrStorage.getvalue()) - if status == '200': - isLoggedIn = True - payload = EventScanPayload( - status=ScanStatus.Scanned, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - elif status == '201': - if isLoggedIn is not None: - logger.info('Please press confirm on your phone.') - isLoggedIn = None - payload = EventScanPayload( - status=ScanStatus.Waiting, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - elif status != '408': - payload = EventScanPayload( - status=ScanStatus.Cancel, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - break - if isLoggedIn: - payload = EventScanPayload( - status=ScanStatus.Confirmed, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - break - elif self.isLogging: - logger.info('Log in time out, reloading QR code.') - payload = EventScanPayload( - status=ScanStatus.Timeout, - qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" - ) - event_stream.emit('scan', payload) - await asyncio.sleep(0.1) - else: - return - logger.info('Loading the contact, this may take a little while.') - await self.web_init() - await self.show_mobile_login() - self.get_contact(True) - if hasattr(loginCallback, '__call__'): - r = await loginCallback(self.storageClass.userName) - else: - utils.clear_screen() - if os.path.exists(picDir or config.DEFAULT_QR): - os.remove(picDir or config.DEFAULT_QR) - logger.info('Login successfully as %s' % self.storageClass.nickName) - await self.start_receiving(exitCallback) - self.isLogging = False - -async def push_login(core): - cookiesDict = core.s.cookies.get_dict() - if 'wxuin' in cookiesDict: - url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( - config.BASE_URL, cookiesDict['wxuin']) - headers = { 'User-Agent' : config.USER_AGENT} - r = core.s.get(url, headers=headers).json() - if 'uuid' in r and r.get('ret') in (0, '0'): - core.uuid = r['uuid'] - return r['uuid'] - return False - -def get_QRuuid(self): - url = '%s/jslogin' % config.BASE_URL - params = { - 'appid' : 'wx782c26e4c19acffb', - 'fun' : 'new', - 'redirect_uri' : 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop', - 'lang' : 'zh_CN' } - headers = { 'User-Agent' : config.USER_AGENT} - r = self.s.get(url, params=params, headers=headers) - regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - self.uuid = data.group(2) - return self.uuid - -async def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): - uuid = uuid or self.uuid - picDir = picDir or config.DEFAULT_QR - qrStorage = io.BytesIO() - qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) - qrCode.png(qrStorage, scale=10) - if hasattr(qrCallback, '__call__'): - await qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) - else: - with open(picDir, 'wb') as f: - f.write(qrStorage.getvalue()) - if enableCmdQR: - utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) - else: - utils.print_qr(picDir) - return qrStorage - -async def check_login(self, uuid=None): - uuid = uuid or self.uuid - url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL - localTime = int(time.time()) - params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( - uuid, int(-localTime / 1579), localTime) - headers = { 'User-Agent' : config.USER_AGENT} - r = self.s.get(url, params=params, headers=headers) - regx = r'window.code=(\d+)' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - if await process_login_info(self, r.text): - return '200' - else: - return '400' - elif data: - return data.group(1) - else: - return '400' - -async def process_login_info(core, loginContent): - ''' when finish login (scanning qrcode) - * syncUrl and fileUploadingUrl will be fetched - * deviceid and msgid will be generated - * skey, wxsid, wxuin, pass_ticket will be fetched - ''' - regx = r'window.redirect_uri="(\S+)";' - core.loginInfo['url'] = re.search(regx, loginContent).group(1) - headers = { 'User-Agent' : config.USER_AGENT, - 'client-version' : config.UOS_PATCH_CLIENT_VERSION, - 'extspam' : config.UOS_PATCH_EXTSPAM, - 'referer' : 'https://wx.qq.com/?&lang=zh_CN&target=t' - } - r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False) - core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')] - for indexUrl, detailedUrl in ( - ("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")), - ("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")), - ("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")), - ("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")), - ("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))): - fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] - if indexUrl in core.loginInfo['url']: - core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ - fileUrl, syncUrl - break - else: - core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] - core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] - core.loginInfo['logintime'] = int(time.time() * 1e3) - core.loginInfo['BaseRequest'] = {} - cookies = core.s.cookies.get_dict() - skey = re.findall('(.*?)', r.text, re.S)[0] - pass_ticket = re.findall('(.*?)', r.text, re.S)[0] - core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey - core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"] - core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"] - core.loginInfo['pass_ticket'] = pass_ticket - - # A question : why pass_ticket == DeviceID ? - # deviceID is only a randomly generated number - - # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM - # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: - # if node.nodeName == 'skey': - # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data - # elif node.nodeName == 'wxsid': - # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data - # elif node.nodeName == 'wxuin': - # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data - # elif node.nodeName == 'pass_ticket': - # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data - if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): - logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) - core.isLogging = False - return False - return True - -async def web_init(self): - url = '%s/webwxinit' % self.loginInfo['url'] - params = { - 'r': int(-time.time() / 1579), - 'pass_ticket': self.loginInfo['pass_ticket'], } - data = { 'BaseRequest': self.loginInfo['BaseRequest'], } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) - dic = json.loads(r.content.decode('utf-8', 'replace')) - # deal with login info - utils.emoji_formatter(dic['User'], 'NickName') - self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) - self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User'])) - self.memberList.append(self.loginInfo['User']) - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncKey']['List']]) - self.storageClass.userName = dic['User']['UserName'] - self.storageClass.nickName = dic['User']['NickName'] - # deal with contact list returned when init - contactList = dic.get('ContactList', []) - chatroomList, otherList = [], [] - for m in contactList: - if m['Sex'] != 0: - otherList.append(m) - elif '@@' in m['UserName']: - m['MemberList'] = [] # don't let dirty info pollute the list - chatroomList.append(m) - elif '@' in m['UserName']: - # mp will be dealt in update_local_friends as well - otherList.append(m) - if chatroomList: - update_local_chatrooms(self, chatroomList) - if otherList: - update_local_friends(self, otherList) - return dic - -async def show_mobile_login(self): - url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest' : self.loginInfo['BaseRequest'], - 'Code' : 3, - 'FromUserName' : self.storageClass.userName, - 'ToUserName' : self.storageClass.userName, - 'ClientMsgId' : int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT, } - r = self.s.post(url, data=json.dumps(data), headers=headers) - return ReturnValue(rawResponse=r) - -async def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): - self.alive = True - def maintain_loop(): - retryCount = 0 - while self.alive: - try: - i = sync_check(self) - if i is None: - self.alive = False - elif i == '0': - pass - else: - msgList, contactList = self.get_msg() - if msgList: - msgList = produce_msg(self, msgList) - for msg in msgList: - self.msgList.put(msg) - if contactList: - chatroomList, otherList = [], [] - for contact in contactList: - if '@@' in contact['UserName']: - chatroomList.append(contact) - else: - otherList.append(contact) - chatroomMsg = update_local_chatrooms(self, chatroomList) - chatroomMsg['User'] = self.loginInfo['User'] - self.msgList.put(chatroomMsg) - update_local_friends(self, otherList) - retryCount = 0 - except requests.exceptions.ReadTimeout: - pass - except Exception: - retryCount += 1 - logger.error(traceback.format_exc()) - if self.receivingRetryCount < retryCount: - self.alive = False - else: - time.sleep(1) - self.logout() - if hasattr(exitCallback, '__call__'): - exitCallback(self.storageClass.userName) - else: - logger.info('LOG OUT!') - if getReceivingFnOnly: - return maintain_loop - else: - maintainThread = threading.Thread(target=maintain_loop) - maintainThread.setDaemon(True) - maintainThread.start() - -def sync_check(self): - url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) - params = { - 'r' : int(time.time() * 1000), - 'skey' : self.loginInfo['skey'], - 'sid' : self.loginInfo['wxsid'], - 'uin' : self.loginInfo['wxuin'], - 'deviceid' : self.loginInfo['deviceid'], - 'synckey' : self.loginInfo['synckey'], - '_' : self.loginInfo['logintime'], } - headers = { 'User-Agent' : config.USER_AGENT} - self.loginInfo['logintime'] += 1 - try: - r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT) - except requests.exceptions.ConnectionError as e: - try: - if not isinstance(e.args[0].args[1], BadStatusLine): - raise - # will return a package with status '0 -' - # and value like: - # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 - # seems like status of typing, but before I make further achievement code will remain like this - return '2' - except Exception: - raise - r.raise_for_status() - regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' - pm = re.search(regx, r.text) - if pm is None or pm.group(1) != '0': - logger.debug('Unexpected sync check result: %s' % r.text) - return None - return pm.group(2) - -def get_msg(self): - self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] - url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['wxsid'], - self.loginInfo['skey'],self.loginInfo['pass_ticket']) - data = { - 'BaseRequest' : self.loginInfo['BaseRequest'], - 'SyncKey' : self.loginInfo['SyncKey'], - 'rr' : ~int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT) - dic = json.loads(r.content.decode('utf-8', 'replace')) - if dic['BaseResponse']['Ret'] != 0: return None, None - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncCheckKey']['List']]) - return dic['AddMsgList'], dic['ModContactList'] - -def logout(self): - if self.alive: - url = '%s/webwxlogout' % self.loginInfo['url'] - params = { - 'redirect' : 1, - 'type' : 1, - 'skey' : self.loginInfo['skey'], } - headers = { 'User-Agent' : config.USER_AGENT} - self.s.get(url, params=params, headers=headers) - self.alive = False - self.isLogging = False - self.s.cookies.clear() - del self.chatroomList[:] - del self.memberList[:] - del self.mpList[:] - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'logout successfully.', - 'Ret': 0, }}) diff --git a/lib/itchat/async_components/messages.py b/lib/itchat/async_components/messages.py deleted file mode 100644 index f842f1f..0000000 --- a/lib/itchat/async_components/messages.py +++ /dev/null @@ -1,527 +0,0 @@ -import os, time, re, io -import json -import mimetypes, hashlib -import logging -from collections import OrderedDict - - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage import templates -from .contact import update_local_uin - -logger = logging.getLogger('itchat') - -def load_messages(core): - core.send_raw_msg = send_raw_msg - core.send_msg = send_msg - core.upload_file = upload_file - core.send_file = send_file - core.send_image = send_image - core.send_video = send_video - core.send = send - core.revoke = revoke - -async def get_download_fn(core, url, msgId): - async def download_fn(downloadDir=None): - params = { - 'msgid': msgId, - 'skey': core.loginInfo['skey'],} - headers = { 'User-Agent' : config.USER_AGENT} - r = core.s.get(url, params=params, stream=True, headers = headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if downloadDir is None: - return tempStorage.getvalue() - with open(downloadDir, 'wb') as f: - f.write(tempStorage.getvalue()) - tempStorage.seek(0) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }, - 'PostFix': utils.get_image_postfix(tempStorage.read(20)), }) - return download_fn - -def produce_msg(core, msgList): - ''' for messages types - * 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg - * 53 webwxvoipnotifymsg, 9999 sysnotice - ''' - rl = [] - srl = [40, 43, 50, 52, 53, 9999] - for m in msgList: - # get actual opposite - if m['FromUserName'] == core.storageClass.userName: - actualOpposite = m['ToUserName'] - else: - actualOpposite = m['FromUserName'] - # produce basic message - if '@@' in m['FromUserName'] or '@@' in m['ToUserName']: - produce_group_chat(core, m) - else: - utils.msg_formatter(m, 'Content') - # set user of msg - if '@@' in actualOpposite: - m['User'] = core.search_chatrooms(userName=actualOpposite) or \ - templates.Chatroom({'UserName': actualOpposite}) - # we don't need to update chatroom here because we have - # updated once when producing basic message - elif actualOpposite in ('filehelper', 'fmessage'): - m['User'] = templates.User({'UserName': actualOpposite}) - else: - m['User'] = core.search_mps(userName=actualOpposite) or \ - core.search_friends(userName=actualOpposite) or \ - templates.User(userName=actualOpposite) - # by default we think there may be a user missing not a mp - m['User'].core = core - if m['MsgType'] == 1: # words - if m['Url']: - regx = r'(.+?\(.+?\))' - data = re.search(regx, m['Content']) - data = 'Map' if data is None else data.group(1) - msg = { - 'Type': 'Map', - 'Text': data,} - else: - msg = { - 'Type': 'Text', - 'Text': m['Content'],} - elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture - download_fn = get_download_fn(core, - '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type' : 'Picture', - 'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'png' if m['MsgType'] == 3 else 'gif'), - 'Text' : download_fn, } - elif m['MsgType'] == 34: # voice - download_fn = get_download_fn(core, - '%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type': 'Recording', - 'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'Text': download_fn,} - elif m['MsgType'] == 37: # friends - m['User']['UserName'] = m['RecommendInfo']['UserName'] - msg = { - 'Type': 'Friends', - 'Text': { - 'status' : m['Status'], - 'userName' : m['RecommendInfo']['UserName'], - 'verifyContent' : m['Ticket'], - 'autoUpdate' : m['RecommendInfo'], }, } - m['User'].verifyDict = msg['Text'] - elif m['MsgType'] == 42: # name card - msg = { - 'Type': 'Card', - 'Text': m['RecommendInfo'], } - elif m['MsgType'] in (43, 62): # tiny video - msgId = m['MsgId'] - async def download_video(videoDir=None): - url = '%s/webwxgetvideo' % core.loginInfo['url'] - params = { - 'msgid': msgId, - 'skey': core.loginInfo['skey'],} - headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT} - r = core.s.get(url, params=params, headers=headers, stream=True) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if videoDir is None: - return tempStorage.getvalue() - with open(videoDir, 'wb') as f: - f.write(tempStorage.getvalue()) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }}) - msg = { - 'Type': 'Video', - 'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'Text': download_video, } - elif m['MsgType'] == 49: # sharing - if m['AppMsgType'] == 0: # chat history - msg = { - 'Type': 'Note', - 'Text': m['Content'], } - elif m['AppMsgType'] == 6: - rawMsg = m - cookiesList = {name:data for name,data in core.s.cookies.items()} - async def download_atta(attaDir=None): - url = core.loginInfo['fileUrl'] + '/webwxgetmedia' - params = { - 'sender': rawMsg['FromUserName'], - 'mediaid': rawMsg['MediaId'], - 'filename': rawMsg['FileName'], - 'fromuser': core.loginInfo['wxuin'], - 'pass_ticket': 'undefined', - 'webwx_data_ticket': cookiesList['webwx_data_ticket'],} - headers = { 'User-Agent' : config.USER_AGENT} - r = core.s.get(url, params=params, stream=True, headers=headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if attaDir is None: - return tempStorage.getvalue() - with open(attaDir, 'wb') as f: - f.write(tempStorage.getvalue()) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }}) - msg = { - 'Type': 'Attachment', - 'Text': download_atta, } - elif m['AppMsgType'] == 8: - download_fn = get_download_fn(core, - '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type' : 'Picture', - 'FileName' : '%s.gif' % ( - time.strftime('%y%m%d-%H%M%S', time.localtime())), - 'Text' : download_fn, } - elif m['AppMsgType'] == 17: - msg = { - 'Type': 'Note', - 'Text': m['FileName'], } - elif m['AppMsgType'] == 2000: - regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]' - data = re.search(regx, m['Content']) - if data: - data = data.group(2).split(u'\u3002')[0] - else: - data = 'You may found detailed info in Content key.' - msg = { - 'Type': 'Note', - 'Text': data, } - else: - msg = { - 'Type': 'Sharing', - 'Text': m['FileName'], } - elif m['MsgType'] == 51: # phone init - msg = update_local_uin(core, m) - elif m['MsgType'] == 10000: - msg = { - 'Type': 'Note', - 'Text': m['Content'],} - elif m['MsgType'] == 10002: - regx = r'\[CDATA\[(.+?)\]\]' - data = re.search(regx, m['Content']) - data = 'System message' if data is None else data.group(1).replace('\\', '') - msg = { - 'Type': 'Note', - 'Text': data, } - elif m['MsgType'] in srl: - msg = { - 'Type': 'Useless', - 'Text': 'UselessMsg', } - else: - logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m))) - msg = { - 'Type': 'Useless', - 'Text': 'UselessMsg', } - m = dict(m, **msg) - rl.append(m) - return rl - -def produce_group_chat(core, msg): - r = re.match('(@[0-9a-z]*?):
(.*)$', msg['Content']) - if r: - actualUserName, content = r.groups() - chatroomUserName = msg['FromUserName'] - elif msg['FromUserName'] == core.storageClass.userName: - actualUserName = core.storageClass.userName - content = msg['Content'] - chatroomUserName = msg['ToUserName'] - else: - msg['ActualUserName'] = core.storageClass.userName - msg['ActualNickName'] = core.storageClass.nickName - msg['IsAt'] = False - utils.msg_formatter(msg, 'Content') - return - chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName) - member = utils.search_dict_list((chatroom or {}).get( - 'MemberList') or [], 'UserName', actualUserName) - if member is None: - chatroom = core.update_chatroom(chatroomUserName) - member = utils.search_dict_list((chatroom or {}).get( - 'MemberList') or [], 'UserName', actualUserName) - if member is None: - logger.debug('chatroom member fetch failed with %s' % actualUserName) - msg['ActualNickName'] = '' - msg['IsAt'] = False - else: - msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName'] - atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName) - msg['IsAt'] = ( - (atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' ')) - in msg['Content'] or msg['Content'].endswith(atFlag)) - msg['ActualUserName'] = actualUserName - msg['Content'] = content - utils.msg_formatter(msg, 'Content') - -async def send_raw_msg(self, msgType, content, toUserName): - url = '%s/webwxsendmsg' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': msgType, - 'Content': content, - 'FromUserName': self.storageClass.userName, - 'ToUserName': (toUserName if toUserName else self.storageClass.userName), - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), - }, - 'Scene': 0, } - headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT} - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -async def send_msg(self, msg='Test Message', toUserName=None): - logger.debug('Request to send a text message to %s: %s' % (toUserName, msg)) - r = await self.send_raw_msg(1, msg, toUserName) - return r - -def _prepare_file(fileDir, file_=None): - fileDict = {} - if file_: - if hasattr(file_, 'read'): - file_ = file_.read() - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'file_ param should be opened file', - 'Ret': -1005, }}) - else: - if not utils.check_file(fileDir): - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No file found in specific dir', - 'Ret': -1002, }}) - with open(fileDir, 'rb') as f: - file_ = f.read() - fileDict['fileSize'] = len(file_) - fileDict['fileMd5'] = hashlib.md5(file_).hexdigest() - fileDict['file_'] = io.BytesIO(file_) - return fileDict - -def upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper', file_=None, preparedFile=None): - logger.debug('Request to upload a %s: %s' % ( - 'picture' if isPicture else 'video' if isVideo else 'file', fileDir)) - if not preparedFile: - preparedFile = _prepare_file(fileDir, file_) - if not preparedFile: - return preparedFile - fileSize, fileMd5, file_ = \ - preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_'] - fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc' - chunks = int((fileSize - 1) / 524288) + 1 - clientMediaId = int(time.time() * 1e4) - uploadMediaRequest = json.dumps(OrderedDict([ - ('UploadType', 2), - ('BaseRequest', self.loginInfo['BaseRequest']), - ('ClientMediaId', clientMediaId), - ('TotalLen', fileSize), - ('StartPos', 0), - ('DataLen', fileSize), - ('MediaType', 4), - ('FromUserName', self.storageClass.userName), - ('ToUserName', toUserName), - ('FileMd5', fileMd5)] - ), separators = (',', ':')) - r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}} - for chunk in range(chunks): - r = upload_chunk_file(self, fileDir, fileSymbol, fileSize, - file_, chunk, chunks, uploadMediaRequest) - file_.close() - if isinstance(r, dict): - return ReturnValue(r) - return ReturnValue(rawResponse=r) - -def upload_chunk_file(core, fileDir, fileSymbol, fileSize, - file_, chunk, chunks, uploadMediaRequest): - url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \ - '/webwxuploadmedia?f=json' - # save it on server - cookiesList = {name:data for name,data in core.s.cookies.items()} - fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream' - fileName = utils.quote(os.path.basename(fileDir)) - files = OrderedDict([ - ('id', (None, 'WU_FILE_0')), - ('name', (None, fileName)), - ('type', (None, fileType)), - ('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))), - ('size', (None, str(fileSize))), - ('chunks', (None, None)), - ('chunk', (None, None)), - ('mediatype', (None, fileSymbol)), - ('uploadmediarequest', (None, uploadMediaRequest)), - ('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])), - ('pass_ticket', (None, core.loginInfo['pass_ticket'])), - ('filename' , (fileName, file_.read(524288), 'application/octet-stream'))]) - if chunks == 1: - del files['chunk']; del files['chunks'] - else: - files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks)) - headers = { 'User-Agent' : config.USER_AGENT} - return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT) - -async def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a file(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if hasattr(fileDir, 'read'): - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'fileDir param should not be an opened file in send_file', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - preparedFile = _prepare_file(fileDir, file_) - if not preparedFile: - return preparedFile - fileSize = preparedFile['fileSize'] - if mediaId is None: - r = self.upload_file(fileDir, preparedFile=preparedFile) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': 6, - 'Content': ("%s" % os.path.basename(fileDir) + - "6" + - "%s%s" % (str(fileSize), mediaId) + - "%s" % os.path.splitext(fileDir)[1].replace('.','')), - 'FromUserName': self.storageClass.userName, - 'ToUserName': toUserName, - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), }, - 'Scene': 0, } - headers = { - 'User-Agent': config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -async def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a image(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if fileDir or file_: - if hasattr(fileDir, 'read'): - file_, fileDir = fileDir, None - if fileDir is None: - fileDir = 'tmp.jpg' # specific fileDir to send gifs - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Either fileDir or file_ should be specific', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': 3, - 'MediaId': mediaId, - 'FromUserName': self.storageClass.userName, - 'ToUserName': toUserName, - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), }, - 'Scene': 0, } - if fileDir[-4:] == '.gif': - url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url'] - data['Msg']['Type'] = 47 - data['Msg']['EmojiFlag'] = 2 - headers = { - 'User-Agent': config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -async def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a video(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if fileDir or file_: - if hasattr(fileDir, 'read'): - file_, fileDir = fileDir, None - if fileDir is None: - fileDir = 'tmp.mp4' # specific fileDir to send other formats - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Either fileDir or file_ should be specific', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isVideo=True, file_=file_) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type' : 43, - 'MediaId' : mediaId, - 'FromUserName' : self.storageClass.userName, - 'ToUserName' : toUserName, - 'LocalID' : int(time.time() * 1e4), - 'ClientMsgId' : int(time.time() * 1e4), }, - 'Scene': 0, } - headers = { - 'User-Agent' : config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -async def send(self, msg, toUserName=None, mediaId=None): - if not msg: - r = ReturnValue({'BaseResponse': { - 'ErrMsg': 'No message.', - 'Ret': -1005, }}) - elif msg[:5] == '@fil@': - if mediaId is None: - r = await self.send_file(msg[5:], toUserName) - else: - r = await self.send_file(msg[5:], toUserName, mediaId) - elif msg[:5] == '@img@': - if mediaId is None: - r = await self.send_image(msg[5:], toUserName) - else: - r = await self.send_image(msg[5:], toUserName, mediaId) - elif msg[:5] == '@msg@': - r = await self.send_msg(msg[5:], toUserName) - elif msg[:5] == '@vid@': - if mediaId is None: - r = await self.send_video(msg[5:], toUserName) - else: - r = await self.send_video(msg[5:], toUserName, mediaId) - else: - r = await self.send_msg(msg, toUserName) - return r - -async def revoke(self, msgId, toUserName, localId=None): - url = '%s/webwxrevokemsg' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - "ClientMsgId": localId or str(time.time() * 1e3), - "SvrMsgId": msgId, - "ToUserName": toUserName} - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) diff --git a/lib/itchat/async_components/register.py b/lib/itchat/async_components/register.py deleted file mode 100644 index 853c69f..0000000 --- a/lib/itchat/async_components/register.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging, traceback, sys, threading -try: - import Queue -except ImportError: - import queue as Queue # type: ignore - -from ..log import set_logging -from ..utils import test_connect -from ..storage import templates - -logger = logging.getLogger('itchat') - -def load_register(core): - core.auto_login = auto_login - core.configured_reply = configured_reply - core.msg_register = msg_register - core.run = run - -async def auto_login(self, EventScanPayload=None,ScanStatus=None,event_stream=None, - hotReload=True, statusStorageDir='itchat.pkl', - enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - if not test_connect(): - logger.info("You can't get access to internet or wechat domain, so exit.") - sys.exit() - self.useHotReload = hotReload - self.hotReloadDir = statusStorageDir - if hotReload: - if await self.load_login_status(statusStorageDir, - loginCallback=loginCallback, exitCallback=exitCallback): - return - await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream, - loginCallback=loginCallback, exitCallback=exitCallback) - await self.dump_login_status(statusStorageDir) - else: - await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream, - loginCallback=loginCallback, exitCallback=exitCallback) - -async def configured_reply(self, event_stream, payload, message_container): - ''' determine the type of message and reply if its method is defined - however, I use a strange way to determine whether a msg is from massive platform - I haven't found a better solution here - The main problem I'm worrying about is the mismatching of new friends added on phone - If you have any good idea, pleeeease report an issue. I will be more than grateful. - ''' - try: - msg = self.msgList.get(timeout=1) - if 'MsgId' in msg.keys(): - message_container[msg['MsgId']] = msg - except Queue.Empty: - pass - else: - if isinstance(msg['User'], templates.User): - replyFn = self.functionDict['FriendChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.MassivePlatform): - replyFn = self.functionDict['MpChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.Chatroom): - replyFn = self.functionDict['GroupChat'].get(msg['Type']) - if replyFn is None: - r = None - else: - try: - r = await replyFn(msg) - if r is not None: - await self.send(r, msg.get('FromUserName')) - except Exception: - logger.warning(traceback.format_exc()) - -def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): - ''' a decorator constructor - return a specific decorator based on information given ''' - if not (isinstance(msgType, list) or isinstance(msgType, tuple)): - msgType = [msgType] - def _msg_register(fn): - for _msgType in msgType: - if isFriendChat: - self.functionDict['FriendChat'][_msgType] = fn - if isGroupChat: - self.functionDict['GroupChat'][_msgType] = fn - if isMpChat: - self.functionDict['MpChat'][_msgType] = fn - if not any((isFriendChat, isGroupChat, isMpChat)): - self.functionDict['FriendChat'][_msgType] = fn - return fn - return _msg_register - -async def run(self, debug=False, blockThread=True): - logger.info('Start auto replying.') - if debug: - set_logging(loggingLevel=logging.DEBUG) - async def reply_fn(): - try: - while self.alive: - await self.configured_reply() - except KeyboardInterrupt: - if self.useHotReload: - await self.dump_login_status() - self.alive = False - logger.debug('itchat received an ^C and exit.') - logger.info('Bye~') - if blockThread: - await reply_fn() - else: - replyThread = threading.Thread(target=reply_fn) - replyThread.setDaemon(True) - replyThread.start() diff --git a/lib/itchat/components/__init__.py b/lib/itchat/components/__init__.py deleted file mode 100644 index 0fc321c..0000000 --- a/lib/itchat/components/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .contact import load_contact -from .hotreload import load_hotreload -from .login import load_login -from .messages import load_messages -from .register import load_register - -def load_components(core): - load_contact(core) - load_hotreload(core) - load_login(core) - load_messages(core) - load_register(core) diff --git a/lib/itchat/components/contact.py b/lib/itchat/components/contact.py deleted file mode 100644 index df5cf92..0000000 --- a/lib/itchat/components/contact.py +++ /dev/null @@ -1,519 +0,0 @@ -import time -import re -import io -import json -import copy -import logging - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage import contact_change -from ..utils import update_info_dict - -logger = logging.getLogger('itchat') - - -def load_contact(core): - core.update_chatroom = update_chatroom - core.update_friend = update_friend - core.get_contact = get_contact - core.get_friends = get_friends - core.get_chatrooms = get_chatrooms - core.get_mps = get_mps - core.set_alias = set_alias - core.set_pinned = set_pinned - core.accept_friend = accept_friend - core.get_head_img = get_head_img - core.create_chatroom = create_chatroom - core.set_chatroom_name = set_chatroom_name - core.delete_member_from_chatroom = delete_member_from_chatroom - core.add_member_into_chatroom = add_member_into_chatroom - - -def update_chatroom(self, userName, detailedMember=False): - if not isinstance(userName, list): - userName = [userName] - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(userName), - 'List': [{ - 'UserName': u, - 'ChatRoomId': '', } for u in userName], } - chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace')).get('ContactList') - if not chatroomList: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No chatroom found', - 'Ret': -1001, }}) - - if detailedMember: - def get_detailed_member_info(encryChatroomId, memberList): - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT, } - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(memberList), - 'List': [{ - 'UserName': member['UserName'], - 'EncryChatRoomId': encryChatroomId} - for member in memberList], } - return json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace'))['ContactList'] - MAX_GET_NUMBER = 50 - for chatroom in chatroomList: - totalMemberList = [] - for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)): - memberList = chatroom['MemberList'][i * - MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER] - totalMemberList += get_detailed_member_info( - chatroom['EncryChatRoomId'], memberList) - chatroom['MemberList'] = totalMemberList - - update_local_chatrooms(self, chatroomList) - r = [self.storageClass.search_chatrooms(userName=c['UserName']) - for c in chatroomList] - return r if 1 < len(r) else r[0] - - -def update_friend(self, userName): - if not isinstance(userName, list): - userName = [userName] - url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( - self.loginInfo['url'], int(time.time())) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Count': len(userName), - 'List': [{ - 'UserName': u, - 'EncryChatRoomId': '', } for u in userName], } - friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers - ).content.decode('utf8', 'replace')).get('ContactList') - - update_local_friends(self, friendList) - r = [self.storageClass.search_friends(userName=f['UserName']) - for f in friendList] - return r if len(r) != 1 else r[0] - - -@contact_change -def update_local_chatrooms(core, l): - ''' - get a list of chatrooms for updating local chatrooms - return a list of given chatrooms with updated info - ''' - for chatroom in l: - # format new chatrooms - utils.emoji_formatter(chatroom, 'NickName') - for member in chatroom['MemberList']: - if 'NickName' in member: - utils.emoji_formatter(member, 'NickName') - if 'DisplayName' in member: - utils.emoji_formatter(member, 'DisplayName') - if 'RemarkName' in member: - utils.emoji_formatter(member, 'RemarkName') - # update it to old chatrooms - oldChatroom = utils.search_dict_list( - core.chatroomList, 'UserName', chatroom['UserName']) - if oldChatroom: - update_info_dict(oldChatroom, chatroom) - # - update other values - memberList = chatroom.get('MemberList', []) - oldMemberList = oldChatroom['MemberList'] - if memberList: - for member in memberList: - oldMember = utils.search_dict_list( - oldMemberList, 'UserName', member['UserName']) - if oldMember: - update_info_dict(oldMember, member) - else: - oldMemberList.append(member) - else: - core.chatroomList.append(chatroom) - oldChatroom = utils.search_dict_list( - core.chatroomList, 'UserName', chatroom['UserName']) - # delete useless members - if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \ - chatroom['MemberList']: - existsUserNames = [member['UserName'] - for member in chatroom['MemberList']] - delList = [] - for i, member in enumerate(oldChatroom['MemberList']): - if member['UserName'] not in existsUserNames: - delList.append(i) - delList.sort(reverse=True) - for i in delList: - del oldChatroom['MemberList'][i] - # - update OwnerUin - if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'): - owner = utils.search_dict_list(oldChatroom['MemberList'], - 'UserName', oldChatroom['ChatRoomOwner']) - oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0) - # - update IsAdmin - if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0: - oldChatroom['IsAdmin'] = \ - oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin']) - else: - oldChatroom['IsAdmin'] = None - # - update Self - newSelf = utils.search_dict_list(oldChatroom['MemberList'], - 'UserName', core.storageClass.userName) - oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User']) - return { - 'Type': 'System', - 'Text': [chatroom['UserName'] for chatroom in l], - 'SystemInfo': 'chatrooms', - 'FromUserName': core.storageClass.userName, - 'ToUserName': core.storageClass.userName, } - - -@contact_change -def update_local_friends(core, l): - ''' - get a list of friends or mps for updating local contact - ''' - fullList = core.memberList + core.mpList - for friend in l: - if 'NickName' in friend: - utils.emoji_formatter(friend, 'NickName') - if 'DisplayName' in friend: - utils.emoji_formatter(friend, 'DisplayName') - if 'RemarkName' in friend: - utils.emoji_formatter(friend, 'RemarkName') - oldInfoDict = utils.search_dict_list( - fullList, 'UserName', friend['UserName']) - if oldInfoDict is None: - oldInfoDict = copy.deepcopy(friend) - if oldInfoDict['VerifyFlag'] & 8 == 0: - core.memberList.append(oldInfoDict) - else: - core.mpList.append(oldInfoDict) - else: - update_info_dict(oldInfoDict, friend) - - -@contact_change -def update_local_uin(core, msg): - ''' - content contains uins and StatusNotifyUserName contains username - they are in same order, so what I do is to pair them together - - I caught an exception in this method while not knowing why - but don't worry, it won't cause any problem - ''' - uins = re.search('([^<]*?)<', msg['Content']) - usernameChangedList = [] - r = { - 'Type': 'System', - 'Text': usernameChangedList, - 'SystemInfo': 'uins', } - if uins: - uins = uins.group(1).split(',') - usernames = msg['StatusNotifyUserName'].split(',') - if 0 < len(uins) == len(usernames): - for uin, username in zip(uins, usernames): - if not '@' in username: - continue - fullContact = core.memberList + core.chatroomList + core.mpList - userDicts = utils.search_dict_list(fullContact, - 'UserName', username) - if userDicts: - if userDicts.get('Uin', 0) == 0: - userDicts['Uin'] = uin - usernameChangedList.append(username) - logger.debug('Uin fetched: %s, %s' % (username, uin)) - else: - if userDicts['Uin'] != uin: - logger.debug('Uin changed: %s, %s' % ( - userDicts['Uin'], uin)) - else: - if '@@' in username: - core.storageClass.updateLock.release() - update_chatroom(core, username) - core.storageClass.updateLock.acquire() - newChatroomDict = utils.search_dict_list( - core.chatroomList, 'UserName', username) - if newChatroomDict is None: - newChatroomDict = utils.struct_friend_info({ - 'UserName': username, - 'Uin': uin, - 'Self': copy.deepcopy(core.loginInfo['User'])}) - core.chatroomList.append(newChatroomDict) - else: - newChatroomDict['Uin'] = uin - elif '@' in username: - core.storageClass.updateLock.release() - update_friend(core, username) - core.storageClass.updateLock.acquire() - newFriendDict = utils.search_dict_list( - core.memberList, 'UserName', username) - if newFriendDict is None: - newFriendDict = utils.struct_friend_info({ - 'UserName': username, - 'Uin': uin, }) - core.memberList.append(newFriendDict) - else: - newFriendDict['Uin'] = uin - usernameChangedList.append(username) - logger.debug('Uin fetched: %s, %s' % (username, uin)) - else: - logger.debug('Wrong length of uins & usernames: %s, %s' % ( - len(uins), len(usernames))) - else: - logger.debug('No uins in 51 message') - logger.debug(msg['Content']) - return r - - -def get_contact(self, update=False): - if not update: - return utils.contact_deep_copy(self, self.chatroomList) - - def _get_contact(seq=0): - url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'], - int(time.time()), seq, self.loginInfo['skey']) - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT, } - try: - r = self.s.get(url, headers=headers) - except Exception: - logger.info( - 'Failed to fetch contact, that may because of the amount of your chatrooms') - for chatroom in self.get_chatrooms(): - self.update_chatroom(chatroom['UserName'], detailedMember=True) - return 0, [] - j = json.loads(r.content.decode('utf-8', 'replace')) - return j.get('Seq', 0), j.get('MemberList') - seq, memberList = 0, [] - while 1: - seq, batchMemberList = _get_contact(seq) - memberList.extend(batchMemberList) - if seq == 0: - break - chatroomList, otherList = [], [] - for m in memberList: - if m['Sex'] != 0: - otherList.append(m) - elif '@@' in m['UserName']: - chatroomList.append(m) - elif '@' in m['UserName']: - # mp will be dealt in update_local_friends as well - otherList.append(m) - if chatroomList: - update_local_chatrooms(self, chatroomList) - if otherList: - update_local_friends(self, otherList) - return utils.contact_deep_copy(self, chatroomList) - - -def get_friends(self, update=False): - if update: - self.get_contact(update=True) - return utils.contact_deep_copy(self, self.memberList) - - -def get_chatrooms(self, update=False, contactOnly=False): - if contactOnly: - return self.get_contact(update=True) - else: - if update: - self.get_contact(True) - return utils.contact_deep_copy(self, self.chatroomList) - - -def get_mps(self, update=False): - if update: - self.get_contact(update=True) - return utils.contact_deep_copy(self, self.mpList) - - -def set_alias(self, userName, alias): - oldFriendInfo = utils.search_dict_list( - self.memberList, 'UserName', userName) - if oldFriendInfo is None: - return ReturnValue({'BaseResponse': { - 'Ret': -1001, }}) - url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % ( - self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket']) - data = { - 'UserName': userName, - 'CmdId': 2, - 'RemarkName': alias, - 'BaseRequest': self.loginInfo['BaseRequest'], } - headers = {'User-Agent': config.USER_AGENT} - r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'), - headers=headers) - r = ReturnValue(rawResponse=r) - if r: - oldFriendInfo['RemarkName'] = alias - return r - - -def set_pinned(self, userName, isPinned=True): - url = '%s/webwxoplog?pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'UserName': userName, - 'CmdId': 3, - 'OP': int(isPinned), - 'BaseRequest': self.loginInfo['BaseRequest'], } - headers = {'User-Agent': config.USER_AGENT} - r = self.s.post(url, json=data, headers=headers) - return ReturnValue(rawResponse=r) - - -def accept_friend(self, userName, v4='', autoUpdate=True): - url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}" - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Opcode': 3, # 3 - 'VerifyUserListSize': 1, - 'VerifyUserList': [{ - 'Value': userName, - 'VerifyUserTicket': v4, }], - 'VerifyContent': '', - 'SceneListCount': 1, - 'SceneList': [33], - 'skey': self.loginInfo['skey'], } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace')) - if autoUpdate: - self.update_friend(userName) - return ReturnValue(rawResponse=r) - - -def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): - ''' get head image - * if you want to get chatroom header: only set chatroomUserName - * if you want to get friend header: only set userName - * if you want to get chatroom member header: set both - ''' - params = { - 'userName': userName or chatroomUserName or self.storageClass.userName, - 'skey': self.loginInfo['skey'], - 'type': 'big', } - url = '%s/webwxgeticon' % self.loginInfo['url'] - if chatroomUserName is None: - infoDict = self.storageClass.search_friends(userName=userName) - if infoDict is None: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No friend found', - 'Ret': -1001, }}) - else: - if userName is None: - url = '%s/webwxgetheadimg' % self.loginInfo['url'] - else: - chatroom = self.storageClass.search_chatrooms( - userName=chatroomUserName) - if chatroomUserName is None: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No chatroom found', - 'Ret': -1001, }}) - if 'EncryChatRoomId' in chatroom: - params['chatroomid'] = chatroom['EncryChatRoomId'] - params['chatroomid'] = params.get( - 'chatroomid') or chatroom['UserName'] - headers = {'User-Agent': config.USER_AGENT} - r = self.s.get(url, params=params, stream=True, headers=headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if picDir is None: - return tempStorage.getvalue() - with open(picDir, 'wb') as f: - f.write(tempStorage.getvalue()) - tempStorage.seek(0) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }, - 'PostFix': utils.get_image_postfix(tempStorage.read(20)), }) - - -def create_chatroom(self, memberList, topic=''): - url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time())) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'MemberCount': len(memberList.split(',')), - 'MemberList': [{'UserName': member} for member in memberList.split(',')], - 'Topic': topic, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) - return ReturnValue(rawResponse=r) - - -def set_chatroom_name(self, chatroomUserName, name): - url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'ChatRoomName': chatroomUserName, - 'NewTopic': name, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) - return ReturnValue(rawResponse=r) - - -def delete_member_from_chatroom(self, chatroomUserName, memberList): - url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'ChatRoomName': chatroomUserName, - 'DelMemberList': ','.join([member['UserName'] for member in memberList]), } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, data=json.dumps(data), headers=headers) - return ReturnValue(rawResponse=r) - - -def add_member_into_chatroom(self, chatroomUserName, memberList, - useInvitation=False): - ''' add or invite member into chatroom - * there are two ways to get members into chatroom: invite or directly add - * but for chatrooms with more than 40 users, you can only use invite - * but don't worry we will auto-force userInvitation for you when necessary - ''' - if not useInvitation: - chatroom = self.storageClass.search_chatrooms( - userName=chatroomUserName) - if not chatroom: - chatroom = self.update_chatroom(chatroomUserName) - if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']: - useInvitation = True - if useInvitation: - fun, memberKeyName = 'invitemember', 'InviteMemberList' - else: - fun, memberKeyName = 'addmember', 'AddMemberList' - url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % ( - self.loginInfo['url'], fun, self.loginInfo['pass_ticket']) - params = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'ChatRoomName': chatroomUserName, - memberKeyName: memberList, } - headers = { - 'content-type': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, data=json.dumps(params), headers=headers) - return ReturnValue(rawResponse=r) diff --git a/lib/itchat/components/hotreload.py b/lib/itchat/components/hotreload.py deleted file mode 100644 index 52cb180..0000000 --- a/lib/itchat/components/hotreload.py +++ /dev/null @@ -1,102 +0,0 @@ -import pickle, os -import logging - -import requests - -from ..config import VERSION -from ..returnvalues import ReturnValue -from ..storage import templates -from .contact import update_local_chatrooms, update_local_friends -from .messages import produce_msg - -logger = logging.getLogger('itchat') - -def load_hotreload(core): - core.dump_login_status = dump_login_status - core.load_login_status = load_login_status - -def dump_login_status(self, fileDir=None): - fileDir = fileDir or self.hotReloadDir - try: - with open(fileDir, 'w') as f: - f.write('itchat - DELETE THIS') - os.remove(fileDir) - except Exception: - raise Exception('Incorrect fileDir') - status = { - 'version' : VERSION, - 'loginInfo' : self.loginInfo, - 'cookies' : self.s.cookies.get_dict(), - 'storage' : self.storageClass.dumps()} - with open(fileDir, 'wb') as f: - pickle.dump(status, f) - logger.debug('Dump login status for hot reload successfully.') - -def load_login_status(self, fileDir, - loginCallback=None, exitCallback=None): - try: - with open(fileDir, 'rb') as f: - j = pickle.load(f) - except Exception as e: - logger.debug('No such file, loading login status failed.') - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No such file, loading login status failed.', - 'Ret': -1002, }}) - - if j.get('version', '') != VERSION: - logger.debug(('you have updated itchat from %s to %s, ' + - 'so cached status is ignored') % ( - j.get('version', 'old version'), VERSION)) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'cached status ignored because of version', - 'Ret': -1005, }}) - self.loginInfo = j['loginInfo'] - self.loginInfo['User'] = templates.User(self.loginInfo['User']) - self.loginInfo['User'].core = self - self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies']) - self.storageClass.loads(j['storage']) - try: - msgList, contactList = self.get_msg() - except Exception: - msgList = contactList = None - if (msgList or contactList) is None: - self.logout() - load_last_login_status(self.s, j['cookies']) - logger.debug('server refused, loading login status failed.') - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'server refused, loading login status failed.', - 'Ret': -1003, }}) - else: - if contactList: - for contact in contactList: - if '@@' in contact['UserName']: - update_local_chatrooms(self, [contact]) - else: - update_local_friends(self, [contact]) - if msgList: - msgList = produce_msg(self, msgList) - for msg in msgList: self.msgList.put(msg) - self.start_receiving(exitCallback) - logger.debug('loading login status succeeded.') - if hasattr(loginCallback, '__call__'): - loginCallback() - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'loading login status succeeded.', - 'Ret': 0, }}) - -def load_last_login_status(session, cookiesDict): - try: - session.cookies = requests.utils.cookiejar_from_dict({ - 'webwxuvid': cookiesDict['webwxuvid'], - 'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'], - 'login_frequency': '2', - 'last_wxuin': cookiesDict['wxuin'], - 'wxloadtime': cookiesDict['wxloadtime'] + '_expired', - 'wxpluginkey': cookiesDict['wxloadtime'], - 'wxuin': cookiesDict['wxuin'], - 'mm_lang': 'zh_CN', - 'MM_WX_NOTIFY_STATE': '1', - 'MM_WX_SOUND_STATE': '1', }) - except Exception: - logger.info('Load status for push login failed, we may have experienced a cookies change.') - logger.info('If you are using the newest version of itchat, you may report a bug.') diff --git a/lib/itchat/components/login.py b/lib/itchat/components/login.py deleted file mode 100644 index 428b840..0000000 --- a/lib/itchat/components/login.py +++ /dev/null @@ -1,418 +0,0 @@ -import os -import time -import re -import io -import threading -import json -import xml.dom.minidom -import random -import traceback -import logging -try: - from httplib import BadStatusLine -except ImportError: - from http.client import BadStatusLine - -import requests -from pyqrcode import QRCode - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage.templates import wrap_user_dict -from .contact import update_local_chatrooms, update_local_friends -from .messages import produce_msg - -logger = logging.getLogger('itchat') - - -def load_login(core): - core.login = login - core.get_QRuuid = get_QRuuid - core.get_QR = get_QR - core.check_login = check_login - core.web_init = web_init - core.show_mobile_login = show_mobile_login - core.start_receiving = start_receiving - core.get_msg = get_msg - core.logout = logout - - -def login(self, enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - if self.alive or self.isLogging: - logger.warning('itchat has already logged in.') - return - self.isLogging = True - logger.info('Ready to login.') - while self.isLogging: - uuid = push_login(self) - if uuid: - qrStorage = io.BytesIO() - else: - logger.info('Getting uuid of QR code.') - while not self.get_QRuuid(): - time.sleep(1) - logger.info('Downloading QR code.') - qrStorage = self.get_QR(enableCmdQR=enableCmdQR, - picDir=picDir, qrCallback=qrCallback) - # logger.info('Please scan the QR code to log in.') - isLoggedIn = False - while not isLoggedIn: - status = self.check_login() - if hasattr(qrCallback, '__call__'): - qrCallback(uuid=self.uuid, status=status, - qrcode=qrStorage.getvalue()) - if status == '200': - isLoggedIn = True - elif status == '201': - if isLoggedIn is not None: - logger.info('Please press confirm on your phone.') - isLoggedIn = None - time.sleep(7) - time.sleep(0.5) - elif status != '408': - break - if isLoggedIn: - break - elif self.isLogging: - logger.info('Log in time out, reloading QR code.') - else: - return # log in process is stopped by user - logger.info('Loading the contact, this may take a little while.') - self.web_init() - self.show_mobile_login() - self.get_contact(True) - if hasattr(loginCallback, '__call__'): - r = loginCallback() - else: - # utils.clear_screen() - if os.path.exists(picDir or config.DEFAULT_QR): - os.remove(picDir or config.DEFAULT_QR) - logger.info('Login successfully as %s' % self.storageClass.nickName) - self.start_receiving(exitCallback) - self.isLogging = False - - -def push_login(core): - cookiesDict = core.s.cookies.get_dict() - if 'wxuin' in cookiesDict: - url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( - config.BASE_URL, cookiesDict['wxuin']) - headers = {'User-Agent': config.USER_AGENT} - r = core.s.get(url, headers=headers).json() - if 'uuid' in r and r.get('ret') in (0, '0'): - core.uuid = r['uuid'] - return r['uuid'] - return False - - -def get_QRuuid(self): - url = '%s/jslogin' % config.BASE_URL - params = { - 'appid': 'wx782c26e4c19acffb', - 'fun': 'new', - 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop', - 'lang': 'zh_CN'} - headers = {'User-Agent': config.USER_AGENT} - r = self.s.get(url, params=params, headers=headers) - regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - self.uuid = data.group(2) - return self.uuid - - -def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): - uuid = uuid or self.uuid - picDir = picDir or config.DEFAULT_QR - qrStorage = io.BytesIO() - qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) - qrCode.png(qrStorage, scale=10) - if hasattr(qrCallback, '__call__'): - qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) - else: - with open(picDir, 'wb') as f: - f.write(qrStorage.getvalue()) - if enableCmdQR: - utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) - else: - utils.print_qr(picDir) - return qrStorage - - -def check_login(self, uuid=None): - uuid = uuid or self.uuid - url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL - localTime = int(time.time()) - params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( - uuid, int(-localTime / 1579), localTime) - headers = {'User-Agent': config.USER_AGENT} - r = self.s.get(url, params=params, headers=headers) - regx = r'window.code=(\d+)' - data = re.search(regx, r.text) - if data and data.group(1) == '200': - if process_login_info(self, r.text): - return '200' - else: - return '400' - elif data: - return data.group(1) - else: - return '400' - - -def process_login_info(core, loginContent): - ''' when finish login (scanning qrcode) - * syncUrl and fileUploadingUrl will be fetched - * deviceid and msgid will be generated - * skey, wxsid, wxuin, pass_ticket will be fetched - ''' - regx = r'window.redirect_uri="(\S+)";' - core.loginInfo['url'] = re.search(regx, loginContent).group(1) - headers = {'User-Agent': config.USER_AGENT, - 'client-version': config.UOS_PATCH_CLIENT_VERSION, - 'extspam': config.UOS_PATCH_EXTSPAM, - 'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t' - } - r = core.s.get(core.loginInfo['url'], - headers=headers, allow_redirects=False) - core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind( - '/')] - for indexUrl, detailedUrl in ( - ("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")), - ("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")), - ("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")), - ("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")), - ("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))): - fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % - url for url in detailedUrl] - if indexUrl in core.loginInfo['url']: - core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ - fileUrl, syncUrl - break - else: - core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] - core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] - core.loginInfo['logintime'] = int(time.time() * 1e3) - core.loginInfo['BaseRequest'] = {} - cookies = core.s.cookies.get_dict() - res = re.findall('(.*?)', r.text, re.S) - skey = res[0] if res else None - res = re.findall( - '(.*?)', r.text, re.S) - pass_ticket = res[0] if res else None - if skey is not None: - core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey - core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"] - core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"] - if pass_ticket is not None: - core.loginInfo['pass_ticket'] = pass_ticket - # A question : why pass_ticket == DeviceID ? - # deviceID is only a randomly generated number - - # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM - # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: - # if node.nodeName == 'skey': - # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data - # elif node.nodeName == 'wxsid': - # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data - # elif node.nodeName == 'wxuin': - # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data - # elif node.nodeName == 'pass_ticket': - # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data - if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): - logger.error( - 'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) - core.isLogging = False - return False - return True - - -def web_init(self): - url = '%s/webwxinit' % self.loginInfo['url'] - params = { - 'r': int(-time.time() / 1579), - 'pass_ticket': self.loginInfo['pass_ticket'], } - data = {'BaseRequest': self.loginInfo['BaseRequest'], } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT, } - r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) - dic = json.loads(r.content.decode('utf-8', 'replace')) - # deal with login info - utils.emoji_formatter(dic['User'], 'NickName') - self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) - self.loginInfo['User'] = wrap_user_dict( - utils.struct_friend_info(dic['User'])) - self.memberList.append(self.loginInfo['User']) - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncKey']['List']]) - self.storageClass.userName = dic['User']['UserName'] - self.storageClass.nickName = dic['User']['NickName'] - # deal with contact list returned when init - contactList = dic.get('ContactList', []) - chatroomList, otherList = [], [] - for m in contactList: - if m['Sex'] != 0: - otherList.append(m) - elif '@@' in m['UserName']: - m['MemberList'] = [] # don't let dirty info pollute the list - chatroomList.append(m) - elif '@' in m['UserName']: - # mp will be dealt in update_local_friends as well - otherList.append(m) - if chatroomList: - update_local_chatrooms(self, chatroomList) - if otherList: - update_local_friends(self, otherList) - return dic - - -def show_mobile_login(self): - url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Code': 3, - 'FromUserName': self.storageClass.userName, - 'ToUserName': self.storageClass.userName, - 'ClientMsgId': int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT, } - r = self.s.post(url, data=json.dumps(data), headers=headers) - return ReturnValue(rawResponse=r) - - -def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): - self.alive = True - - def maintain_loop(): - retryCount = 0 - while self.alive: - try: - i = sync_check(self) - if i is None: - self.alive = False - elif i == '0': - pass - else: - msgList, contactList = self.get_msg() - if msgList: - msgList = produce_msg(self, msgList) - for msg in msgList: - self.msgList.put(msg) - if contactList: - chatroomList, otherList = [], [] - for contact in contactList: - if '@@' in contact['UserName']: - chatroomList.append(contact) - else: - otherList.append(contact) - chatroomMsg = update_local_chatrooms( - self, chatroomList) - chatroomMsg['User'] = self.loginInfo['User'] - self.msgList.put(chatroomMsg) - update_local_friends(self, otherList) - retryCount = 0 - except requests.exceptions.ReadTimeout: - pass - except Exception: - retryCount += 1 - logger.error(traceback.format_exc()) - if self.receivingRetryCount < retryCount: - logger.error("Having tried %s times, but still failed. " % ( - retryCount) + "Stop trying...") - self.alive = False - else: - time.sleep(1) - self.logout() - if hasattr(exitCallback, '__call__'): - exitCallback() - else: - logger.info('LOG OUT!') - if getReceivingFnOnly: - return maintain_loop - else: - maintainThread = threading.Thread(target=maintain_loop) - maintainThread.setDaemon(True) - maintainThread.start() - - -def sync_check(self): - url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) - params = { - 'r': int(time.time() * 1000), - 'skey': self.loginInfo['skey'], - 'sid': self.loginInfo['wxsid'], - 'uin': self.loginInfo['wxuin'], - 'deviceid': self.loginInfo['deviceid'], - 'synckey': self.loginInfo['synckey'], - '_': self.loginInfo['logintime'], } - headers = {'User-Agent': config.USER_AGENT} - self.loginInfo['logintime'] += 1 - try: - r = self.s.get(url, params=params, headers=headers, - timeout=config.TIMEOUT) - except requests.exceptions.ConnectionError as e: - try: - if not isinstance(e.args[0].args[1], BadStatusLine): - raise - # will return a package with status '0 -' - # and value like: - # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 - # seems like status of typing, but before I make further achievement code will remain like this - return '2' - except Exception: - raise - r.raise_for_status() - regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' - pm = re.search(regx, r.text) - if pm is None or pm.group(1) != '0': - logger.error('Unexpected sync check result: %s' % r.text) - return None - return pm.group(2) - - -def get_msg(self): - self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] - url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['wxsid'], - self.loginInfo['skey'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'SyncKey': self.loginInfo['SyncKey'], - 'rr': ~int(time.time()), } - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent': config.USER_AGENT} - r = self.s.post(url, data=json.dumps(data), - headers=headers, timeout=config.TIMEOUT) - dic = json.loads(r.content.decode('utf-8', 'replace')) - if dic['BaseResponse']['Ret'] != 0: - return None, None - self.loginInfo['SyncKey'] = dic['SyncKey'] - self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) - for item in dic['SyncCheckKey']['List']]) - return dic['AddMsgList'], dic['ModContactList'] - - -def logout(self): - if self.alive: - url = '%s/webwxlogout' % self.loginInfo['url'] - params = { - 'redirect': 1, - 'type': 1, - 'skey': self.loginInfo['skey'], } - headers = {'User-Agent': config.USER_AGENT} - self.s.get(url, params=params, headers=headers) - self.alive = False - self.isLogging = False - self.s.cookies.clear() - del self.chatroomList[:] - del self.memberList[:] - del self.mpList[:] - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'logout successfully.', - 'Ret': 0, }}) diff --git a/lib/itchat/components/messages.py b/lib/itchat/components/messages.py deleted file mode 100644 index 85c0ca2..0000000 --- a/lib/itchat/components/messages.py +++ /dev/null @@ -1,528 +0,0 @@ -import os, time, re, io -import json -import mimetypes, hashlib -import logging -from collections import OrderedDict - -import requests - -from .. import config, utils -from ..returnvalues import ReturnValue -from ..storage import templates -from .contact import update_local_uin - -logger = logging.getLogger('itchat') - -def load_messages(core): - core.send_raw_msg = send_raw_msg - core.send_msg = send_msg - core.upload_file = upload_file - core.send_file = send_file - core.send_image = send_image - core.send_video = send_video - core.send = send - core.revoke = revoke - -def get_download_fn(core, url, msgId): - def download_fn(downloadDir=None): - params = { - 'msgid': msgId, - 'skey': core.loginInfo['skey'],} - headers = { 'User-Agent' : config.USER_AGENT } - r = core.s.get(url, params=params, stream=True, headers = headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if downloadDir is None: - return tempStorage.getvalue() - with open(downloadDir, 'wb') as f: - f.write(tempStorage.getvalue()) - tempStorage.seek(0) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }, - 'PostFix': utils.get_image_postfix(tempStorage.read(20)), }) - return download_fn - -def produce_msg(core, msgList): - ''' for messages types - * 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg - * 53 webwxvoipnotifymsg, 9999 sysnotice - ''' - rl = [] - srl = [40, 43, 50, 52, 53, 9999] - for m in msgList: - # get actual opposite - if m['FromUserName'] == core.storageClass.userName: - actualOpposite = m['ToUserName'] - else: - actualOpposite = m['FromUserName'] - # produce basic message - if '@@' in m['FromUserName'] or '@@' in m['ToUserName']: - produce_group_chat(core, m) - else: - utils.msg_formatter(m, 'Content') - # set user of msg - if '@@' in actualOpposite: - m['User'] = core.search_chatrooms(userName=actualOpposite) or \ - templates.Chatroom({'UserName': actualOpposite}) - # we don't need to update chatroom here because we have - # updated once when producing basic message - elif actualOpposite in ('filehelper', 'fmessage'): - m['User'] = templates.User({'UserName': actualOpposite}) - else: - m['User'] = core.search_mps(userName=actualOpposite) or \ - core.search_friends(userName=actualOpposite) or \ - templates.User(userName=actualOpposite) - # by default we think there may be a user missing not a mp - m['User'].core = core - if m['MsgType'] == 1: # words - if m['Url']: - regx = r'(.+?\(.+?\))' - data = re.search(regx, m['Content']) - data = 'Map' if data is None else data.group(1) - msg = { - 'Type': 'Map', - 'Text': data,} - else: - msg = { - 'Type': 'Text', - 'Text': m['Content'],} - elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture - download_fn = get_download_fn(core, - '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type' : 'Picture', - 'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'png' if m['MsgType'] == 3 else 'gif'), - 'Text' : download_fn, } - elif m['MsgType'] == 34: # voice - download_fn = get_download_fn(core, - '%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type': 'Recording', - 'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'Text': download_fn,} - elif m['MsgType'] == 37: # friends - m['User']['UserName'] = m['RecommendInfo']['UserName'] - msg = { - 'Type': 'Friends', - 'Text': { - 'status' : m['Status'], - 'userName' : m['RecommendInfo']['UserName'], - 'verifyContent' : m['Ticket'], - 'autoUpdate' : m['RecommendInfo'], }, } - m['User'].verifyDict = msg['Text'] - elif m['MsgType'] == 42: # name card - msg = { - 'Type': 'Card', - 'Text': m['RecommendInfo'], } - elif m['MsgType'] in (43, 62): # tiny video - msgId = m['MsgId'] - def download_video(videoDir=None): - url = '%s/webwxgetvideo' % core.loginInfo['url'] - params = { - 'msgid': msgId, - 'skey': core.loginInfo['skey'],} - headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT } - r = core.s.get(url, params=params, headers=headers, stream=True) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if videoDir is None: - return tempStorage.getvalue() - with open(videoDir, 'wb') as f: - f.write(tempStorage.getvalue()) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }}) - msg = { - 'Type': 'Video', - 'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()), - 'Text': download_video, } - elif m['MsgType'] == 49: # sharing - if m['AppMsgType'] == 0: # chat history - msg = { - 'Type': 'Note', - 'Text': m['Content'], } - elif m['AppMsgType'] == 6: - rawMsg = m - cookiesList = {name:data for name,data in core.s.cookies.items()} - def download_atta(attaDir=None): - url = core.loginInfo['fileUrl'] + '/webwxgetmedia' - params = { - 'sender': rawMsg['FromUserName'], - 'mediaid': rawMsg['MediaId'], - 'filename': rawMsg['FileName'], - 'fromuser': core.loginInfo['wxuin'], - 'pass_ticket': 'undefined', - 'webwx_data_ticket': cookiesList['webwx_data_ticket'],} - headers = { 'User-Agent' : config.USER_AGENT } - r = core.s.get(url, params=params, stream=True, headers=headers) - tempStorage = io.BytesIO() - for block in r.iter_content(1024): - tempStorage.write(block) - if attaDir is None: - return tempStorage.getvalue() - with open(attaDir, 'wb') as f: - f.write(tempStorage.getvalue()) - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Successfully downloaded', - 'Ret': 0, }}) - msg = { - 'Type': 'Attachment', - 'Text': download_atta, } - elif m['AppMsgType'] == 8: - download_fn = get_download_fn(core, - '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) - msg = { - 'Type' : 'Picture', - 'FileName' : '%s.gif' % ( - time.strftime('%y%m%d-%H%M%S', time.localtime())), - 'Text' : download_fn, } - elif m['AppMsgType'] == 17: - msg = { - 'Type': 'Note', - 'Text': m['FileName'], } - elif m['AppMsgType'] == 2000: - regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]' - data = re.search(regx, m['Content']) - if data: - data = data.group(2).split(u'\u3002')[0] - else: - data = 'You may found detailed info in Content key.' - msg = { - 'Type': 'Note', - 'Text': data, } - else: - msg = { - 'Type': 'Sharing', - 'Text': m['FileName'], } - elif m['MsgType'] == 51: # phone init - msg = update_local_uin(core, m) - elif m['MsgType'] == 10000: - msg = { - 'Type': 'Note', - 'Text': m['Content'],} - elif m['MsgType'] == 10002: - regx = r'\[CDATA\[(.+?)\]\]' - data = re.search(regx, m['Content']) - data = 'System message' if data is None else data.group(1).replace('\\', '') - msg = { - 'Type': 'Note', - 'Text': data, } - elif m['MsgType'] in srl: - msg = { - 'Type': 'Useless', - 'Text': 'UselessMsg', } - else: - logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m))) - msg = { - 'Type': 'Useless', - 'Text': 'UselessMsg', } - m = dict(m, **msg) - rl.append(m) - return rl - -def produce_group_chat(core, msg): - r = re.match('(@[0-9a-z]*?):
(.*)$', msg['Content']) - if r: - actualUserName, content = r.groups() - chatroomUserName = msg['FromUserName'] - elif msg['FromUserName'] == core.storageClass.userName: - actualUserName = core.storageClass.userName - content = msg['Content'] - chatroomUserName = msg['ToUserName'] - else: - msg['ActualUserName'] = core.storageClass.userName - msg['ActualNickName'] = core.storageClass.nickName - msg['IsAt'] = False - utils.msg_formatter(msg, 'Content') - return - chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName) - member = utils.search_dict_list((chatroom or {}).get( - 'MemberList') or [], 'UserName', actualUserName) - if member is None: - chatroom = core.update_chatroom(chatroomUserName) - member = utils.search_dict_list((chatroom or {}).get( - 'MemberList') or [], 'UserName', actualUserName) - if member is None: - logger.debug('chatroom member fetch failed with %s' % actualUserName) - msg['ActualNickName'] = '' - msg['IsAt'] = False - else: - msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName'] - atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName) - msg['IsAt'] = ( - (atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' ')) - in msg['Content'] or msg['Content'].endswith(atFlag)) - msg['ActualUserName'] = actualUserName - msg['Content'] = content - utils.msg_formatter(msg, 'Content') - -def send_raw_msg(self, msgType, content, toUserName): - url = '%s/webwxsendmsg' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': msgType, - 'Content': content, - 'FromUserName': self.storageClass.userName, - 'ToUserName': (toUserName if toUserName else self.storageClass.userName), - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), - }, - 'Scene': 0, } - headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -def send_msg(self, msg='Test Message', toUserName=None): - logger.debug('Request to send a text message to %s: %s' % (toUserName, msg)) - r = self.send_raw_msg(1, msg, toUserName) - return r - -def _prepare_file(fileDir, file_=None): - fileDict = {} - if file_: - if hasattr(file_, 'read'): - file_ = file_.read() - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'file_ param should be opened file', - 'Ret': -1005, }}) - else: - if not utils.check_file(fileDir): - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No file found in specific dir', - 'Ret': -1002, }}) - with open(fileDir, 'rb') as f: - file_ = f.read() - fileDict['fileSize'] = len(file_) - fileDict['fileMd5'] = hashlib.md5(file_).hexdigest() - fileDict['file_'] = io.BytesIO(file_) - return fileDict - -def upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper', file_=None, preparedFile=None): - logger.debug('Request to upload a %s: %s' % ( - 'picture' if isPicture else 'video' if isVideo else 'file', fileDir)) - if not preparedFile: - preparedFile = _prepare_file(fileDir, file_) - if not preparedFile: - return preparedFile - fileSize, fileMd5, file_ = \ - preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_'] - fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc' - chunks = int((fileSize - 1) / 524288) + 1 - clientMediaId = int(time.time() * 1e4) - uploadMediaRequest = json.dumps(OrderedDict([ - ('UploadType', 2), - ('BaseRequest', self.loginInfo['BaseRequest']), - ('ClientMediaId', clientMediaId), - ('TotalLen', fileSize), - ('StartPos', 0), - ('DataLen', fileSize), - ('MediaType', 4), - ('FromUserName', self.storageClass.userName), - ('ToUserName', toUserName), - ('FileMd5', fileMd5)] - ), separators = (',', ':')) - r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}} - for chunk in range(chunks): - r = upload_chunk_file(self, fileDir, fileSymbol, fileSize, - file_, chunk, chunks, uploadMediaRequest) - file_.close() - if isinstance(r, dict): - return ReturnValue(r) - return ReturnValue(rawResponse=r) - -def upload_chunk_file(core, fileDir, fileSymbol, fileSize, - file_, chunk, chunks, uploadMediaRequest): - url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \ - '/webwxuploadmedia?f=json' - # save it on server - cookiesList = {name:data for name,data in core.s.cookies.items()} - fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream' - fileName = utils.quote(os.path.basename(fileDir)) - files = OrderedDict([ - ('id', (None, 'WU_FILE_0')), - ('name', (None, fileName)), - ('type', (None, fileType)), - ('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))), - ('size', (None, str(fileSize))), - ('chunks', (None, None)), - ('chunk', (None, None)), - ('mediatype', (None, fileSymbol)), - ('uploadmediarequest', (None, uploadMediaRequest)), - ('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])), - ('pass_ticket', (None, core.loginInfo['pass_ticket'])), - ('filename' , (fileName, file_.read(524288), 'application/octet-stream'))]) - if chunks == 1: - del files['chunk']; del files['chunks'] - else: - files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks)) - headers = { 'User-Agent' : config.USER_AGENT } - return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT) - -def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a file(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if hasattr(fileDir, 'read'): - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'fileDir param should not be an opened file in send_file', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - preparedFile = _prepare_file(fileDir, file_) - if not preparedFile: - return preparedFile - fileSize = preparedFile['fileSize'] - if mediaId is None: - r = self.upload_file(fileDir, preparedFile=preparedFile) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': 6, - 'Content': ("%s" % os.path.basename(fileDir) + - "6" + - "%s%s" % (str(fileSize), mediaId) + - "%s" % os.path.splitext(fileDir)[1].replace('.','')), - 'FromUserName': self.storageClass.userName, - 'ToUserName': toUserName, - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), }, - 'Scene': 0, } - headers = { - 'User-Agent': config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a image(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if fileDir or file_: - if hasattr(fileDir, 'read'): - file_, fileDir = fileDir, None - if fileDir is None: - fileDir = 'tmp.jpg' # specific fileDir to send gifs - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Either fileDir or file_ should be specific', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type': 3, - 'MediaId': mediaId, - 'FromUserName': self.storageClass.userName, - 'ToUserName': toUserName, - 'LocalID': int(time.time() * 1e4), - 'ClientMsgId': int(time.time() * 1e4), }, - 'Scene': 0, } - if fileDir[-4:] == '.gif': - url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url'] - data['Msg']['Type'] = 47 - data['Msg']['EmojiFlag'] = 2 - headers = { - 'User-Agent': config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - logger.debug('Request to send a video(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if fileDir or file_: - if hasattr(fileDir, 'read'): - file_, fileDir = fileDir, None - if fileDir is None: - fileDir = 'tmp.mp4' # specific fileDir to send other formats - else: - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'Either fileDir or file_ should be specific', - 'Ret': -1005, }}) - if toUserName is None: - toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isVideo=True, file_=file_) - if r: - mediaId = r['MediaId'] - else: - return r - url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % ( - self.loginInfo['url'], self.loginInfo['pass_ticket']) - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - 'Msg': { - 'Type' : 43, - 'MediaId' : mediaId, - 'FromUserName' : self.storageClass.userName, - 'ToUserName' : toUserName, - 'LocalID' : int(time.time() * 1e4), - 'ClientMsgId' : int(time.time() * 1e4), }, - 'Scene': 0, } - headers = { - 'User-Agent' : config.USER_AGENT, - 'Content-Type': 'application/json;charset=UTF-8', } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) - -def send(self, msg, toUserName=None, mediaId=None): - if not msg: - r = ReturnValue({'BaseResponse': { - 'ErrMsg': 'No message.', - 'Ret': -1005, }}) - elif msg[:5] == '@fil@': - if mediaId is None: - r = self.send_file(msg[5:], toUserName) - else: - r = self.send_file(msg[5:], toUserName, mediaId) - elif msg[:5] == '@img@': - if mediaId is None: - r = self.send_image(msg[5:], toUserName) - else: - r = self.send_image(msg[5:], toUserName, mediaId) - elif msg[:5] == '@msg@': - r = self.send_msg(msg[5:], toUserName) - elif msg[:5] == '@vid@': - if mediaId is None: - r = self.send_video(msg[5:], toUserName) - else: - r = self.send_video(msg[5:], toUserName, mediaId) - else: - r = self.send_msg(msg, toUserName) - return r - -def revoke(self, msgId, toUserName, localId=None): - url = '%s/webwxrevokemsg' % self.loginInfo['url'] - data = { - 'BaseRequest': self.loginInfo['BaseRequest'], - "ClientMsgId": localId or str(time.time() * 1e3), - "SvrMsgId": msgId, - "ToUserName": toUserName} - headers = { - 'ContentType': 'application/json; charset=UTF-8', - 'User-Agent' : config.USER_AGENT } - r = self.s.post(url, headers=headers, - data=json.dumps(data, ensure_ascii=False).encode('utf8')) - return ReturnValue(rawResponse=r) diff --git a/lib/itchat/components/register.py b/lib/itchat/components/register.py deleted file mode 100644 index 9c60677..0000000 --- a/lib/itchat/components/register.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging, traceback, sys, threading -try: - import Queue -except ImportError: - import queue as Queue - -from ..log import set_logging -from ..utils import test_connect -from ..storage import templates - -logger = logging.getLogger('itchat') - -def load_register(core): - core.auto_login = auto_login - core.configured_reply = configured_reply - core.msg_register = msg_register - core.run = run - -def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', - enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - if not test_connect(): - logger.info("You can't get access to internet or wechat domain, so exit.") - sys.exit() - self.useHotReload = hotReload - self.hotReloadDir = statusStorageDir - if hotReload: - rval=self.load_login_status(statusStorageDir, - loginCallback=loginCallback, exitCallback=exitCallback) - if rval: - return - logger.error('Hot reload failed, logging in normally, error={}'.format(rval)) - self.logout() - self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, - loginCallback=loginCallback, exitCallback=exitCallback) - self.dump_login_status(statusStorageDir) - else: - self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, - loginCallback=loginCallback, exitCallback=exitCallback) - -def configured_reply(self): - ''' determine the type of message and reply if its method is defined - however, I use a strange way to determine whether a msg is from massive platform - I haven't found a better solution here - The main problem I'm worrying about is the mismatching of new friends added on phone - If you have any good idea, pleeeease report an issue. I will be more than grateful. - ''' - try: - msg = self.msgList.get(timeout=1) - except Queue.Empty: - pass - else: - if isinstance(msg['User'], templates.User): - replyFn = self.functionDict['FriendChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.MassivePlatform): - replyFn = self.functionDict['MpChat'].get(msg['Type']) - elif isinstance(msg['User'], templates.Chatroom): - replyFn = self.functionDict['GroupChat'].get(msg['Type']) - if replyFn is None: - r = None - else: - try: - r = replyFn(msg) - if r is not None: - self.send(r, msg.get('FromUserName')) - except Exception: - logger.warning(traceback.format_exc()) - -def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): - ''' a decorator constructor - return a specific decorator based on information given ''' - if not (isinstance(msgType, list) or isinstance(msgType, tuple)): - msgType = [msgType] - def _msg_register(fn): - for _msgType in msgType: - if isFriendChat: - self.functionDict['FriendChat'][_msgType] = fn - if isGroupChat: - self.functionDict['GroupChat'][_msgType] = fn - if isMpChat: - self.functionDict['MpChat'][_msgType] = fn - if not any((isFriendChat, isGroupChat, isMpChat)): - self.functionDict['FriendChat'][_msgType] = fn - return fn - return _msg_register - -def run(self, debug=False, blockThread=True): - logger.info('Start auto replying.') - if debug: - set_logging(loggingLevel=logging.DEBUG) - def reply_fn(): - try: - while self.alive: - self.configured_reply() - except KeyboardInterrupt: - if self.useHotReload: - self.dump_login_status() - self.alive = False - logger.debug('itchat received an ^C and exit.') - logger.info('Bye~') - if blockThread: - reply_fn() - else: - replyThread = threading.Thread(target=reply_fn) - replyThread.setDaemon(True) - replyThread.start() diff --git a/lib/itchat/config.py b/lib/itchat/config.py deleted file mode 100644 index 2ac6328..0000000 --- a/lib/itchat/config.py +++ /dev/null @@ -1,17 +0,0 @@ -import os, platform - -VERSION = '1.5.0.dev' - -# use this envrionment to initialize the async & sync componment -ASYNC_COMPONENTS = os.environ.get('ITCHAT_UOS_ASYNC', False) - -BASE_URL = 'https://login.weixin.qq.com' -OS = platform.system() # Windows, Linux, Darwin -DIR = os.getcwd() -DEFAULT_QR = 'QR.png' -TIMEOUT = (10, 60) - -USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36' - -UOS_PATCH_CLIENT_VERSION = '2.0.0' -UOS_PATCH_EXTSPAM = 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==' diff --git a/lib/itchat/content.py b/lib/itchat/content.py deleted file mode 100644 index 41dc0b1..0000000 --- a/lib/itchat/content.py +++ /dev/null @@ -1,14 +0,0 @@ -TEXT = 'Text' -MAP = 'Map' -CARD = 'Card' -NOTE = 'Note' -SHARING = 'Sharing' -PICTURE = 'Picture' -RECORDING = VOICE = 'Recording' -ATTACHMENT = 'Attachment' -VIDEO = 'Video' -FRIENDS = 'Friends' -SYSTEM = 'System' - -INCOME_MSG = [TEXT, MAP, CARD, NOTE, SHARING, PICTURE, - RECORDING, VOICE, ATTACHMENT, VIDEO, FRIENDS, SYSTEM] diff --git a/lib/itchat/core.py b/lib/itchat/core.py deleted file mode 100644 index f3871b5..0000000 --- a/lib/itchat/core.py +++ /dev/null @@ -1,456 +0,0 @@ -import requests - -from . import storage - -class Core(object): - def __init__(self): - ''' init is the only method defined in core.py - alive is value showing whether core is running - - you should call logout method to change it - - after logout, a core object can login again - storageClass only uses basic python types - - so for advanced uses, inherit it yourself - receivingRetryCount is for receiving loop retry - - it's 5 now, but actually even 1 is enough - - failing is failing - ''' - self.alive, self.isLogging = False, False - self.storageClass = storage.Storage(self) - self.memberList = self.storageClass.memberList - self.mpList = self.storageClass.mpList - self.chatroomList = self.storageClass.chatroomList - self.msgList = self.storageClass.msgList - self.loginInfo = {} - self.s = requests.Session() - self.uuid = None - self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}} - self.useHotReload, self.hotReloadDir = False, 'itchat.pkl' - self.receivingRetryCount = 5 - def login(self, enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - ''' log in like web wechat does - for log in - - a QR code will be downloaded and opened - - then scanning status is logged, it paused for you confirm - - finally it logged in and show your nickName - for options - - enableCmdQR: show qrcode in command line - - integers can be used to fit strange char length - - picDir: place for storing qrcode - - qrCallback: method that should accept uuid, status, qrcode - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - for usage - ..code::python - - import itchat - itchat.login() - - it is defined in components/login.py - and of course every single move in login can be called outside - - you may scan source code to see how - - and modified according to your own demand - ''' - raise NotImplementedError() - def get_QRuuid(self): - ''' get uuid for qrcode - uuid is the symbol of qrcode - - for logging in, you need to get a uuid first - - for downloading qrcode, you need to pass uuid to it - - for checking login status, uuid is also required - if uuid has timed out, just get another - it is defined in components/login.py - ''' - raise NotImplementedError() - def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): - ''' download and show qrcode - for options - - uuid: if uuid is not set, latest uuid you fetched will be used - - enableCmdQR: show qrcode in cmd - - picDir: where to store qrcode - - qrCallback: method that should accept uuid, status, qrcode - it is defined in components/login.py - ''' - raise NotImplementedError() - def check_login(self, uuid=None): - ''' check login status - for options: - - uuid: if uuid is not set, latest uuid you fetched will be used - for return values: - - a string will be returned - - for meaning of return values - - 200: log in successfully - - 201: waiting for press confirm - - 408: uuid timed out - - 0 : unknown error - for processing: - - syncUrl and fileUrl is set - - BaseRequest is set - blocks until reaches any of above status - it is defined in components/login.py - ''' - raise NotImplementedError() - def web_init(self): - ''' get info necessary for initializing - for processing: - - own account info is set - - inviteStartCount is set - - syncKey is set - - part of contact is fetched - it is defined in components/login.py - ''' - raise NotImplementedError() - def show_mobile_login(self): - ''' show web wechat login sign - the sign is on the top of mobile phone wechat - sign will be added after sometime even without calling this function - it is defined in components/login.py - ''' - raise NotImplementedError() - def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): - ''' open a thread for heart loop and receiving messages - for options: - - exitCallback: callback after logged out - - it contains calling of logout - - getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned. - for processing: - - messages: msgs are formatted and passed on to registered fns - - contact : chatrooms are updated when related info is received - it is defined in components/login.py - ''' - raise NotImplementedError() - def get_msg(self): - ''' fetch messages - for fetching - - method blocks for sometime until - - new messages are to be received - - or anytime they like - - synckey is updated with returned synccheckkey - it is defined in components/login.py - ''' - raise NotImplementedError() - def logout(self): - ''' logout - if core is now alive - logout will tell wechat backstage to logout - and core gets ready for another login - it is defined in components/login.py - ''' - raise NotImplementedError() - def update_chatroom(self, userName, detailedMember=False): - ''' update chatroom - for chatroom contact - - a chatroom contact need updating to be detailed - - detailed means members, encryid, etc - - auto updating of heart loop is a more detailed updating - - member uin will also be filled - - once called, updated info will be stored - for options - - userName: 'UserName' key of chatroom or a list of it - - detailedMember: whether to get members of contact - it is defined in components/contact.py - ''' - raise NotImplementedError() - def update_friend(self, userName): - ''' update chatroom - for friend contact - - once called, updated info will be stored - for options - - userName: 'UserName' key of a friend or a list of it - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_contact(self, update=False): - ''' fetch part of contact - for part - - all the massive platforms and friends are fetched - - if update, only starred chatrooms are fetched - for options - - update: if not set, local value will be returned - for results - - chatroomList will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_friends(self, update=False): - ''' fetch friends list - for options - - update: if not set, local value will be returned - for results - - a list of friends' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_chatrooms(self, update=False, contactOnly=False): - ''' fetch chatrooms list - for options - - update: if not set, local value will be returned - - contactOnly: if set, only starred chatrooms will be returned - for results - - a list of chatrooms' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_mps(self, update=False): - ''' fetch massive platforms list - for options - - update: if not set, local value will be returned - for results - - a list of platforms' info dicts will be returned - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_alias(self, userName, alias): - ''' set alias for a friend - for options - - userName: 'UserName' key of info dict - - alias: new alias - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_pinned(self, userName, isPinned=True): - ''' set pinned for a friend or a chatroom - for options - - userName: 'UserName' key of info dict - - isPinned: whether to pin - it is defined in components/contact.py - ''' - raise NotImplementedError() - def accept_friend(self, userName, v4,autoUpdate=True): - ''' accept a friend or accept a friend - for options - - userName: 'UserName' for friend's info dict - - status: - - for adding status should be 2 - - for accepting status should be 3 - - ticket: greeting message - - userInfo: friend's other info for adding into local storage - it is defined in components/contact.py - ''' - raise NotImplementedError() - def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): - ''' place for docs - for options - - if you want to get chatroom header: only set chatroomUserName - - if you want to get friend header: only set userName - - if you want to get chatroom member header: set both - it is defined in components/contact.py - ''' - raise NotImplementedError() - def create_chatroom(self, memberList, topic=''): - ''' create a chatroom - for creating - - its calling frequency is strictly limited - for options - - memberList: list of member info dict - - topic: topic of new chatroom - it is defined in components/contact.py - ''' - raise NotImplementedError() - def set_chatroom_name(self, chatroomUserName, name): - ''' set chatroom name - for setting - - it makes an updating of chatroom - - which means detailed info will be returned in heart loop - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - name: new chatroom name - it is defined in components/contact.py - ''' - raise NotImplementedError() - def delete_member_from_chatroom(self, chatroomUserName, memberList): - ''' deletes members from chatroom - for deleting - - you can't delete yourself - - if so, no one will be deleted - - strict-limited frequency - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - memberList: list of members' info dict - it is defined in components/contact.py - ''' - raise NotImplementedError() - def add_member_into_chatroom(self, chatroomUserName, memberList, - useInvitation=False): - ''' add members into chatroom - for adding - - you can't add yourself or member already in chatroom - - if so, no one will be added - - if member will over 40 after adding, invitation must be used - - strict-limited frequency - for options - - chatroomUserName: 'UserName' key of chatroom info dict - - memberList: list of members' info dict - - useInvitation: if invitation is not required, set this to use - it is defined in components/contact.py - ''' - raise NotImplementedError() - def send_raw_msg(self, msgType, content, toUserName): - ''' many messages are sent in a common way - for demo - .. code:: python - - @itchat.msg_register(itchat.content.CARD) - def reply(msg): - itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName']) - - there are some little tricks here, you may discover them yourself - but remember they are tricks - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_msg(self, msg='Test Message', toUserName=None): - ''' send plain text message - for options - - msg: should be unicode if there's non-ascii words in msg - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper', file_=None, preparedFile=None): - ''' upload file to server and get mediaId - for options - - fileDir: dir for file ready for upload - - isPicture: whether file is a picture - - isVideo: whether file is a video - for return values - will return a ReturnValue - if succeeded, mediaId is in r['MediaId'] - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): - ''' send attachment - for options - - fileDir: dir for file ready for upload - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - ''' send image - for options - - fileDir: dir for file ready for upload - - if it's a gif, name it like 'xx.gif' - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): - ''' send video - for options - - fileDir: dir for file ready for upload - - if mediaId is set, it's unnecessary to set fileDir - - mediaId: mediaId for file. - - if set, file will not be uploaded twice - - toUserName: 'UserName' key of friend dict - it is defined in components/messages.py - ''' - raise NotImplementedError() - def send(self, msg, toUserName=None, mediaId=None): - ''' wrapped function for all the sending functions - for options - - msg: message starts with different string indicates different type - - list of type string: ['@fil@', '@img@', '@msg@', '@vid@'] - - they are for file, image, plain text, video - - if none of them matches, it will be sent like plain text - - toUserName: 'UserName' key of friend dict - - mediaId: if set, uploading will not be repeated - it is defined in components/messages.py - ''' - raise NotImplementedError() - def revoke(self, msgId, toUserName, localId=None): - ''' revoke message with its and msgId - for options - - msgId: message Id on server - - toUserName: 'UserName' key of friend dict - - localId: message Id at local (optional) - it is defined in components/messages.py - ''' - raise NotImplementedError() - def dump_login_status(self, fileDir=None): - ''' dump login status to a specific file - for option - - fileDir: dir for dumping login status - it is defined in components/hotreload.py - ''' - raise NotImplementedError() - def load_login_status(self, fileDir, - loginCallback=None, exitCallback=None): - ''' load login status from a specific file - for option - - fileDir: file for loading login status - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - it is defined in components/hotreload.py - ''' - raise NotImplementedError() - def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', - enableCmdQR=False, picDir=None, qrCallback=None, - loginCallback=None, exitCallback=None): - ''' log in like web wechat does - for log in - - a QR code will be downloaded and opened - - then scanning status is logged, it paused for you confirm - - finally it logged in and show your nickName - for options - - hotReload: enable hot reload - - statusStorageDir: dir for storing log in status - - enableCmdQR: show qrcode in command line - - integers can be used to fit strange char length - - picDir: place for storing qrcode - - loginCallback: callback after successfully logged in - - if not set, screen is cleared and qrcode is deleted - - exitCallback: callback after logged out - - it contains calling of logout - - qrCallback: method that should accept uuid, status, qrcode - for usage - ..code::python - - import itchat - itchat.auto_login() - - it is defined in components/register.py - and of course every single move in login can be called outside - - you may scan source code to see how - - and modified according to your own demond - ''' - raise NotImplementedError() - def configured_reply(self): - ''' determine the type of message and reply if its method is defined - however, I use a strange way to determine whether a msg is from massive platform - I haven't found a better solution here - The main problem I'm worrying about is the mismatching of new friends added on phone - If you have any good idea, pleeeease report an issue. I will be more than grateful. - ''' - raise NotImplementedError() - def msg_register(self, msgType, - isFriendChat=False, isGroupChat=False, isMpChat=False): - ''' a decorator constructor - return a specific decorator based on information given - ''' - raise NotImplementedError() - def run(self, debug=True, blockThread=True): - ''' start auto respond - for option - - debug: if set, debug info will be shown on screen - it is defined in components/register.py - ''' - raise NotImplementedError() - def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, - wechatAccount=None): - return self.storageClass.search_friends(name, userName, remarkName, - nickName, wechatAccount) - def search_chatrooms(self, name=None, userName=None): - return self.storageClass.search_chatrooms(name, userName) - def search_mps(self, name=None, userName=None): - return self.storageClass.search_mps(name, userName) diff --git a/lib/itchat/log.py b/lib/itchat/log.py deleted file mode 100644 index 4485cc9..0000000 --- a/lib/itchat/log.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging - -class LogSystem(object): - handlerList = [] - showOnCmd = True - loggingLevel = logging.INFO - loggingFile = None - def __init__(self): - self.logger = logging.getLogger('itchat') - self.logger.addHandler(logging.NullHandler()) - self.logger.setLevel(self.loggingLevel) - self.cmdHandler = logging.StreamHandler() - self.fileHandler = None - self.logger.addHandler(self.cmdHandler) - def set_logging(self, showOnCmd=True, loggingFile=None, - loggingLevel=logging.INFO): - if showOnCmd != self.showOnCmd: - if showOnCmd: - self.logger.addHandler(self.cmdHandler) - else: - self.logger.removeHandler(self.cmdHandler) - self.showOnCmd = showOnCmd - if loggingFile != self.loggingFile: - if self.loggingFile is not None: # clear old fileHandler - self.logger.removeHandler(self.fileHandler) - self.fileHandler.close() - if loggingFile is not None: # add new fileHandler - self.fileHandler = logging.FileHandler(loggingFile) - self.logger.addHandler(self.fileHandler) - self.loggingFile = loggingFile - if loggingLevel != self.loggingLevel: - self.logger.setLevel(loggingLevel) - self.loggingLevel = loggingLevel - -ls = LogSystem() -set_logging = ls.set_logging diff --git a/lib/itchat/returnvalues.py b/lib/itchat/returnvalues.py deleted file mode 100644 index f42f4e8..0000000 --- a/lib/itchat/returnvalues.py +++ /dev/null @@ -1,67 +0,0 @@ -#coding=utf8 -TRANSLATE = 'Chinese' - -class ReturnValue(dict): - ''' turn return value of itchat into a boolean value - for requests: - ..code::python - - import requests - r = requests.get('http://httpbin.org/get') - print(ReturnValue(rawResponse=r) - - for normal dict: - ..code::python - - returnDict = { - 'BaseResponse': { - 'Ret': 0, - 'ErrMsg': 'My error msg', }, } - print(ReturnValue(returnDict)) - ''' - def __init__(self, returnValueDict={}, rawResponse=None): - if rawResponse: - try: - returnValueDict = rawResponse.json() - except ValueError: - returnValueDict = { - 'BaseResponse': { - 'Ret': -1004, - 'ErrMsg': 'Unexpected return value', }, - 'Data': rawResponse.content, } - for k, v in returnValueDict.items(): - self[k] = v - if not 'BaseResponse' in self: - self['BaseResponse'] = { - 'ErrMsg': 'no BaseResponse in raw response', - 'Ret': -1000, } - if TRANSLATE: - self['BaseResponse']['RawMsg'] = self['BaseResponse'].get('ErrMsg', '') - self['BaseResponse']['ErrMsg'] = \ - TRANSLATION[TRANSLATE].get( - self['BaseResponse'].get('Ret', '')) \ - or self['BaseResponse'].get('ErrMsg', u'No ErrMsg') - self['BaseResponse']['RawMsg'] = \ - self['BaseResponse']['RawMsg'] or self['BaseResponse']['ErrMsg'] - def __nonzero__(self): - return self['BaseResponse'].get('Ret') == 0 - def __bool__(self): - return self.__nonzero__() - def __str__(self): - return '{%s}' % ', '.join( - ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) - def __repr__(self): - return '' % self.__str__() - -TRANSLATION = { - 'Chinese': { - -1000: u'返回值不带BaseResponse', - -1001: u'无法找到对应的成员', - -1002: u'文件位置错误', - -1003: u'服务器拒绝连接', - -1004: u'服务器返回异常值', - -1005: u'参数错误', - -1006: u'无效操作', - 0: u'请求成功', - }, -} diff --git a/lib/itchat/storage/__init__.py b/lib/itchat/storage/__init__.py deleted file mode 100644 index 5c65724..0000000 --- a/lib/itchat/storage/__init__.py +++ /dev/null @@ -1,117 +0,0 @@ -import os, time, copy -from threading import Lock - -from .messagequeue import Queue -from .templates import ( - ContactList, AbstractUserDict, User, - MassivePlatform, Chatroom, ChatroomMember) - -def contact_change(fn): - def _contact_change(core, *args, **kwargs): - with core.storageClass.updateLock: - return fn(core, *args, **kwargs) - return _contact_change - -class Storage(object): - def __init__(self, core): - self.userName = None - self.nickName = None - self.updateLock = Lock() - self.memberList = ContactList() - self.mpList = ContactList() - self.chatroomList = ContactList() - self.msgList = Queue(-1) - self.lastInputUserName = None - self.memberList.set_default_value(contactClass=User) - self.memberList.core = core - self.mpList.set_default_value(contactClass=MassivePlatform) - self.mpList.core = core - self.chatroomList.set_default_value(contactClass=Chatroom) - self.chatroomList.core = core - def dumps(self): - return { - 'userName' : self.userName, - 'nickName' : self.nickName, - 'memberList' : self.memberList, - 'mpList' : self.mpList, - 'chatroomList' : self.chatroomList, - 'lastInputUserName' : self.lastInputUserName, } - def loads(self, j): - self.userName = j.get('userName', None) - self.nickName = j.get('nickName', None) - del self.memberList[:] - for i in j.get('memberList', []): - self.memberList.append(i) - del self.mpList[:] - for i in j.get('mpList', []): - self.mpList.append(i) - del self.chatroomList[:] - for i in j.get('chatroomList', []): - self.chatroomList.append(i) - # I tried to solve everything in pickle - # but this way is easier and more storage-saving - for chatroom in self.chatroomList: - if 'MemberList' in chatroom: - for member in chatroom['MemberList']: - member.core = chatroom.core - member.chatroom = chatroom - if 'Self' in chatroom: - chatroom['Self'].core = chatroom.core - chatroom['Self'].chatroom = chatroom - self.lastInputUserName = j.get('lastInputUserName', None) - def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, - wechatAccount=None): - with self.updateLock: - if (name or userName or remarkName or nickName or wechatAccount) is None: - return copy.deepcopy(self.memberList[0]) # my own account - elif userName: # return the only userName match - for m in self.memberList: - if m['UserName'] == userName: - return copy.deepcopy(m) - else: - matchDict = { - 'RemarkName' : remarkName, - 'NickName' : nickName, - 'Alias' : wechatAccount, } - for k in ('RemarkName', 'NickName', 'Alias'): - if matchDict[k] is None: - del matchDict[k] - if name: # select based on name - contact = [] - for m in self.memberList: - if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]): - contact.append(m) - else: - contact = self.memberList[:] - if matchDict: # select again based on matchDict - friendList = [] - for m in contact: - if all([m.get(k) == v for k, v in matchDict.items()]): - friendList.append(m) - return copy.deepcopy(friendList) - else: - return copy.deepcopy(contact) - def search_chatrooms(self, name=None, userName=None): - with self.updateLock: - if userName is not None: - for m in self.chatroomList: - if m['UserName'] == userName: - return copy.deepcopy(m) - elif name is not None: - matchList = [] - for m in self.chatroomList: - if name in m['NickName']: - matchList.append(copy.deepcopy(m)) - return matchList - def search_mps(self, name=None, userName=None): - with self.updateLock: - if userName is not None: - for m in self.mpList: - if m['UserName'] == userName: - return copy.deepcopy(m) - elif name is not None: - matchList = [] - for m in self.mpList: - if name in m['NickName']: - matchList.append(copy.deepcopy(m)) - return matchList diff --git a/lib/itchat/storage/messagequeue.py b/lib/itchat/storage/messagequeue.py deleted file mode 100644 index 53ed669..0000000 --- a/lib/itchat/storage/messagequeue.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging -try: - import Queue as queue -except ImportError: - import queue - -from .templates import AttributeDict - -logger = logging.getLogger('itchat') - -class Queue(queue.Queue): - def put(self, message): - queue.Queue.put(self, Message(message)) - -class Message(AttributeDict): - def download(self, fileName): - if hasattr(self.text, '__call__'): - return self.text(fileName) - else: - return b'' - def __getitem__(self, value): - if value in ('isAdmin', 'isAt'): - v = value[0].upper() + value[1:] # ''[1:] == '' - logger.debug('%s is expired in 1.3.0, use %s instead.' % (value, v)) - value = v - return super(Message, self).__getitem__(value) - def __str__(self): - return '{%s}' % ', '.join( - ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], - self.__str__()) diff --git a/lib/itchat/storage/templates.py b/lib/itchat/storage/templates.py deleted file mode 100644 index 6a670d7..0000000 --- a/lib/itchat/storage/templates.py +++ /dev/null @@ -1,318 +0,0 @@ -import logging, copy, pickle -from weakref import ref - -from ..returnvalues import ReturnValue -from ..utils import update_info_dict - -logger = logging.getLogger('itchat') - -class AttributeDict(dict): - def __getattr__(self, value): - keyName = value[0].upper() + value[1:] - try: - return self[keyName] - except KeyError: - raise AttributeError("'%s' object has no attribute '%s'" % ( - self.__class__.__name__.split('.')[-1], keyName)) - def get(self, v, d=None): - try: - return self[v] - except KeyError: - return d - -class UnInitializedItchat(object): - def _raise_error(self, *args, **kwargs): - logger.warning('An itchat instance is called before initialized') - def __getattr__(self, value): - return self._raise_error - -class ContactList(list): - ''' when a dict is append, init function will be called to format that dict ''' - def __init__(self, *args, **kwargs): - super(ContactList, self).__init__(*args, **kwargs) - self.__setstate__(None) - @property - def core(self): - return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat - @core.setter - def core(self, value): - self._core = ref(value) - def set_default_value(self, initFunction=None, contactClass=None): - if hasattr(initFunction, '__call__'): - self.contactInitFn = initFunction - if hasattr(contactClass, '__call__'): - self.contactClass = contactClass - def append(self, value): - contact = self.contactClass(value) - contact.core = self.core - if self.contactInitFn is not None: - contact = self.contactInitFn(self, contact) or contact - super(ContactList, self).append(contact) - def __deepcopy__(self, memo): - r = self.__class__([copy.deepcopy(v) for v in self]) - r.contactInitFn = self.contactInitFn - r.contactClass = self.contactClass - r.core = self.core - return r - def __getstate__(self): - return 1 - def __setstate__(self, state): - self.contactInitFn = None - self.contactClass = User - def __str__(self): - return '[%s]' % ', '.join([repr(v) for v in self]) - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], - self.__str__()) - -class AbstractUserDict(AttributeDict): - def __init__(self, *args, **kwargs): - super(AbstractUserDict, self).__init__(*args, **kwargs) - @property - def core(self): - return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat - @core.setter - def core(self, value): - self._core = ref(value) - def update(self): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not be updated' % \ - self.__class__.__name__, }, }) - def set_alias(self, alias): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not set alias' % \ - self.__class__.__name__, }, }) - def set_pinned(self, isPinned=True): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not be pinned' % \ - self.__class__.__name__, }, }) - def verify(self): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s do not need verify' % \ - self.__class__.__name__, }, }) - def get_head_image(self, imageDir=None): - return self.core.get_head_img(self.userName, picDir=imageDir) - def delete_member(self, userName): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not delete member' % \ - self.__class__.__name__, }, }) - def add_member(self, userName): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not add member' % \ - self.__class__.__name__, }, }) - def send_raw_msg(self, msgType, content): - return self.core.send_raw_msg(msgType, content, self.userName) - def send_msg(self, msg='Test Message'): - return self.core.send_msg(msg, self.userName) - def send_file(self, fileDir, mediaId=None): - return self.core.send_file(fileDir, self.userName, mediaId) - def send_image(self, fileDir, mediaId=None): - return self.core.send_image(fileDir, self.userName, mediaId) - def send_video(self, fileDir=None, mediaId=None): - return self.core.send_video(fileDir, self.userName, mediaId) - def send(self, msg, mediaId=None): - return self.core.send(msg, self.userName, mediaId) - def search_member(self, name=None, userName=None, remarkName=None, nickName=None, - wechatAccount=None): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s do not have members' % \ - self.__class__.__name__, }, }) - def __deepcopy__(self, memo): - r = self.__class__() - for k, v in self.items(): - r[copy.deepcopy(k)] = copy.deepcopy(v) - r.core = self.core - return r - def __str__(self): - return '{%s}' % ', '.join( - ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], - self.__str__()) - def __getstate__(self): - return 1 - def __setstate__(self, state): - pass - -class User(AbstractUserDict): - def __init__(self, *args, **kwargs): - super(User, self).__init__(*args, **kwargs) - self.__setstate__(None) - def update(self): - r = self.core.update_friend(self.userName) - if r: - update_info_dict(self, r) - return r - def set_alias(self, alias): - return self.core.set_alias(self.userName, alias) - def set_pinned(self, isPinned=True): - return self.core.set_pinned(self.userName, isPinned) - def verify(self): - return self.core.add_friend(**self.verifyDict) - def __deepcopy__(self, memo): - r = super(User, self).__deepcopy__(memo) - r.verifyDict = copy.deepcopy(self.verifyDict) - return r - def __setstate__(self, state): - super(User, self).__setstate__(state) - self.verifyDict = {} - self['MemberList'] = fakeContactList - -class MassivePlatform(AbstractUserDict): - def __init__(self, *args, **kwargs): - super(MassivePlatform, self).__init__(*args, **kwargs) - self.__setstate__(None) - def __setstate__(self, state): - super(MassivePlatform, self).__setstate__(state) - self['MemberList'] = fakeContactList - -class Chatroom(AbstractUserDict): - def __init__(self, *args, **kwargs): - super(Chatroom, self).__init__(*args, **kwargs) - memberList = ContactList() - userName = self.get('UserName', '') - refSelf = ref(self) - def init_fn(parentList, d): - d.chatroom = refSelf() or \ - parentList.core.search_chatrooms(userName=userName) - memberList.set_default_value(init_fn, ChatroomMember) - if 'MemberList' in self: - for member in self.memberList: - memberList.append(member) - self['MemberList'] = memberList - @property - def core(self): - return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat - @core.setter - def core(self, value): - self._core = ref(value) - self.memberList.core = value - for member in self.memberList: - member.core = value - def update(self, detailedMember=False): - r = self.core.update_chatroom(self.userName, detailedMember) - if r: - update_info_dict(self, r) - self['MemberList'] = r['MemberList'] - return r - def set_alias(self, alias): - return self.core.set_chatroom_name(self.userName, alias) - def set_pinned(self, isPinned=True): - return self.core.set_pinned(self.userName, isPinned) - def delete_member(self, userName): - return self.core.delete_member_from_chatroom(self.userName, userName) - def add_member(self, userName): - return self.core.add_member_into_chatroom(self.userName, userName) - def search_member(self, name=None, userName=None, remarkName=None, nickName=None, - wechatAccount=None): - with self.core.storageClass.updateLock: - if (name or userName or remarkName or nickName or wechatAccount) is None: - return None - elif userName: # return the only userName match - for m in self.memberList: - if m.userName == userName: - return copy.deepcopy(m) - else: - matchDict = { - 'RemarkName' : remarkName, - 'NickName' : nickName, - 'Alias' : wechatAccount, } - for k in ('RemarkName', 'NickName', 'Alias'): - if matchDict[k] is None: - del matchDict[k] - if name: # select based on name - contact = [] - for m in self.memberList: - if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]): - contact.append(m) - else: - contact = self.memberList[:] - if matchDict: # select again based on matchDict - friendList = [] - for m in contact: - if all([m.get(k) == v for k, v in matchDict.items()]): - friendList.append(m) - return copy.deepcopy(friendList) - else: - return copy.deepcopy(contact) - def __setstate__(self, state): - super(Chatroom, self).__setstate__(state) - if not 'MemberList' in self: - self['MemberList'] = fakeContactList - -class ChatroomMember(AbstractUserDict): - def __init__(self, *args, **kwargs): - super(AbstractUserDict, self).__init__(*args, **kwargs) - self.__setstate__(None) - @property - def chatroom(self): - r = getattr(self, '_chatroom', lambda: fakeChatroom)() - if r is None: - userName = getattr(self, '_chatroomUserName', '') - r = self.core.search_chatrooms(userName=userName) - if isinstance(r, dict): - self.chatroom = r - return r or fakeChatroom - @chatroom.setter - def chatroom(self, value): - if isinstance(value, dict) and 'UserName' in value: - self._chatroom = ref(value) - self._chatroomUserName = value['UserName'] - def get_head_image(self, imageDir=None): - return self.core.get_head_img(self.userName, self.chatroom.userName, picDir=imageDir) - def delete_member(self, userName): - return self.core.delete_member_from_chatroom(self.chatroom.userName, self.userName) - def send_raw_msg(self, msgType, content): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def send_msg(self, msg='Test Message'): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def send_file(self, fileDir, mediaId=None): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def send_image(self, fileDir, mediaId=None): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def send_video(self, fileDir=None, mediaId=None): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def send(self, msg, mediaId=None): - return ReturnValue({'BaseResponse': { - 'Ret': -1006, - 'ErrMsg': '%s can not send message directly' % \ - self.__class__.__name__, }, }) - def __setstate__(self, state): - super(ChatroomMember, self).__setstate__(state) - self['MemberList'] = fakeContactList - -def wrap_user_dict(d): - userName = d.get('UserName') - if '@@' in userName: - r = Chatroom(d) - elif d.get('VerifyFlag', 8) & 8 == 0: - r = User(d) - else: - r = MassivePlatform(d) - return r - -fakeItchat = UnInitializedItchat() -fakeContactList = ContactList() -fakeChatroom = Chatroom() diff --git a/lib/itchat/utils.py b/lib/itchat/utils.py deleted file mode 100644 index 965b987..0000000 --- a/lib/itchat/utils.py +++ /dev/null @@ -1,163 +0,0 @@ -import re, os, sys, subprocess, copy, traceback, logging - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser -try: - from urllib import quote as _quote - quote = lambda n: _quote(n.encode('utf8', 'replace')) -except ImportError: - from urllib.parse import quote - -import requests - -from . import config - -logger = logging.getLogger('itchat') - -emojiRegex = re.compile(r'') -htmlParser = HTMLParser() -if not hasattr(htmlParser, 'unescape'): - import html - htmlParser.unescape = html.unescape - # FIX Python 3.9 HTMLParser.unescape is removed. See https://docs.python.org/3.9/whatsnew/3.9.html -try: - b = u'\u2588' - sys.stdout.write(b + '\r') - sys.stdout.flush() -except UnicodeEncodeError: - BLOCK = 'MM' -else: - BLOCK = b -friendInfoTemplate = {} -for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province', - 'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature', - 'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'): - friendInfoTemplate[k] = '' -for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag', - 'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin', - 'StarFriend', 'Statues'): - friendInfoTemplate[k] = 0 -friendInfoTemplate['MemberList'] = [] - -def clear_screen(): - os.system('cls' if config.OS == 'Windows' else 'clear') - -def emoji_formatter(d, k): - ''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage - like :face with tears of joy: will be replaced with :cat face with tears of joy: - ''' - def _emoji_debugger(d, k): - s = d[k].replace('') # fix missing bug - def __fix_miss_match(m): - return '' % ({ - '1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603', - '1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d', - '1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622', - }.get(m.group(1), m.group(1))) - return emojiRegex.sub(__fix_miss_match, s) - def _emoji_formatter(m): - s = m.group(1) - if len(s) == 6: - return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0')) - ).encode('utf8').decode('unicode-escape', 'replace') - elif len(s) == 10: - return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0')) - ).encode('utf8').decode('unicode-escape', 'replace') - else: - return ('\\U%s'%m.group(1).rjust(8, '0') - ).encode('utf8').decode('unicode-escape', 'replace') - d[k] = _emoji_debugger(d, k) - d[k] = emojiRegex.sub(_emoji_formatter, d[k]) - -def msg_formatter(d, k): - emoji_formatter(d, k) - d[k] = d[k].replace('
', '\n') - d[k] = htmlParser.unescape(d[k]) - -def check_file(fileDir): - try: - with open(fileDir): - pass - return True - except Exception: - return False - -def print_qr(fileDir): - if config.OS == 'Darwin': - subprocess.call(['open', fileDir]) - elif config.OS == 'Linux': - subprocess.call(['xdg-open', fileDir]) - else: - os.startfile(fileDir) - -def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True): - blockCount = int(enableCmdQR) - if abs(blockCount) == 0: - blockCount = 1 - white *= abs(blockCount) - if blockCount < 0: - white, black = black, white - sys.stdout.write(' '*50 + '\r') - sys.stdout.flush() - qr = qrText.replace('0', white).replace('1', black) - sys.stdout.write(qr) - sys.stdout.flush() - -def struct_friend_info(knownInfo): - member = copy.deepcopy(friendInfoTemplate) - for k, v in copy.deepcopy(knownInfo).items(): member[k] = v - return member - -def search_dict_list(l, key, value): - ''' Search a list of dict - * return dict with specific value & key ''' - for i in l: - if i.get(key) == value: - return i - -def print_line(msg, oneLine = False): - if oneLine: - sys.stdout.write(' '*40 + '\r') - sys.stdout.flush() - else: - sys.stdout.write('\n') - sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace' - ).decode(sys.stdin.encoding or 'utf8', 'replace')) - sys.stdout.flush() - -def test_connect(retryTime=5): - for i in range(retryTime): - try: - r = requests.get(config.BASE_URL) - return True - except Exception: - if i == retryTime - 1: - logger.error(traceback.format_exc()) - return False - -def contact_deep_copy(core, contact): - with core.storageClass.updateLock: - return copy.deepcopy(contact) - -def get_image_postfix(data): - data = data[:20] - if b'GIF' in data: - return 'gif' - elif b'PNG' in data: - return 'png' - elif b'JFIF' in data: - return 'jpg' - return '' - -def update_info_dict(oldInfoDict, newInfoDict): - ''' only normal values will be updated here - because newInfoDict is normal dict, so it's not necessary to consider templates - ''' - for k, v in newInfoDict.items(): - if any((isinstance(v, t) for t in (tuple, list, dict))): - pass # these values will be updated somewhere else - elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0): - oldInfoDict[k] = v \ No newline at end of file diff --git a/plugins/README.md b/plugins/README.md index 2a44615..6804a20 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -81,7 +81,7 @@ ``` - `isgroup`: `Context`是否是群聊消息。 -- `msg`: `itchat`中原始的消息对象。 +- `msg`: the original message object from the channel. - `receiver`: 需要回复消息的对象ID。 - `session_id`: 会话ID(一般是发送触发bot消息的用户ID,如果在群聊中并且`conf`里设置了`group_chat_in_one_session`,那么此处便是群聊ID) diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 7c4f132..065d859 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -59,7 +59,7 @@ COMMANDS = { }, "id": { "alias": ["id", "用户"], - "desc": "获取用户id", # wechaty和wechatmp的用户id不会变化,可用于绑定管理员 + "desc": "获取用户id", }, "reset": { "alias": ["reset", "重置会话"], @@ -204,7 +204,7 @@ class Godcmd(Plugin): COMMANDS["reset"]["alias"].append(custom_command) self.password = gconf["password"] - self.admin_users = gconf["admin_users"] # 预存的管理员账号,这些账号不需要认证。itchat的用户名每次都会变,不可用 + self.admin_users = gconf["admin_users"] global_config["admin_users"] = self.admin_users self.isrunning = True # 机器人是否运行中 diff --git a/plugins/keyword/keyword.py b/plugins/keyword/keyword.py index a43834e..b5fced3 100644 --- a/plugins/keyword/keyword.py +++ b/plugins/keyword/keyword.py @@ -71,7 +71,6 @@ class Keyword(Plugin): response = requests.get(reply_text) with open(file_path, "wb") as f: f.write(response.content) - #channel/wechat/wechat_channel.py和channel/wechat_channel.py中缺少ReplyType.FILE类型。 reply = Reply() reply.type = ReplyType.FILE reply.content = file_path diff --git a/plugins/tool/tool.py b/plugins/tool/tool.py index fe36a68..8121ae4 100644 --- a/plugins/tool/tool.py +++ b/plugins/tool/tool.py @@ -216,11 +216,6 @@ class Tool(Plugin): "browser_use_summary": kwargs.get("browser_use_summary", True), # 是否对返回结果使用tool功能 # for url-get tool "url_get_use_summary": kwargs.get("url_get_use_summary", True), # 是否对返回结果使用tool功能 - # for wechat tool - "wechat_hot_reload": kwargs.get("wechat_hot_reload", True), # 是否使用热重载的方式发送wechat - "wechat_cpt_path": kwargs.get("wechat_cpt_path", os.path.join(get_appdata_dir(), "itchat.pkl")), # wechat 配置文件(`itchat.pkl`) - "wechat_send_group": kwargs.get("wechat_send_group", False), # 是否向群组发送消息 - "wechat_nickname_mapping": kwargs.get("wechat_nickname_mapping", "{}"), # 关于人的代号映射关系。键为代号值为微信名(昵称、备注名均可) # for wikipedia tool "wikipedia_top_k_results": kwargs.get("wikipedia_top_k_results", 2), # 只返回前k个搜索结果 # for wolfram-alpha tool diff --git a/requirements.txt b/requirements.txt index 6fd539f..cccb8e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,8 @@ openai==0.27.8 aiohttp>=3.8.6,<3.10 -HTMLParser>=0.0.2 -PyQRCode==1.2.1 -qrcode==7.4.2 requests>=2.28.2 chardet>=5.1.0 Pillow -pre-commit web.py linkai>=0.0.6.0 agentmesh-sdk>=0.1.3 @@ -15,7 +11,6 @@ PyYAML>=6.0 croniter>=2.0.0 # wechatcom & wechatmp -web.py wechatpy # zhipuai diff --git a/voice/audio_convert.py b/voice/audio_convert.py index 9dd9641..f9420ef 100644 --- a/voice/audio_convert.py +++ b/voice/audio_convert.py @@ -6,7 +6,7 @@ from common.log import logger try: import pysilk except ImportError: - logger.debug("import pysilk failed, wechaty voice message will not be supported.") + logger.debug("import pysilk failed, silk voice format will not be supported.") try: from pydub import AudioSegment