def __init__(self, cache_path=None, console_qr=False, qr_path=None, qr_callback=None, login_callback=None, logout_callback=None): """ :param cache_path: * 设置当前会话的缓存路径,并开启缓存功能;为 `None` (默认) 则不开启缓存功能。 * 开启缓存后可在短时间内避免重复扫码,缓存失效时会重新要求登陆。 * 设为 `True` 时,使用默认的缓存路径 'wxpy.pkl'。 :param console_qr: * 在终端中显示登陆二维码,需要安装 pillow 模块 (`pip3 install pillow`)。 * 可为整数(int),表示二维码单元格的宽度,通常为 2 (当被设为 `True` 时,也将在内部当作 2)。 * 也可为负数,表示以反色显示二维码,适用于浅底深字的命令行界面。 * 例如: 在大部分 Linux 系统中可设为 `True` 或 2,而在 macOS Terminal 的默认白底配色中,应设为 -2。 :param qr_path: 保存二维码的路径 :param qr_callback: 获得二维码后的回调,接收参数: uuid, status, qrcode :param login_callback: 登陆成功后的回调,接收参数同上 :param logout_callback: 登出时的回调,接收参数同上 """ self.core = itchat.Core() itchat.instanceList.append(self) enhance_connection(self.core.s) if cache_path is True: cache_path = 'wxpy.pkl' self.cache_path = cache_path if console_qr is True: console_qr = 2 self.core.auto_login(hotReload=bool(cache_path), statusStorageDir=cache_path, enableCmdQR=console_qr, picDir=qr_path, qrCallback=qr_callback, loginCallback=login_callback, exitCallback=logout_callback) self.self = Friend(self.core.loginInfo['User'], self) self.file_helper = Chat(wrap_user_name('filehelper'), self) self.messages = Messages() self.registered = Registered(self) self.is_listening = False self.listening_thread = None self.temp_dir = tempfile.TemporaryDirectory(prefix='wxpy_') self.start() atexit.register(self._cleanup)
def __init__(self, console_qr=False, qr_path=None, qr_callback=None, login_callback=None, logout_callback=None, scan_callback=None, flag=None, **kwargs): self.request: Request = kwargs.pop('request') # 初始化函数 login_callback = login_callback or self.login_callback logout_callback = logout_callback or self.loginout_callback cache_path = self.default_cache_path qr_callback = qr_callback or self.qr_callback self.scan_callback = scan_callback or self.default_scan_callback self.flag = flag self.alive_key = utils.get_alive_key(self.request) super().__init__(cache_path=cache_path, console_qr=console_qr, qr_path=qr_path, qr_callback=qr_callback, login_callback=login_callback, logout_callback=logout_callback) self.max_history = getattr(settings, 'MAX_HISTORY', self.default_max_history) self.messages = Messages(max_history=self.max_history) self.kwrags = kwargs self.initial()
class Bot(object): """ 机器人对象,用于登陆和操作微信账号,涵盖大部分 Web 微信的功能 """ def __init__(self, cache_path=None, console_qr=False, qr_path=None, qr_callback=None, login_callback=None, logout_callback=None): """ :param cache_path: * 设置当前会话的缓存路径,并开启缓存功能;为 `None` (默认) 则不开启缓存功能。 * 开启缓存后可在短时间内避免重复扫码,缓存失效时会重新要求登陆。 * 设为 `True` 时,使用默认的缓存路径 'wxpy.pkl'。 :param console_qr: * 在终端中显示登陆二维码,需要安装 pillow 模块 (`pip3 install pillow`)。 * 可为整数(int),表示二维码单元格的宽度,通常为 2 (当被设为 `True` 时,也将在内部当作 2)。 * 也可为负数,表示以反色显示二维码,适用于浅底深字的命令行界面。 * 例如: 在大部分 Linux 系统中可设为 `True` 或 2,而在 macOS Terminal 的默认白底配色中,应设为 -2。 :param qr_path: 保存二维码的路径 :param qr_callback: 获得二维码后的回调,接收参数: uuid, status, qrcode :param login_callback: 登陆成功后的回调,接收参数同上 :param logout_callback: 登出时的回调,接收参数同上 """ self.core = itchat.Core() itchat.instanceList.append(self) enhance_connection(self.core.s) if cache_path is True: cache_path = 'wxpy.pkl' self.cache_path = cache_path if console_qr is True: console_qr = 2 self.core.auto_login(hotReload=bool(cache_path), statusStorageDir=cache_path, enableCmdQR=console_qr, picDir=qr_path, qrCallback=qr_callback, loginCallback=login_callback, exitCallback=logout_callback) self.self = Friend(self.core.loginInfo['User'], self) self.file_helper = Chat(wrap_user_name('filehelper'), self) self.messages = Messages() self.registered = Registered(self) self.is_listening = False self.listening_thread = None self.temp_dir = tempfile.TemporaryDirectory(prefix='wxpy_') self.start() atexit.register(self._cleanup) def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.self.name) @handle_response() def logout(self): """ 登出当前账号 """ logger.info('{}: logging out'.format(self)) return self.core.logout() @property def alive(self): """ 若为登陆状态,则为 True,否则为 False """ return self.core.alive @alive.setter def alive(self, value): self.core.alive = value def dump_login_status(self, cache_path=None): logger.info('{}: dumping login status'.format(self)) return self.core.dump_login_status(cache_path or self.cache_path) # chats def except_self(self, chats_or_dicts): """ 从聊天对象合集或用户字典列表中排除自身 :param chats_or_dicts: 聊天对象合集或用户字典列表 :return: 排除自身后的列表 :rtype: :class:`wxpy.Chats` """ return list( filter(lambda x: get_user_name(x) != self.self.user_name, chats_or_dicts)) def chats(self, update=False): """ 获取所有聊天对象 :param update: 是否更新 :return: 聊天对象合集 :rtype: :class:`wxpy.Chats` """ return Chats( self.friends(update) + self.groups(update) + self.mps(update), self) def _retrieve_itchat_storage(self, attr): with self.core.storageClass.updateLock: return getattr(self.core.storageClass, attr) @handle_response(Friend) def friends(self, update=False): """ 获取所有好友 :param update: 是否更新 :return: 聊天对象合集 :rtype: :class:`wxpy.Chats` """ if update: logger.info('{}: updating friends'.format(self)) return self.core.get_friends(update=update) else: return self._retrieve_itchat_storage('memberList') @handle_response(Group) def groups(self, update=False, contact_only=False): """ 获取所有群聊对象 一些不活跃的群可能无法被获取到,可通过在群内发言,或修改群名称的方式来激活 :param update: 是否更新 :param contact_only: 是否限于保存为联系人的群聊 :return: 群聊合集 :rtype: :class:`wxpy.Groups` """ # itchat 原代码有些难懂,似乎 itchat 中的 get_contact() 所获取的内容视其 update 参数而变化 # 如果 update=False 获取所有类型的本地聊天对象 # 反之如果 update=True,变为获取收藏的聊天室 if update or contact_only: logger.info('{}: updating groups'.format(self)) return self.core.get_chatrooms(update=update, contactOnly=contact_only) else: return self._retrieve_itchat_storage('chatroomList') @handle_response(MP) def mps(self, update=False): """ 获取所有公众号 :param update: 是否更新 :return: 聊天对象合集 :rtype: :class:`wxpy.Chats` """ if update: logger.info('{}: updating mps'.format(self)) return self.core.get_mps(update=update) else: return self._retrieve_itchat_storage('mpList') @handle_response(User) def user_details(self, user_or_users, chunk_size=50): """ 获取单个或批量获取多个用户的详细信息(地区、性别、签名等),但不可用于群聊成员 :param user_or_users: 单个或多个用户对象或 user_name :param chunk_size: 分配请求时的单批数量,目前为 50 :return: 单个或多个用户用户的详细信息 """ def chunks(): total = ensure_list(user_or_users) for i in range(0, len(total), chunk_size): yield total[i:i + chunk_size] @handle_response() def process_one_chunk(_chunk): return self.core.update_friend(userName=get_user_name(_chunk)) if isinstance(user_or_users, (list, tuple)): ret = list() for chunk in chunks(): chunk_ret = process_one_chunk(chunk) if isinstance(chunk_ret, list): ret += chunk_ret else: ret.append(chunk_ret) return ret else: return process_one_chunk(user_or_users) def search(self, name=None, **attributes): """ 在所有类型的聊天对象中进行搜索 :param name: 名称 (可以是昵称、备注等) :param attributes: 属性键值对,键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东' :return: 匹配的聊天对象合集 :rtype: :class:`wxpy.Chats` """ return self.chats().search(name, **attributes) # add / create @handle_response() def add_friend(self, user, verify_content=''): """ 添加用户为好友 :param user: 用户对象、微信ID,或 user_name :param verify_content: 验证说明信息 """ logger.info('{}: adding {} (verify_content: {})'.format( self, user, verify_content)) user_name = get_user_name(user) if re.match(r'^@[\da-f]{32,}$', user_name): status = 2 else: status = 1 return self.core.add_friend(userName=user_name, status=status, verifyContent=verify_content, autoUpdate=True) def accept_friend(self, user, verify_content=''): """ 接受用户为好友 :param user: 用户对象或 user_name :param verify_content: 验证说明信息 :return: 新的好友对象 :rtype: :class:`wxpy.Friend` """ logger.info('{}: accepting {} (verify_content: {})'.format( self, user, verify_content)) @handle_response() def do(): return self.core.add_friend(userName=get_user_name(user), status=3, verifyContent=verify_content, autoUpdate=True) do() # 若上一步没有抛出异常,则返回该好友 for friend in self.friends(): if friend == user: return friend def create_group(self, users, topic=None): """ 创建一个新的群聊 :param users: 用户列表 :param topic: 群名称 :return: 若建群成功,返回一个新的群聊对象 :rtype: :class:`wxpy.Group` """ logger.info('{}: creating group (topic: {}), with users:\n{}'.format( self, topic, pformat(users))) @handle_response() def request(): return self.core.create_chatroom(memberList=dict_list, topic=topic or '') dict_list = wrap_user_name(self.except_self(ensure_list(users))) ret = request() user_name = ret.get('ChatRoomName') if user_name: return Group(self.core.update_chatroom(userName=user_name), self) else: raise Exception('Failed to create group:\n{}'.format(pformat(ret))) # upload def upload_file(self, path): """ | 上传文件,并获取 media_id | 可用于重复发送图片、表情、视频,和文件 :param path: 文件路径 :return: media_id :rtype: str """ logger.info('{}: uploading file: {}'.format(self, path)) @handle_response() def do(): upload = functools.partial(self.core.upload_file, fileDir=path) ext = os.path.splitext(path)[1].lower() if ext in ('.bmp', '.png', '.jpeg', '.jpg', '.gif'): return upload(isPicture=True) elif ext == '.mp4': return upload(isVideo=True) else: return upload() return do().get('MediaId') # messages / register def _process_message(self, msg): """ 处理接收到的消息 """ if not self.alive: return config = self.registered.get_config(msg) logger.debug('{}: new message (func: {}):\n{}'.format( self, config.func.__name__ if config else None, msg)) if not config: return def process(): # noinspection PyBroadException try: ret = config.func(msg) if ret is not None: msg.reply(ret) except: logger.exception('\nAn error occurred in {}.'.format( config.func)) if config.run_async: Thread(target=process, daemon=True).start() else: process() def register(self, chats=None, msg_types=None, except_self=True, run_async=True, enabled=True): """ 装饰器:用于注册消息配置 :param chats: 消息所在的聊天对象:单个或列表形式的多个聊天对象或聊天类型,为空时匹配所有聊天对象 :param msg_types: 消息的类型:单个或列表形式的多个消息类型,为空时匹配所有消息类型 (SYSTEM 类消息除外) :param except_self: 排除由自己发送的消息 :param run_async: 是否异步执行所配置的函数:可提高响应速度 :param enabled: 当前配置的默认开启状态,可事后动态开启或关闭 """ def do_register(func): self.registered.append( MessageConfig(bot=self, func=func, chats=chats, msg_types=msg_types, except_self=except_self, run_async=run_async, enabled=enabled)) return func return do_register def _listen(self): try: logger.info('{}: started'.format(self)) self.is_listening = True while self.alive and self.is_listening: try: msg = Message(self.core.msgList.get(timeout=0.5), self) except queue.Empty: continue if msg.type is not SYSTEM: self.messages.append(msg) self._process_message(msg) finally: self.is_listening = False logger.info('{}: stopped'.format(self)) def start(self): """ 开始消息监听和处理 (登陆后会自动开始) """ if not self.alive: logger.warning('{} has been logged out!'.format(self)) elif self.is_listening: logger.warning( '{} is already running, no need to start again.'.format(self)) else: self.listening_thread = Thread(target=self._listen, daemon=True) self.listening_thread.start() def stop(self): """ 停止消息监听和处理 (登出后会自动停止) """ if self.is_listening: self.is_listening = False self.listening_thread.join() else: logger.warning('{} is not running.'.format(self)) def join(self): """ 堵塞进程,直到结束消息监听 (例如,机器人被登出时) """ if isinstance(self.listening_thread, Thread): try: logger.info('{}: joined'.format(self)) self.listening_thread.join() except KeyboardInterrupt: pass def _cleanup(self): if self.is_listening: self.stop() if self.alive and self.core.useHotReload: self.dump_login_status() self.temp_dir.cleanup()
class Bot(object): """ 机器人对象,用于登陆和操作微信账号,涵盖大部分 Web 微信的功能 """ def __init__( self, cache_path=None, console_qr=False, qr_path=None, qr_callback=None, login_callback=None, logout_callback=None ): """ :param cache_path: * 设置当前会话的缓存路径,并开启缓存功能;为 `None` (默认) 则不开启缓存功能。 * 开启缓存后可在短时间内避免重复扫码,缓存失效时会重新要求登陆。 * 设为 `True` 时,使用默认的缓存路径 'wxpy.pkl'。 :param console_qr: * 在终端中显示登陆二维码,需要安装 pillow 模块 (`pip3 install pillow`)。 * 可为整数(int),表示二维码单元格的宽度,通常为 2 (当被设为 `True` 时,也将在内部当作 2)。 * 也可为负数,表示以反色显示二维码,适用于浅底深字的命令行界面。 * 例如: 在大部分 Linux 系统中可设为 `True` 或 2,而在 macOS Terminal 的默认白底配色中,应设为 -2。 :param qr_path: 保存二维码的路径 :param qr_callback: 获得二维码后的回调,接收参数: uuid, status, qrcode :param login_callback: 登陆成功后的回调,接收参数同上 :param logout_callback: 登出时的回调,接收参数同上 """ self.core = itchat.Core() itchat.instanceList.append(self) if cache_path is True: cache_path = 'wxpy.pkl' if console_qr is True: console_qr = 2 self.core.auto_login( hotReload=bool(cache_path), statusStorageDir=cache_path, enableCmdQR=console_qr, picDir=qr_path, qrCallback=qr_callback, loginCallback=login_callback, exitCallback=logout_callback ) self.message_configs = MessageConfigs(self) self.messages = Messages(bot=self) self.file_helper = Chat(wrap_user_name('filehelper'), self) self.self = Chat(self.core.loginInfo['User'], self) self.self.bot = self self.cache_path = cache_path def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.self.name) @handle_response() def logout(self): """ 登出当前账号 """ return self.core.logout() @property def alive(self): """ 当前的登陆状态 :return: 若为登陆状态,则为 True,否则为 False """ return self.core.alive @alive.setter def alive(self, value): self.core.alive = value def dump_login_status(self, cache_path=None): return self.core.dump_login_status(cache_path or self.cache_path) # chats def except_self(self, chats_or_dicts): """ 从聊天对象合集或用户字典列表中排除自身 :param chats_or_dicts: 聊天对象合集或用户字典列表 :return: 排除自身后的列表 """ return list(filter(lambda x: get_user_name(x) != self.self.user_name, chats_or_dicts)) def chats(self, update=False): """ 获取所有聊天对象 :param update: 是否更新 :return: 聊天对象合集 """ return Chats(self.friends(update) + self.groups(update) + self.mps(update), self) def _retrieve_itchat_storage(self, attr): with self.core.storageClass.updateLock: return getattr(self.core.storageClass, attr) @handle_response(Friend) def friends(self, update=False): """ 获取所有好友 :param update: 是否更新 :return: 聊天对象合集 """ if update: return self.core.get_friends(update=update) else: return self._retrieve_itchat_storage('memberList') @handle_response(Group) def groups(self, update=False, contact_only=False): """ 获取所有群聊 :param update: 是否更新 :param contact_only: 是否限于保存为联系人的群聊 :return: 群聊合集 """ # itchat 原代码有些难懂,似乎 itchat 中的 get_contact() 所获取的内容视其 update 参数而变化 # 如果 update=False 获取所有类型的本地聊天对象 # 反之如果 update=True,变为获取收藏的聊天室 if update or contact_only: return self.core.get_chatrooms(update=update, contactOnly=contact_only) else: return self._retrieve_itchat_storage('chatroomList') @handle_response(MP) def mps(self, update=False): """ 获取所有公众号 :param update: 是否更新 :return: 聊天对象合集 """ if update: return self.core.get_mps(update=update) else: return self._retrieve_itchat_storage('mpList') @handle_response(User) def user_details(self, user_or_users, chunk_size=50): """ 获取单个或批量获取多个用户的详细信息(地区、性别、签名等),但不可用于群聊成员 :param user_or_users: 单个或多个用户对象或 user_name :param chunk_size: 分配请求时的单批数量,目前为 50 :return: 单个或多个用户用户的详细信息 """ def chunks(): total = ensure_list(user_or_users) for i in range(0, len(total), chunk_size): yield total[i:i + chunk_size] @handle_response() def process_one_chunk(_chunk): return self.core.update_friend(userName=get_user_name(_chunk)) if isinstance(user_or_users, (list, tuple)): ret = list() for chunk in chunks(): chunk_ret = process_one_chunk(chunk) if isinstance(chunk_ret, list): ret += chunk_ret else: ret.append(chunk_ret) return ret else: return process_one_chunk(user_or_users) def search(self, name=None, **attributes): """ 在所有类型的聊天对象中进行搜索 :param name: 名称 (可以是昵称、备注等) :param attributes: 属性键值对,键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东' :return: 匹配的聊天对象合集 """ return self.chats().search(name, **attributes) # add / create @handle_response() def add_friend(self, user, verify_content=''): """ 添加用户为好友 :param user: 用户对象或 user_name :param verify_content: 验证说明信息 """ return self.core.add_friend( userName=get_user_name(user), status=2, verifyContent=verify_content, autoUpdate=True ) def accept_friend(self, user, verify_content=''): """ 接受用户为好友 :param user: 用户对象或 user_name :param verify_content: 验证说明信息 :return: 新的好友对象 """ @handle_response() def do(): return self.core.add_friend( userName=get_user_name(user), status=3, verifyContent=verify_content, autoUpdate=True ) do() # 若上一步没有抛出异常,则返回该好友 for friend in self.friends(): if friend == user: return friend def create_group(self, users, topic=None): """ 创建一个新的群聊 :param users: 用户列表 :param topic: 群名称 :return: 若建群成功,返回一个新的群聊对象 """ @handle_response() def request(): return self.core.create_chatroom( memberList=ensure_list(wrap_user_name(users)), topic=topic or '' ) ret = request() user_name = ret.get('ChatRoomName') if user_name: return Group(self.core.update_chatroom(userName=user_name), self) else: raise ResponseError('Failed to create group:\n{}'.format(pformat(ret))) # messages def _process_message(self, msg): """ 处理接收到的消息 """ if not self.alive: return config = self.message_configs.get_config(msg) if not config: return def process(): # noinspection PyBroadException try: ret = config.func(msg) if ret is not None: self.core.send(msg=str(ret), toUserName=msg.sender.user_name) except: logger.exception('\nAn error occurred in {}.'.format(config.func)) if config.run_async: Thread(target=process).start() else: process() def register( self, senders=None, msg_types=None, except_self=True, run_async=True, enabled=True ): """ 装饰器:用于注册消息配置 :param senders: 单个或列表形式的多个聊天对象或聊天类型,为空时匹配所有聊天对象 :param msg_types: 单个或列表形式的多个消息类型,为空时匹配所有消息类型 (SYSTEM 类消息除外) :param except_self: 排除自己在手机上发送的消息 :param run_async: 异步执行配置的函数,可提高响应速度 :param enabled: 当前配置的默认开启状态,可事后动态开启或关闭 """ def register(func): self.message_configs.append(MessageConfig( bot=self, func=func, senders=senders, msg_types=msg_types, except_self=except_self, run_async=run_async, enabled=enabled )) return func return register def start(self, block=True): """ 开始监听和处理消息 :param block: 是否堵塞线程,为 False 时将在新的线程中运行 """ def listen(): logger.info('{} Auto-reply started.'.format(self)) try: while self.alive: msg = Message(self.core.msgList.get(), self) if msg.type is not SYSTEM: self.messages.append(msg) self._process_message(msg) except KeyboardInterrupt: logger.info('KeyboardInterrupt received, ending...') self.alive = False if self.core.useHotReload: self.dump_login_status() logger.info('Bye.') if block: listen() else: t = Thread(target=listen, daemon=True) t.start()