[
  {
    "path": ".gitignore",
    "content": ".idea\n*.pkl\n_installed_files.txt\n_private\n\n### Linux template\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n### Windows template\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n### macOS template\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\nwxpy/main.py\n\n"
  },
  {
    "path": "LICENSE",
    "content": "**The MIT License**\n\nCopyright 2017 [Youfou](https://github.com/youfou)\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.rst LICENSE\n"
  },
  {
    "path": "README.rst",
    "content": "wxpy: 用 Python 玩微信\n==============================\n\n.. image:: https://badge.fury.io/py/wxpy.svg\n    :target: https://badge.fury.io/py/wxpy\n\n.. image:: https://img.shields.io/pypi/pyversions/wxpy.svg\n        :target: https://github.com/youfou/wxpy\n\n.. image:: https://readthedocs.org/projects/wxpy/badge/?version=latest\n    :target: http://wxpy.readthedocs.io/zh/latest/?badge=latest\n\n微信机器人 / 可能是最优雅的微信个人号 API\n    wxpy 在 itchat 的基础上，通过大量接口优化提升了模块的易用性，并进行丰富的功能扩展\n\n\n..  attention::\n\n    | **强烈建议仅使用小号运行机器人！**\n\n    | 从近期 (17年6月下旬) 反馈来看，使用机器人存在一定概率被限制登录的可能性。\n    | 主要表现为无法登陆 Web 微信 (但不影响手机等其他平台)。\n\n\n\n用来干啥\n----------------\n\n一些常见的场景\n\n* 控制路由器、智能家居等具有开放接口的玩意儿\n* 运行脚本时自动把日志发送到你的微信\n* 加群主为好友，自动拉进群中\n* 跨号或跨群转发消息\n* 自动陪人聊天\n* 逗人玩\n* ...\n\n总而言之，可用来实现各种微信个人号的自动化操作\n\n\n..\n    体验一下\n    ----------------\n\n    **这有一个现成的微信机器人，想不想调戏一下？**\n\n    记得填写入群口令 👉 [ **wxpy** ]，与群里的大神们谈笑风生 😏\n\n    ..  image:: https://github.com/youfou/wxpy/raw/master/docs/wechat-group.png\n\n\n轻松安装\n----------------\n\nwxpy 支持 Python 3.4-3.6，以及 2.7 版本\n\n将下方命令中的 \"pip\" 替换为 \"pip3\" 或 \"pip2\"，可确保安装到对应的 Python 版本中\n\n1. 从 PYPI 官方源下载安装 (在国内可能比较慢或不稳定):\n\n..  code:: shell\n\n    pip install -U wxpy\n\n2. 从豆瓣 PYPI 镜像源下载安装 (**推荐国内用户选用**):\n\n..  code:: shell\n\n    pip install -U wxpy -i \"https://pypi.doubanio.com/simple/\"\n\n\n简单上手\n----------------\n\n\n登陆微信:\n\n..  code:: python\n\n    # 导入模块\n    from wxpy import *\n    # 初始化机器人，扫码登陆\n    bot = Bot()\n\n找到好友:\n\n..  code:: python\n\n    # 搜索名称含有 \"游否\" 的男性深圳好友\n    my_friend = bot.friends().search('游否', sex=MALE, city=\"深圳\")[0]\n\n发送消息:\n\n..  code:: python\n\n    # 发送文本给好友\n    my_friend.send('Hello WeChat!')\n    # 发送图片\n    my_friend.send_image('my_picture.jpg')\n\n自动响应各类消息:\n\n..  code:: python\n\n    # 打印来自其他好友、群聊和公众号的消息\n    @bot.register()\n    def print_others(msg):\n        print(msg)\n\n    # 回复 my_friend 的消息 (优先匹配后注册的函数!)\n    @bot.register(my_friend)\n    def reply_my_friend(msg):\n        return 'received: {} ({})'.format(msg.text, msg.type)\n\n    # 自动接受新的好友请求\n    @bot.register(msg_types=FRIENDS)\n    def auto_accept_friends(msg):\n        # 接受好友请求\n        new_friend = msg.card.accept()\n        # 向新的好友发送消息\n        new_friend.send('哈哈，我自动接受了你的好友请求')\n\n保持登陆/运行:\n\n..  code:: python\n\n    # 进入 Python 命令行、让程序保持运行\n    embed()\n\n    # 或者仅仅堵塞线程\n    # bot.join()\n\n\n模块特色\n----------------\n\n* 全面对象化接口，调用更优雅\n* 默认多线程响应消息，回复更快\n* 包含 聊天机器人、共同好友 等 `实用组件 <http://wxpy.readthedocs.io/zh/latest/utils.html>`_\n* 只需两行代码，在其他项目中用微信接收警告\n* `愉快的探索和调试 <http://wxpy.readthedocs.io/zh/latest/console.html>`_，无需涂涂改改\n* 可混合使用 itchat 的原接口\n* 当然，还覆盖了各类常见基本功能:\n\n    * 发送文本、图片、视频、文件\n    * 通过关键词或用户属性搜索 好友、群聊、群成员等\n    * 获取好友/群成员的昵称、备注、性别、地区等信息\n    * 加好友，建群，邀请入群，移出群\n\n说明文档\n----------------\n\nhttp://wxpy.readthedocs.io\n\n更新日志\n----------------\n\nhttps://github.com/youfou/wxpy/releases\n\n项目主页\n----------------\n\nhttps://github.com/youfou/wxpy\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the shell_entry line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = wxpy\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "docs/bot.rst",
    "content": "机器人对象\n==============================\n\n..  module:: wxpy\n\n机器人 :class:`Bot` 对象可被理解为一个 Web 微信客户端。\n\n\n..  note::\n\n    | 关于发送消息，请参见 :doc:`chats`。\n    | 关于消息对象和自动处理，请参见 :doc:`messages`。\n\n\n初始化/登陆\n----------------\n\n..  note::\n\n    :class:`Bot` 在初始化时便会执行登陆操作，需要手机扫描登陆。\n\n..  autoclass:: Bot\n\n..  automethod:: Bot.enable_puid\n\n\n..  attribute:: Bot.auto_mark_as_read\n\n    为 True 时，将自动消除手机端的新消息小红点提醒 (默认为 False)\n\n\n获取聊天对象\n----------------\n\n..  attribute:: Bot.self\n\n    机器人自身 (作为一个聊天对象)\n\n    若需要给自己发送消息，请先进行以下一次性操作::\n\n        # 在 Web 微信中把自己加为好友\n        bot.self.add()\n        bot.self.accept()\n\n        # 发送消息给自己\n        bot.self.send('能收到吗？')\n\n\n..  attribute:: Bot.file_helper\n\n    文件传输助手\n\n..  automethod:: Bot.friends\n\n..  automethod:: Bot.groups\n\n..  automethod:: Bot.mps\n\n..  automethod:: Bot.chats\n\n\n搜索聊天对象\n----------------\n\n..  note::\n\n    * 通过 `.search()` 获得的搜索结果 **均为列表**\n    * 若希望找到唯一结果，可使用 :any:`ensure_one()`\n\n搜索好友::\n\n    # 搜索名称包含 '游否' 的深圳男性好友\n    found = bot.friends().search('游否', sex=MALE, city='深圳')\n    # [<Friend: 游否>]\n    # 确保搜索结果是唯一的，并取出唯一结果\n    youfou = ensure_one(found)\n    # <Friend: 游否>\n\n搜索群聊::\n\n    # 搜索名称包含 'wxpy'，且成员中包含 `游否` 的群聊对象\n    wxpy_groups = bot.groups().search('wxpy', [youfou])\n    # [<Group: wxpy 交流群 1>, <Group: wxpy 交流群 2>]\n\n在群聊中搜素::\n\n    # 在刚刚找到的第一个群中搜索\n    group = wxpy_groups[0]\n    # 搜索该群中所有浙江的群友\n    found = group.search(province='浙江')\n    # [<Member: 浙江群友 1>, <Group: 浙江群友 2>, <Group: 浙江群友 3> ...]\n\n搜索任何类型的聊天对象 (但不包含群内成员) ::\n\n    # 搜索名称含有 'wxpy' 的任何聊天对象\n    found = bot.search('wxpy')\n    # [<Friend: wxpy 机器人>, <Group: wxpy 交流群 1>, <Group: wxpy 交流群 2>]\n\n加好友和建群\n----------------\n\n..  automethod:: Bot.add_friend\n\n..  automethod:: Bot.add_mp\n\n..  automethod:: Bot.accept_friend\n\n自动接受好友请求::\n\n    # 注册好友请求类消息\n    @bot.register(msg_types=FRIENDS)\n    # 自动接受验证信息中包含 'wxpy' 的好友请求\n    def auto_accept_friends(msg):\n        # 判断好友请求中的验证文本\n        if 'wxpy' in msg.text.lower():\n            # 接受好友 (msg.card 为该请求的用户对象)\n            new_friend = bot.accept_friend(msg.card)\n            # 或 new_friend = msg.card.accept()\n            # 向新的好友发送消息\n            new_friend.send('哈哈，我自动接受了你的好友请求')\n\n..  automethod:: Bot.create_group\n\n\n其他\n----------------\n\n..  automethod:: Bot.user_details\n\n..  automethod:: Bot.upload_file\n\n..  automethod:: Bot.join\n\n..  automethod:: Bot.logout\n\n\n控制多个微信 (多开)\n--------------------------------\n\n仅需初始化多个 :class:`Bot` 对象，即可同时控制多个微信::\n\n    bot1 = Bot()\n    bot2 = Bot()\n\n"
  },
  {
    "path": "docs/chats.rst",
    "content": "聊天对象\n==============================\n\n..  module:: wxpy\n\n通过机器人对象 :class:`Bot <Bot>` 的\n:meth:`chats() <Bot.chats>`,\n:meth:`friends() <Bot.friends>`，:meth:`groups() <Bot.groups>`,\n:meth:`mps() <Bot.mps>` 方法,\n可分别获取到当前机器人的 所有聊天对象、好友、群聊，以及公众号列表。\n\n而获得到的聊天对象合集 :class:`Chats` 和 :class:`Groups` 具有一些合集方法，例如：:meth:`Chats.search` 可用于按条件搜索聊天对象::\n\n    from wxpy import *\n    bot = Bot()\n    my_friend = bot.friends().search('游否', sex=MALE, city='深圳')[0]\n    # <Friend: 游否>\n\n在找到好友(或其他聊天对象)后，还可使用该聊天对象的 :meth:`send <Chat.send>` 系列方法，对其发送消息::\n\n    # 发送文本\n    my_friend.send('Hello, WeChat!')\n    # 发送图片\n    my_friend.send_image('my_picture.png')\n    # 发送视频\n    my_friend.send_video('my_video.mov')\n    # 发送文件\n    my_friend.send_file('my_file.zip')\n    # 以动态的方式发送图片\n    my_friend.send('@img@my_picture.png')\n\n\n各类型的继承关系\n--------------------------------------\n\n在继续了解各个聊天对象之前，我们需要首先 **理解各种不同类型聊天对象的继承关系**\n\n基础类\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n所有聊天对象，均继承于以下两种基础类，并拥有相应的属性和方法。\n\n基本聊天对象 :class:`Chat`\n    * 所有的聊天对象均继承于此类型\n    * 拥有 微信ID、昵称 等属性\n    * 拥有 发送消息 :meth:`Chat.send`, 获取头像 :meth:`Chat.get_avatar` 等方法\n\n单个聊天对象 :class:`User`\n    * 继承于 :class:`Chat`，表示个体聊天对象 (而非群聊)。\n    * 被以下聊天对象所继承\n        * 好友对象 :class:`Friend`\n        * 群成员对象 :class:`Member`\n        * 公众号对象 :class:`MP`\n    * 拥有 性别、省份、城市、是否为好友 等属性\n    * 拥有 加为好友 :meth:`User.add`, 接受为好友 :meth:`User.accept` 等方法\n\n实际类\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n在实际使用过程中，我们会更多的用到以下实际聊天对象类型。\n\n..  tip:: 请牢记，除了自身私有的属性和方法外，它们还拥有对应基础类的属性和方法 (未重复列出)。\n\n* 好友 :class:`Friend`\n* 群聊 :class:`Group`\n* 群成员 :class:`Member`\n* 公众号 :class:`MP`\n\n..  note::\n\n    **阅读以下内容，你将了解:**\n\n        * 如何获取他们的各种属性 (ID、昵称、性别、地区、是否为好友关系等)\n        * 如何对他们进行发送消息、加为好友、加入群聊、下载头像 等操作\n\n\n基本聊天对象\n--------------------------------------\n\n所有聊天对象都继承于\"基本聊天对象\"，并拥有相应的属性和方法。\n\n..  autoclass:: Chat\n    :members:\n\n    ..  attribute:: bot\n\n        所属的 :class:`机器人对象 <Bot>`\n\n    ..  attribute:: raw\n\n        原始数据\n\n\n\n单个聊天对象\n--------------------------------------\n\n..  autoclass:: User\n    :members:\n\n好友\n-------------------\n\n..  autoclass:: Friend\n    :members:\n\n群聊\n-------------------\n\n..  autoclass:: Group\n    :members:\n\n\n群成员\n^^^^^^^^^^^^^^^^^^^^\n\n..  autoclass:: Member\n    :members:\n\n实用技巧\n^^^^^^^^^^^^^^^^^^^^\n\n判断一位用户是否在群中只需用 `in` 语句::\n\n    friend = bot.friends().search('游否')[0]\n    group = bot.groups().search('wxpy 交流群')[0]\n\n    if friend in group:\n        print('是的，{} 在 {} 中！'.format(friend.name, group.name))\n        # 是的，游否 在 wxpy 交流群 中！\n\n若要遍历群成员，可直接对群对象使用 `for` 语句::\n\n    # 打印所有群成员\n    for member in group:\n        print(member)\n\n若需查看群成员数量，直接使用 `len()` 即可::\n\n    len(group) # 这个群的成员数量\n\n若需判断一位群成员是否就是某个好友，使用 `==` 即可::\n\n    member = group.search('游否')[0]\n    if member == friend:\n        print('{} is {}'.format(member, friend))\n        # <Member: 游否> is <Friend: 游否>\n\n\n公众号\n-------------------\n\n..  autoclass:: MP\n    :members:\n\n聊天对象合集\n-------------------\n\n好友、公众号、群聊成员的合集\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n在 :class:`Chats` 对象中，除了最常用到的 :meth:`search() <Chats.search>` 外，还有两个特别的方法，:meth:`stats() <Chats.stats>` 与 :meth:`stats_text() <Chats.stats_text>`，可用来统计好友或群成员的性别和地区分布::\n\n    bot.friends().stats_text()\n    # 游否 共有 100 位微信好友\\n\\n男性: 67 (67.0%)\\n女性: 23 (23.0%) ...\n\n..  autoclass:: Chats\n    :members:\n\n群聊的合集\n^^^^^^^^^^^^^^^^^^^^\n\n..  autoclass:: Groups\n    :members:\n"
  },
  {
    "path": "docs/conf.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# wxpy documentation build configuration file, created by\n# sphinx-quickstart on Sat Feb 25 23:57:26 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath('..'))\n\nimport wxpy\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n]\n\n# For Read the Docs\non_rtd = os.environ.get('READTHEDOCS', None) == 'True'\nif on_rtd:\n    html_theme = 'default'\nelse:\n    import sphinx_rtd_theme\n\n    html_theme = 'sphinx_rtd_theme'\n    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\n    extensions.append('sphinx.ext.githubpages')\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'wxpy'\ncopyright = wxpy.__copyright__\nauthor = wxpy.__author__\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = wxpy.__version__\n# The full version, including alpha/beta/rc tags.\nrelease = wxpy.__version__\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the shell_entry line for these cases.\nlanguage = 'zh_CN'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n# html_theme = 'nature'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'wxpydoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'wxpy.tex', 'wxpy Documentation',\n     author, 'manual'),\n]\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'wxpy', 'wxpy Documentation',\n     [author], 1)\n]\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'wxpy', 'wxpy 文档',\n     author, 'wxpy', '微信个人号 API，用 Python 玩微信',\n     'API'),\n]\n\n# -- Options for Epub output ----------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\nepub_author = author\nepub_publisher = author\nepub_copyright = copyright\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\nautoclass_content = 'both'\nautodoc_member_order = 'bysource'\n\nsuppress_warnings = ['image.nonlocal_uri']\n"
  },
  {
    "path": "docs/console.rst",
    "content": "愉快的探索和调试\n==============================\n\n..  module:: wxpy\n\n想要做点小试验，调试代码，或是探索 wxpy 的功能特性？反复修改和运行太麻烦。\n\n试试下面两种玩法，告别涂涂改改的摸索方式。\n\n\n使用 `embed()`\n------------------------------\n\n..  note:: 适用于在现有的代码中进行探索和调试\n\n只需将 :any:`embed()` 放在代码中的任何位置。运行后，就可以从那儿开始探索和调试。\n\n例如，初始化一个机器人，然后看看它能做些什么::\n\n    from wxpy import *\n    bot = Bot()\n    embed() # 进入 Python 命令行\n\n    # 输入对象名称并回车\n    >>> bot\n    # Out[1]: <Bot: 游否>\n    >>> bot.friends()\n    # Out[2]: [<Friend: 路人甲>, <Friend: 路人乙>, <Friend: 路人丙>]\n\n\n..  autofunction:: embed\n    :noindex:\n\n\n使用 `wxpy` 命令\n------------------------------\n\n..  highlight:: shell\n\n..  note:: 适用于在命令行中边写边探索\n\n第二种情况：想要简单写几行，而不想创建脚本，那么使用 `wxpy` 命令行边写边探索，更方便。\n\n在命令行中输入 `wxpy -h` 可快速查看使用说明。\n\n\n选项\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n* bot1 bot2 bot3...\n    * 一个或多个需要初始化的机器人对象的名称，以空格分割\n    * 默认：不初始化机器人\n    * 例子: `bot1 bot2`\n\n* -c / --cache\n    * 使用会话缓存功能，将创建 `wxpy_*.pkl` 缓存文件\n    * 默认：不缓存会话\n    * 例子：`-c`\n\n* -q 宽度 / --console_qr 宽度\n    * 终端二维码的单元格宽度\n    * 默认：不使用终端二维码\n    * 例子：`-q 2`\n\n* -l 等级 / --logging_level 等级 (注意是小写 L，不是 I)\n    * 日志等级\n    * 默认：`INFO`\n    * 例子：`-l DEBUG`\n\n* -s 交互界面 / --shell 交互界面\n    * 选择所需使用的 Python 交互界面\n    * 可为：`ipython`，`bpython`，`python`，或它们的首字母\n    * 默认：以上首个可用的 Python 命令行\n    * 例子：`-s bpython`\n\n* -v / --version\n    * 展示版本信息并退出z\n    * 例子：`-v`\n\n\n例子\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n初始化一个名为 `bot` 的机器人::\n\n    wxpy bot\n\n在此基础上，使用终端二维码，且单元格宽度为 2::\n\n    wxpy bot -q 2\n\n分别初始化名为 `bot1` 和 `bot2` 的两个机器人::\n\n    wxpy bot1 bot2\n\n在此基础上，使用会话缓存功能::\n\n    wxpy bot1 bot2 -c\n\n在此基础上，指定使用 bpython::\n\n    wxpy bot1 bot2 -c -s bpython\n\n"
  },
  {
    "path": "docs/faq.rst",
    "content": "必看: 常见问题 FAQ\n==============================\n\n..  module:: wxpy\n\n..  hint::\n\n    这里罗列了一些常见的问题，在提出新的问题前，请先看完本文。\n\n\n启动后马上退出了？\n--------------------------------\n\n因为主线程执行完成了，程序自然会退出。\n\n只需在代码结尾加一句 :any:`embed()` 即可堵塞线程，还能进入 Python 命令行::\n\n    from wxpy import *\n\n    # 你的其他代码...\n\n    # 堵塞线程，并进入 Python 命令行\n    embed()\n\n或者，也可以使用 :any:`Bot.join()` 仅仅堵塞线程::\n\n    bot = Bot()\n\n    # 你的其他代码...\n\n    # 仅仅堵塞线程\n    bot.join()\n\n    # 机器人登出后会继续往下执行\n\n\n每次登陆都要扫码？\n--------------------------------\n\n可启用登陆状态缓存功能，在短时间内重新运行程序，可自动登录。\n\n具体请见 :any:`Bot` 中的 `cache_path` 参数说明。\n\n\n可以在 Linux 中使用吗？\n----------------------------------------------------------------\n\nwxpy 不依赖于图形界面，因此完全兼容各种纯终端的服务器。\n\n但有一点需要注意，在纯终端环境中，登陆时必须使用\"终端二维码\"参数。\n\n具体请见 :any:`Bot` 中的 `console_qr` 参数说明。\n\n..  tip::\n\n    遇到以下错误？请使用 :any:`Bot` 的 `console_qr` 参数。 ::\n\n        FileNotFoundError: [Errno 2] No such file or directory: 'xdg-open'\n\n\n支持 红包、转账、朋友圈… 吗？\n--------------------------------\n\nwxpy 使用了 Web 微信的通讯协议，因此仅能覆盖 Web 微信本身所具备的功能。\n\n所以以下功能目前 **均不支持**\n\n* 支付相关 - 红包、转账、收款 等都不支持\n* 在群聊中@他人 - 是的，Web 微信中被人@后也不会提醒\n* 发送名片 - 但可以通过 :any:`send_raw_msg()` 转发\n* 发送分享链接 - 也无法转发\n* 发送语音消息\n* 朋友圈相关\n\n\n为什么要开发 wxpy？\n--------------------------------\n\nwxpy 的初衷是帮助人们利用微信来使生活和工作更轻松。\n\n..  note::\n\n    希望每位使用者在使用机器人时\n\n    * 维护良好的交流环境\n    * 永远不骚扰他人\n    * 遵守法律和平台规则\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. wxpy documentation master file, created by\n   sphinx-quickstart on Sat Feb 25 23:57:26 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\n\nwxpy: 用 Python 玩微信\n==============================\n\n.. image:: https://badge.fury.io/py/wxpy.svg\n    :target: https://pypi.python.org/pypi/wxpy/\n\n.. image:: https://img.shields.io/pypi/pyversions/wxpy.svg\n        :target: https://github.com/youfou/wxpy\n\n.. image:: https://readthedocs.org/projects/wxpy/badge/?version=latest\n    :target: http://wxpy.readthedocs.io/zh/latest/?badge=latest\n\n微信机器人 / 可能是最优雅的微信个人号 API\n    wxpy 在 itchat 的基础上，通过大量接口优化提升了模块的易用性，并进行丰富的功能扩展\n\n\n..  attention::\n\n    | **强烈建议仅使用小号运行机器人！**\n\n    | 从近期 (17年6月下旬) 反馈来看，使用机器人存在一定概率被限制登录的可能性。\n    | 主要表现为无法登陆 Web 微信 (但不影响手机等其他平台)。\n\n\n项目主页\n----------------\n\nhttps://github.com/youfou/wxpy\n\n\n用来干啥\n----------------\n\n一些常见的场景\n\n* 控制路由器、智能家居等具有开放接口的玩意儿\n* 运行脚本时自动把日志发送到你的微信\n* 加群主为好友，自动拉进群中\n* 跨号或跨群转发消息\n* 自动陪人聊天\n* 逗人玩\n* ...\n\n总而言之，可用来实现各种微信个人号的自动化操作\n\n..\n    体验一下\n    ----------------\n\n    **这有一个现成的微信机器人，想不想调戏一下？**\n\n    记得填写入群口令 👉 [ **wxpy** ]，与群里的大神们谈笑风生 😏\n\n    ..  image:: wechat-group.png\n\n\n轻松安装\n----------------\n\nwxpy 支持 Python 3.4-3.6，以及 2.7 版本\n\n将下方命令中的 \"pip\" 替换为 \"pip3\" 或 \"pip2\"，可确保安装到对应的 Python 版本中\n\n1. 从 PYPI 官方源下载安装 (在国内可能比较慢或不稳定):\n\n..  code:: shell\n\n    pip install -U wxpy\n\n2. 从豆瓣 PYPI 镜像源下载安装 (**推荐国内用户选用**):\n\n..  code:: shell\n\n    pip install -U wxpy -i \"https://pypi.doubanio.com/simple/\"\n\n简单上手\n----------------\n\n..  automodule:: wxpy\n\n\n模块特色\n----------------\n\n* 全面对象化接口，调用更优雅\n* 默认多线程响应消息，回复更快\n* 包含 聊天机器人、共同好友 等 :doc:`实用组件 <utils>`\n* 只需两行代码，在其他项目中 :doc:`用微信接收警告 <logging_with_wechat>`\n* :doc:`愉快的探索和调试 <console>`，无需涂涂改改\n* 可混合使用 itchat 的原接口\n* 当然，还覆盖了各类常见基本功能:\n\n    * 发送文本、图片、视频、文件\n    * 通过关键词或用户属性搜索 好友、群聊、群成员等\n    * 获取好友/群成员的昵称、备注、性别、地区等信息\n    * 加好友，建群，邀请入群，移出群\n\n更新日志\n----------------\n\nhttps://github.com/youfou/wxpy/releases\n\n\n文档目录\n----------------\n\n..  toctree::\n    :maxdepth: 2\n\n    bot\n    chats\n    messages\n    logging_with_wechat\n    console\n    utils\n    response_error\n    itchat\n    faq\n\n"
  },
  {
    "path": "docs/itchat.rst",
    "content": "itchat 与原始数据\n==============================\n\n..  module:: wxpy\n\n\n正是得益于 |itchat| 的坚实基础，wxpy 才能够在短时间内快速实现这些新的接口和功能。\n\n感谢 itchat 维护者们的辛勤付出。\n\n以下为如何在 wxpy 中混合使用 itchat 的原接口和原始数据。\n\n\n..  |itchat| raw:: html\n\n    <a href=\"https://github.com/littlecodersh/itchat\" target=\"_blank\">itchat</a>\n\n\n使用 itchat 的原接口\n------------------------------\n\n只需在 wxpy 的 :class:`Bot` 对象后紧跟 `.core.*` 即可调用 itchat 的原接口。\n\n例如，使用 itchat 的 `search_friends` 接口::\n\n    from wxpy import *\n    bot = Bot()\n    found = bot.core.search_friends('游否')\n\n..  attention:: 通过 itchat 原接口所获取到的结果为原始数据，可能无法直接传递到 wxpy 的对应方法中。\n\n\n使用原始数据\n------------------------------\n\nwxpy 的所有 **聊天对象** 和 **消息对象** 均基于从 itchat 获取到的数据进行封装。若需使用原始数据，只需在对象后紧跟 `.raw`。\n\n例如，查看一个 :class:`好友 <Friend>` 对象的原始数据::\n\n    from wxpy import *\n    bot = Bot()\n    a_friend = bot.friends()[0]\n    print(a_friend.raw)\n\n"
  },
  {
    "path": "docs/logging_with_wechat.rst",
    "content": "用微信监控你的程序\n==============================\n\n..  module:: wxpy\n\n通过利用微信强大的通知能力，我们可以把程序中的警告/日志发到自己的微信上。\n\nwxpy 提供以下两种方式来实现这个需求。\n\n\n获得专用 Logger\n------------------------------\n\n.. autofunction:: get_wechat_logger\n\n::\n\n    from wxpy import get_wechat_logger\n\n    # 获得一个专用 Logger\n    # 当不设置 `receiver` 时，会将日志发送到随后扫码登陆的微信的\"文件传输助手\"\n    logger = get_wechat_logger()\n\n    # 发送警告\n    logger.warning('这是一条 WARNING 等级的日志，你收到了吗？')\n\n    # 接收捕获的异常\n    try:\n        1 / 0\n    except:\n        logger.exception('现在你又收到了什么？')\n\n\n加入到现有的 Logger\n------------------------------\n\n.. autoclass:: WeChatLoggingHandler\n\n::\n\n    import logging\n    from wxpy import WeChatLoggingHandler\n\n    # 这是你现有的 Logger\n    logger = logging.getLogger(__name__)\n\n    # 初始化一个微信 Handler\n    wechat_handler = WeChatLoggingHandler()\n    # 加到入现有的 Logger\n    logger.addHandler(wechat_handler)\n\n    logger.warning('你有一条新的告警，请查收。')\n\n\n指定接收者\n------------------------------\n\n当然，我们也可以使用其他聊天对象来接收日志。\n\n比如，先在微信中建立一个群聊，并在里面加入需要关注这些日志的人员。然后把这个群作为接收者。\n\n::\n\n    from wxpy import *\n\n    # 初始化机器人\n    bot = Bot()\n    # 找到需要接收日志的群 -- `ensure_one()` 用于确保找到的结果是唯一的，避免发错地方\n    group_receiver = ensure_one(bot.groups().search('XX业务-告警通知'))\n\n    # 指定这个群为接收者\n    logger = get_wechat_logger(group_receiver)\n\n    logger.error('打扰大家了，但这是一条重要的错误日志...')\n\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=wxpy\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' shell_entry was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/messages.rst",
    "content": "消息处理\n==============================\n\n..  module:: wxpy\n\n每当机器人接收到消息时，会自动执行以下两个步骤\n\n1. 将消息保存到 :class:`Bot.messages` 中\n2. 查找消息预先注册的函数，并执行(若有匹配的函数)\n\n\n消息对象\n----------------\n\n消息对象代表每一条从微信获取到的消息。\n\n\n基本属性\n^^^^^^^^^^^^^^^^\n\n..  autoattribute:: Message.type\n\n..  attribute:: Message.bot\n\n    接收此消息的 :class:`机器人对象 <Bot>`\n\n..  autoattribute:: Message.id\n\n\n内容数据\n^^^^^^^^^^^^^^^^\n\n..  autoattribute:: Message.text\n\n..  automethod:: Message.get_file\n\n..  autoattribute:: Message.file_name\n\n..  autoattribute:: Message.file_size\n\n..  autoattribute:: Message.media_id\n\n..  attribute:: Message.raw\n\n    原始数据 (dict 数据)\n\n用户相关\n^^^^^^^^^^^^^^^^\n\n\n..  autoattribute:: Message.chat\n\n..  autoattribute:: Message.sender\n\n..  autoattribute:: Message.receiver\n\n..  autoattribute:: Message.member\n\n..  autoattribute:: Message.card\n\n\n群聊相关\n^^^^^^^^^^^^^^^^\n\n..  autoattribute:: Message.member\n\n..  autoattribute:: Message.is_at\n\n时间相关\n^^^^^^^^^^^^^^^^^\n\n..  autoattribute:: Message.create_time\n\n..  autoattribute:: Message.receive_time\n\n..  autoattribute:: Message.latency\n\n\n其他属性\n^^^^^^^^^^^^^^^^\n\n..  autoattribute:: Message.url\n\n..  autoattribute:: Message.articles\n\n..  autoattribute:: Message.location\n\n..  autoattribute:: Message.img_height\n\n..  autoattribute:: Message.img_width\n\n..  autoattribute:: Message.play_length\n\n..  autoattribute:: Message.voice_length\n\n\n回复方法\n^^^^^^^^^^^^^^^^\n\n..  method:: Message.reply(...)\n\n    等同于 :meth:`Message.chat.send(...) <Chat.send>`\n\n..  method:: Message.reply_image(...)\n\n    等同于 :meth:`Message.chat.send_image(...) <Chat.send_image>`\n\n..  method:: Message.reply_file(...)\n\n    等同于 :meth:`Message.chat.send_file(...) <Chat.send_file>`\n\n..  method:: Message.reply_video(...)\n\n    等同于 :meth:`Message.chat.send_video(...) <Chat.send_video>`\n\n..  method:: Message.reply_msg(...)\n\n    等同于 :meth:`Message.chat.send_msg(...) <Chat.send_msg>`\n\n..  method:: Message.reply_raw_msg(...)\n\n    等同于 :meth:`Message.chat.send_raw_msg(...) <Chat.send_raw_msg>`\n\n\n转发消息\n^^^^^^^^^^^^^^^^\n\n..  automethod:: Message.forward\n\n\n自动处理消息\n---------------------\n\n可通过 **预先注册** 的方式，实现消息的自动处理。\n\n\n\"预先注册\" 是指\n    预先将特定聊天对象的特定类型消息，注册到对应的处理函数，以实现自动回复等功能。\n\n\n注册消息\n^^^^^^^^^^^^^^\n\n..  hint::\n\n    | 每当收到新消息时，将根据注册规则找到匹配条件的执行函数。\n    | 并将 :class:`消息对象 <Message>` 作为唯一参数传入该函数。\n\n将 :meth:`Bot.register` 作为函数的装饰器，即可完成注册。\n\n::\n\n    # 打印所有*群聊*对象中的*文本*消息\n    @bot.register(Group, TEXT)\n    def print_group_msg(msg):\n        print(msg)\n\n\n..  attention:: 优先匹配 **后注册** 的函数，且仅匹配 **一个** 注册函数。\n\n..  automethod:: Bot.register\n\n..  tip::\n\n    1.  `chats` 和 `msg_types` 参数可以接收一个列表或干脆一个单项。按需使用，方便灵活。\n    2.  `chats` 参数既可以是聊天对象实例，也可以是对象类。当为类时，表示匹配该类型的所有聊天对象。\n    3. 在被注册函数中，可以通过直接 `return <回复内容>` 的方式来回复消息，等同于调用 `msg.reply(<回复内容>)`。\n\n\n开始运行\n^^^^^^^^^^^^^^\n\n..  note::\n\n    | 在完成注册操作后，若没有其他操作，程序会因主线程执行完成而退出。\n    | **因此务必堵塞线程以保持监听状态！**\n    | wxpy 的 :any:`embed()` 可在堵塞线程的同时，进入 Python 命令行，方便调试，一举两得。\n\n\n::\n\n    from wxpy import *\n\n    bot = Bot()\n\n    @bot.register()\n    def print_messages(msg):\n        print(msg)\n\n    # 堵塞线程，并进入 Python 命令行\n    embed()\n\n\n..  autofunction:: embed\n\n\n示例代码\n^^^^^^^^^^^^^\n\n在以下例子中，机器人将\n\n* 忽略 \"一个无聊的群\" 的所有消息\n* 回复好友 \"游否\" 和其他群聊中被 @ 的 TEXT 类消息\n* 打印所有其他消息\n\n初始化机器人，并找到好友和群聊::\n\n    from wxpy import *\n    bot = Bot()\n    my_friend = bot.friends().search('游否')[0]\n    boring_group = bot.groups().search('一个无聊的群')[0]\n\n\n打印所有其他消息::\n\n    @bot.register()\n    def just_print(msg):\n        # 打印消息\n        print(msg)\n\n\n回复好友\"游否\"和其他群聊中被 @ 的 TEXT 类消息::\n\n    @bot.register([my_friend, Group], TEXT)\n    def auto_reply(msg):\n        # 如果是群聊，但没有被 @，则不回复\n        if isinstance(msg.chat, Group) and not msg.is_at:\n            return\n        else:\n            # 回复消息内容和类型\n            return '收到消息: {} ({})'.format(msg.text, msg.type)\n\n\n忽略\"一个无聊的群\"的所有消息::\n\n    @bot.register(boring_group)\n    def ignore(msg):\n        # 啥也不做\n        return\n\n\n堵塞线程，并进入 Python 命令行::\n\n    embed()\n\n\n动态开关注册配置\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n..  note:: 该操作需要在额外的线程中进行！\n\n\n查看当前的注册配置情况::\n\n    bot.registered\n    # [<MessageConfig: just_print (Async, Enabled)>,\n    #  <MessageConfig: auto_reply (Async, Enabled)>,\n    #  <MessageConfig: ignore (Async, Enabled)>]\n\n\n关闭所有注册配置::\n\n    bot.registered.disable()\n\n重新开启 `just_print` 函数::\n\n    bot.registered.enable(just_print)\n\n\n查看当前开启的注册配置::\n\n    bot.registered.enabled\n    # [<MessageConfig: just_print (Async, Enabled)>]\n\n\n..  autoclass:: wxpy.api.messages.Registered\n    :members:\n\n已发送消息\n----------------\n\n..  autoclass:: SentMessage\n\n    ..  hint:: 大部分属性与 :class:`Message` 相同\n\n    ..  automethod:: recall\n\n历史消息\n----------------\n\n可通过访问 `bot.messages` 来查看历史消息列表。\n\n消息列表为 :class:`Messages` 对象，具有搜索功能。\n\n例如，搜索所有自己在手机上发出的消息::\n\n    sent_msgs = bot.messages.search(sender=bot.self)\n    print(sent_msgs)\n\n\n..  autoclass:: Messages\n\n    ..  attribute:: max_history\n\n        设置最大保存条数，即：仅保存最后的 n 条消息。\n\n        ::\n\n            bot = Bot()\n            # 设置历史消息的最大保存数量为 10000 条\n            bot.messages.max_history = 10000\n\n    ..  automethod:: search\n\n        ::\n\n            # 搜索所有自己发送的，文本中包含 'wxpy' 的消息\n            bot.messages.search('wxpy', sender=bot.self)\n\n"
  },
  {
    "path": "docs/response_error.rst",
    "content": "异常处理\n==============================\n\n\n异常的抛出和捕捉\n--------------------\n\n\n..  module:: wxpy\n\n每当使用 wxpy 向微信发出请求 (例如发送消息、加好友、建群等操作)，wxpy 都会在收到服务端响应后进行检查。\n\n若响应中的错误码不为 0，程序将抛出 :class:`ResponseError` 异常。\n\n..  autoclass:: ResponseError\n\n    ..  attribute:: err_code\n\n        错误码 (int)\n\n    ..  attribute:: err_msg\n\n        错误消息 (文本)，但可能为空\n\n捕捉异常::\n\n    try:\n        # 尝试向某个群员发送消息\n        group.members[3].send('Hello')\n    except ResponseError as e:\n        # 若群员还不是好友，将抛出 ResponseError 错误\n        print(e.err_code, e.err_msg) # 查看错误号和错误消息\n\n\n已知错误码\n--------------------\n\n通常来说，每个错误码表示一种类型的错误。\n\n但因微信未公开 (也没有义务公开) 这套错误码体系的具体说明，我们只能根据经验猜测部分错误码的定义。\n\n以下为一些常见的已知错误码。欢迎提交 PR `进行完善`_。\n\n    .. _进行完善: https://github.com/youfou/wxpy/blob/master/docs/response_error.rst\n\n\n1205\n^^^^^^^^^^^^^^^^^^^^^^^\n\n通常因为操作频率过高。需要控制频率，避免再次引起该错误。\n\n..  attention:: Web 微信对 **加好友、建群** 这两种操作的频率限制尤其严格！\n\n对于微信而言，为了机器人避免打扰其他用户，以及控制服务器的负载压力，需要对各种不同的操作进行频率限制。\n\n通常每种操作可有多层频率限制，而每层频率限制分为两个参数:\n\n    周期、次数，分布表示: 在 x 周期内，只能发送 y 个请求。\n\n举个例子：\n\n    对于 **发送消息** 操作，可能会是这样 (数值为虚构):\n\n        +----+----------+----------+\n        | 层 | 限制周期 | 限制次数 |\n        +----+----------+----------+\n        | 1  | 2 分钟   | 120      |\n        +----+----------+----------+\n        | 2  | 10 分钟  |  300     |\n        +----+----------+----------+\n        | 3  | 1 小时   |  1000    |\n        +----+----------+----------+\n        | 4  | 24 小时  | 2000     |\n        +----+----------+----------+\n\n        | 可能会有用户在 1 分钟内狂发 100 条消息。\n        | 但这样的频率不可能维持一整天，所以一天内 3000 条是足够的。\n\n    通过以上方式，微信可实现较为合理的限制。\n\n\n1204\n^^^^^^^^^^^^^^^^^^^^^^^\n\n通常因为操作对象不为好友关系。例如尝试向一位不为好友的群员发送消息时，会引起这个错误。\n\n1100, 1101, 1102\n^^^^^^^^^^^^^^^^^^^^^^^\n\n通常表示机器人已经掉线，需要重新登录。\n\n请重新初始化 :class:`Bot` 对象，并重新注册消息。\n\n    因为重新登录后，聊天对象的 `user_name` 可能已经变化，所以原先的消息注册也会因此失效。\n"
  },
  {
    "path": "docs/utils.rst",
    "content": "实用组件\n==============================\n\n..  module:: wxpy\n\n额外内置了一些实用的小组件，可按需使用。\n\n\n聊天机器人\n------------------------------\n\n目前提供了以下两种自动聊天机器人接口。\n\n\n图灵\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n..  autoclass:: Tuling\n    :members:\n\n    ::\n\n        bot = Bot()\n        my_friend = ensure_one(bot.search('游否'))\n        tuling = Tuling(api_key='你申请的 API KEY')\n\n        # 使用图灵机器人自动与指定好友聊天\n        @bot.register(my_friend)\n        def reply_my_friend(msg):\n            tuling.do_reply(msg)\n\n\n小 i\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n..  autoclass:: XiaoI\n    :members:\n\n    ::\n\n        bot = Bot()\n        my_friend = ensure_one(bot.search('寒风'))\n        xiaoi = XiaoI('你申请的 Key', '你申请的 Secret')\n\n        # 使用小 i 机器人自动与指定好友聊天\n        @bot.register(my_friend)\n        def reply_my_friend(msg):\n            xiaoi.do_reply(msg)\n\n\n查找共同好友\n------------------------------\n\n..  autofunction:: mutual_friends\n\n    ::\n\n        bot1 = Bot()\n        bot2 = Bot()\n\n        # 打印共同好友\n        for mf in mutual_friends(bot, bot2):\n            print(mf)\n\n\n确保查找结果的唯一性\n------------------------------\n\n..  autofunction:: ensure_one\n\n    ::\n\n        bot = Bot()\n        # 确保只找到了一个叫\"游否\"的好友，并返回这个好友\n        my_friend = ensure_one(bot.search('游否'))\n        # <Friend: 游否>\n\n\n在多个群中同步消息\n------------------------------\n\n..  autofunction:: sync_message_in_groups\n\n\n检测频率限制\n------------------------------\n\n..  autofunction:: detect_freq_limit\n\n    例如，测试发送文本消息的频率限制::\n\n        bot = Bot('test.pkl')\n\n        # 定义需要检测的操作\n        def action():\n            bot.file_helper.send()\n\n        # 执行检测\n        result = detect_freq_limit(action)\n        # 查看结果\n        print(result)\n        # (120, 120.111222333)\n\n\n忽略 `ResponseError` 异常\n------------------------------\n\n..  autofunction:: dont_raise_response_error\n\n"
  },
  {
    "path": "requirements.txt",
    "content": "itchat==1.2.32\nrequests\nfuture"
  },
  {
    "path": "setup.cfg",
    "content": "[aliases]\nrelease=sdist build egg_info upload\n"
  },
  {
    "path": "setup.py",
    "content": "# coding: utf-8\n# from __future__ import unicode_literals\n\nimport re\nimport codecs\n\nfrom setuptools import find_packages, setup\n\nwith codecs.open('wxpy/__init__.py', encoding='utf-8') as fp:\n    version = re.search(r\"__version__\\s*=\\s*'([\\w\\-.]+)'\", fp.read()).group(1)\n\nwith codecs.open('README.rst', encoding='utf-8') as fp:\n    readme = fp.read()\n\nsetup(\n    name='wxpy',\n    version=version,\n    packages=find_packages(),\n    include_package_data=True,\n    entry_points={\n        'console_scripts': [\n            'wxpy = wxpy.utils:shell_entry'\n        ]\n    },\n    install_requires=[\n        'itchat==1.2.32',\n        'requests',\n        'future',\n    ],\n    tests_require=[\n        'pytest',\n    ],\n    url='https://github.com/youfou/wxpy',\n    license='MIT',\n    author='Youfou',\n    author_email='youfou@qq.com',\n    description='微信机器人 / 可能是最优雅的微信个人号 API',\n    long_description=readme,\n    keywords=[\n        '微信',\n        'WeChat',\n        'API'\n    ],\n    classifiers=[\n        'Development Status :: 4 - Beta',\n        'License :: OSI Approved :: MIT License',\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 2.7',\n        'Programming Language :: Python :: 3.4',\n        'Programming Language :: Python :: 3.5',\n        'Programming Language :: Python :: 3.6',\n        'Operating System :: OS Independent',\n        'Topic :: Communications :: Chat',\n        'Topic :: Utilities',\n    ]\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/chats/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/chats/test_chat.py",
    "content": "class TestChat:\n    def test_pin_unpin(self, friend):\n        friend.pin()\n        friend.unpin()\n"
  },
  {
    "path": "tests/api/chats/test_chats.py",
    "content": "from collections import Counter\n\nfrom wxpy import *\n\n\nclass TestChats:\n    def test_search(self, group, friend):\n        found = group.search('wxpy 机器人')\n        assert friend in found\n        assert isinstance(found, Chats)\n\n    def test_stats(self, group):\n        stats = group.members.stats()\n        assert isinstance(stats, dict)\n        for attr in 'province', 'city', 'sex':\n            assert attr in stats\n            assert isinstance(stats[attr], Counter)\n\n    def test_stats_text(self, group):\n        text = group.members.stats_text()\n        assert '位群成员' in text\n"
  },
  {
    "path": "tests/api/chats/test_friend.py",
    "content": "class TestFriend:\n    pass"
  },
  {
    "path": "tests/api/chats/test_group.py",
    "content": "import pytest\n\nfrom wxpy import *\n\n\nclass TestGroup:\n    def test_group_attributes(self, group, friend, member):\n        isinstance(group.members, Chats)\n        assert friend in group\n        assert member in group\n        assert group.self == group.bot.self\n        assert group.self in group\n        assert not group.is_owner\n        assert group.owner == friend\n\n    def test_update_group(self, group):\n        group.update_group(members_details=True)\n        assert group.members[-1].sex is not None\n\n    def test_add_members(self, group, member):\n        try:\n            group.add_members(member)\n        except ResponseError as e:\n            if e.err_code != 1205:\n                raise e\n\n    def test_remove_members(self, member):\n        with pytest.raises(ResponseError) as e:\n            member.remove()\n            assert e.err_code == -66\n"
  },
  {
    "path": "tests/api/chats/test_groups.py",
    "content": "class TestGroups:\n    def test_search(self, bot, group, member, friend):\n        found = bot.groups().search(group.name, users=[bot.self, member, friend])\n        assert group in found\n"
  },
  {
    "path": "tests/api/chats/test_member.py",
    "content": ""
  },
  {
    "path": "tests/api/chats/test_mp.py",
    "content": "class TestMP:\n    pass\n"
  },
  {
    "path": "tests/api/chats/test_user.py",
    "content": "import re\n\nfrom wxpy import *\n\n\nclass TestUser:\n    def test_friend_attributes(self, friend):\n        assert isinstance(friend, Friend)\n        assert friend.nick_name == 'wxpy 机器人'\n        assert friend.wxid in ('wxpy_bot', None)\n        assert friend.province == '广东'\n        assert friend.city == '深圳'\n        assert friend.sex == MALE\n        assert friend.signature == '如果没有正确响应，可能正在调试中…'\n        assert re.match(r'@[\\da-f]{32,}', friend.user_name)\n        assert friend.is_friend\n\n    # def test_add(self, member):\n    #     member.add('wxpy tests: test_add')\n\n    def test_accept(self, member):\n        # 似乎只要曾经是好友，就可以调用这个方法，达到\"找回已删除的好友\"的效果\n        member.accept()\n\n    def test_remark_name(self, friend, member):\n        new_remark_name = '__test__123__'\n\n        for user in friend, member:\n            current_remark_name = user.remark_name or ''\n            for remark_name in new_remark_name, current_remark_name:\n                user.set_remark_name(remark_name)\n"
  },
  {
    "path": "tests/api/messages/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/messages/test_message.py",
    "content": "from datetime import datetime\n\nfrom tests.conftest import wait_for_message\nfrom wxpy import *\n\n\ndef sent_message(sent_msg, msg_type, receiver):\n    assert isinstance(sent_msg, SentMessage)\n    assert sent_msg.type == msg_type\n    assert sent_msg.receiver == receiver\n    assert sent_msg.bot == receiver.bot\n    assert sent_msg.sender == receiver.bot.self\n    assert isinstance(sent_msg.receive_time, datetime)\n    assert isinstance(sent_msg.create_time, datetime)\n    assert sent_msg.create_time < sent_msg.receive_time\n\n\nclass TestMessage:\n    def test_text_message(self, group, friend):\n        sent_message(group.send('text'), TEXT, group)\n        msg = wait_for_message(group, TEXT)\n        assert isinstance(msg, Message)\n        assert msg.type == TEXT\n        assert msg.text == 'Hello!'\n        assert not msg.is_at\n        assert msg.chat == group\n        assert msg.sender == group\n        assert msg.receiver == group.self\n        assert msg.member == friend\n        assert 0 < msg.latency < 30\n\n        group.send('at')\n        msg = wait_for_message(group, TEXT)\n        assert msg.is_at\n\n    def test_picture_message(self, group, image_path):\n        sent = group.send_image(image_path)\n        sent_message(sent, PICTURE, group)\n        assert sent.path == image_path\n\n    def test_video_message(self, group, video_path):\n        sent = group.send_video(video_path)\n        sent_message(sent, VIDEO, group)\n        assert sent.path == video_path\n\n    def test_raw_message(self, group):\n        # 发送名片\n        raw_type = 42\n        raw_content = '<msg username=\"{}\" nickname=\"{}\"/>'.format('wxpy_bot', 'wxpy 机器人')\n        sent_message(group.send_raw_msg(raw_type, raw_content), None, group)\n\n    def test_send(self, friend, file_path, image_path, video_path):\n        text_to_send = 'test sending text'\n        sent = friend.send(text_to_send)\n        sent_message(sent, TEXT, friend)\n        assert sent.text == text_to_send\n\n        sent = friend.send('@fil@{}'.format(file_path))\n        sent_message(sent, ATTACHMENT, friend)\n        assert sent.path == file_path\n\n        sent = friend.send('@img@{}'.format(image_path))\n        sent_message(sent, PICTURE, friend)\n        assert sent.path == image_path\n\n        sent = friend.send('@vid@{}'.format(video_path))\n        sent_message(sent, VIDEO, friend)\n        assert sent.path == video_path\n\n        # 发送名片\n        raw_type = 42\n        raw_content = '<msg username=\"{}\" nickname=\"{}\"/>'.format('wxpy_bot', 'wxpy 机器人')\n        uri = '/webwxsendmsg'\n        sent = friend.send_raw_msg(raw_type, raw_content)\n        sent_message(sent, None, friend)\n\n        assert sent.type is None\n        assert sent.raw_type == raw_type\n        assert sent.raw_content == raw_content\n        assert sent.uri == uri\n"
  },
  {
    "path": "tests/api/test_bot.py",
    "content": "import logging\nimport os\nimport time\n\nfrom wxpy import *\n\n\nclass TestBot:\n    def test_self(self, bot):\n        assert bot.self.name is not None\n        assert bot.self.name == bot.core.loginInfo['User']['NickName']\n\n    def test_repr(self, bot):\n        assert repr(bot) == '<Bot: {}>'.format(bot.self.name)\n\n    def test_alive(self, bot):\n        assert bot.alive\n\n    def test_dump_login_status(self, bot):\n        bot.dump_login_status()\n        updated_at = os.path.getmtime(bot.cache_path)\n        assert time.time() - updated_at < 1\n\n    def test_enable_puid(self, bot, base_dir):\n        from wxpy.utils.puid_map import PuidMap\n        puid_path = os.path.join(base_dir, 'wxpy_bot_puid.pkl')\n        puid_map = bot.enable_puid(puid_path)\n        assert isinstance(puid_map, PuidMap)\n\n    def test_chats(self, bot):\n        chats = bot.chats()\n        assert isinstance(chats, Chats)\n        assert set(chats) == set(bot.friends() + bot.groups() + bot.mps())\n\n    def test_friends(self, bot):\n        friends = bot.friends()\n        assert isinstance(friends, Chats)\n        assert bot.self in friends\n        for friend in friends:\n            assert isinstance(friend, Friend)\n\n    def test_groups(self, bot):\n        groups = bot.groups()\n        assert isinstance(groups, Groups)\n        for group in groups:\n            assert isinstance(group, Group)\n            assert bot.self in group\n\n    def test_mps(self, bot):\n        mps = bot.mps()\n        assert isinstance(mps, Chats)\n        for mp in mps:\n            assert isinstance(mp, MP)\n\n    def test_search(self, bot):\n        found_1 = bot.search(bot.self.name, sex=bot.self.sex or None)\n        assert bot.self in found_1\n        found_2 = bot.search(nick_name='__!#@$#%$__')\n        assert not found_2\n\n        for found in found_1, found_2:\n            assert isinstance(found, Chats)\n            assert found.source == bot\n\n    def test_create_group(self, bot):\n        users = bot.friends()[:3]\n        topic = 'test creating group'\n        try:\n            new_group = bot.create_group(users, topic)\n        except ResponseError as e:\n            logging.warning('Failed to create group: {}'.format(e))\n        except Exception as e:\n            if 'Failed to create group:' in str(e):\n                logging.warning(e)\n            else:\n                raise e\n        else:\n            assert new_group.name == topic\n            assert new_group in bot.groups()\n            assert set(users) == set(new_group.members)\n\n            new_name = 'testing'\n            new_group.rename_group(new_name)\n            assert new_group.name == new_name\n\n    def test_upload_file(self, bot, file_path, friend):\n        media_id = bot.upload_file(file_path)\n        friend.send_file(file_path, media_id=media_id)\n"
  },
  {
    "path": "tests/attachments/file.txt",
    "content": "Hello from wxpy!\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "# coding: utf-8\n\n\"\"\"\n部分用例需要与 \"wxpy 机器人\" 进行互动\n\"\"\"\n\nimport os\nimport time\nfrom functools import partial\nfrom queue import Queue\n\nimport pytest\n\nfrom wxpy import *\n\n_base_dir = os.path.dirname(os.path.realpath(__file__))\n\nprint('logging in test bot...')\n_bot = Bot(os.path.join(_base_dir, 'wxpy_bot.pkl'))\n_friend = ensure_one(_bot.friends().search('wxpy 机器人'))\n_group = ensure_one(_bot.groups().search('wxpy test'))\n_member = ensure_one(_group.search('游否'))\n\n_shared_dict = dict()\n\nattachments_dir = os.path.join(_base_dir, 'attachments')\ngen_attachment_path = partial(os.path.join, attachments_dir)\n\nglobal_use = partial(pytest.fixture, scope='session', autouse=True)\n\n\n@global_use()\ndef base_dir():\n    return _base_dir\n\n\n@global_use()\ndef bot():\n    return _bot\n\n\n@global_use()\ndef friend():\n    yield _friend\n    while True:\n        try:\n            _friend.set_remark_name('')\n        except ResponseError as e:\n            if e.err_code == 1205:\n                time.sleep(10)\n                continue\n        else:\n            break\n\n\n@global_use()\ndef group():\n    return _group\n\n\n@global_use()\ndef shared_dict():\n    return _shared_dict\n\n\n@global_use()\ndef member():\n    return _member\n\n\n@global_use()\ndef image_path():\n    return gen_attachment_path('image.png')\n\n\n@global_use()\ndef file_path():\n    return gen_attachment_path('file.txt')\n\n\n@global_use()\ndef video_path():\n    return gen_attachment_path('video.mp4')\n\n\ndef wait_for_message(chats=None, msg_types=None, except_self=True, timeout=30):\n    \"\"\"\n    等待一条指定的消息，并返回这条消息\n\n    :param chats: 所需等待消息所在的聊天会话\n    :param msg_types: 所需等待的消息类型\n    :param except_self: 是否排除自己发送的消息\n    :param timeout: 等待的超时秒数，若为 None 则一直等待，直到收到所需的消息\n    :return: 若在超时内等到了消息，则返回此消息，否则抛出 `queue.Empty` 异常\n    \"\"\"\n\n    received = Queue()\n\n    @_bot.register(chats=chats, msg_types=msg_types, except_self=except_self)\n    def _func(msg):\n        received.put(msg)\n\n    _config = _bot.registered.get_config_by_func(_func)\n\n    ret = received.get(timeout=timeout)\n\n    _bot.registered.remove(_config)\n\n    return ret\n"
  },
  {
    "path": "tests/ext/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "wxpy/__compat__.py",
    "content": ""
  },
  {
    "path": "wxpy/__init__.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\n\n\"\"\"\n\n\n登陆微信::\n\n    # 导入模块\n    from wxpy import *\n    # 初始化机器人，扫码登陆\n    bot = Bot()\n\n找到好友::\n\n    # 搜索名称含有 \"游否\" 的男性深圳好友\n    my_friend = bot.friends().search('游否', sex=MALE, city=\"深圳\")[0]\n\n发送消息::\n\n    # 发送文本给好友\n    my_friend.send('Hello WeChat!')\n    # 发送图片\n    my_friend.send_image('my_picture.jpg')\n\n自动响应各类消息::\n\n    # 打印来自其他好友、群聊和公众号的消息\n    @bot.register()\n    def print_others(msg):\n        print(msg)\n\n    # 回复 my_friend 的消息 (优先匹配后注册的函数!)\n    @bot.register(my_friend)\n    def reply_my_friend(msg):\n        return 'received: {} ({})'.format(msg.text, msg.type)\n\n    # 自动接受新的好友请求\n    @bot.register(msg_types=FRIENDS)\n    def auto_accept_friends(msg):\n        # 接受好友请求\n        new_friend = msg.card.accept()\n        # 向新的好友发送消息\n        new_friend.send('哈哈，我自动接受了你的好友请求')\n\n保持登陆/运行::\n\n    # 进入 Python 命令行、让程序保持运行\n    embed()\n\n    # 或者仅仅堵塞线程\n    # bot.join()\n\n\n\"\"\"\n\nimport logging\nimport sys\n\nfrom .api.bot import Bot\nfrom .api.chats import Chat, Chats, Friend, Group, Groups, MP, Member, User\nfrom .api.consts import ATTACHMENT, CARD, FRIENDS, MAP, NOTE, PICTURE, RECORDING, SHARING, SYSTEM, TEXT, VIDEO\nfrom .api.consts import FEMALE, MALE\nfrom .api.messages import Article, Message, Messages, SentMessage\nfrom .exceptions import ResponseError\nfrom .ext import Tuling, WeChatLoggingHandler, XiaoI, get_wechat_logger, sync_message_in_groups\nfrom .utils import BaseRequest, detect_freq_limit, dont_raise_response_error, embed, ensure_one, mutual_friends\n\n__title__ = 'wxpy'\n__version__ = '0.3.9.8'\n__author__ = 'Youfou'\n__license__ = 'MIT'\n__copyright__ = '2017, Youfou'\n\nversion_details = 'wxpy {ver} from {path} (python {pv.major}.{pv.minor}.{pv.micro})'.format(\n    ver=__version__, path=__path__[0], pv=sys.version_info)\n\ntry:\n    # Python 2.7+\n    from logging import NullHandler\nexcept ImportError:\n    class NullHandler(logging.Handler):\n        def emit(self, record):\n            pass\n\nlogging.getLogger(__name__).addHandler(NullHandler())\n"
  },
  {
    "path": "wxpy/__main__.py",
    "content": "# coding: utf-8\n\nfrom .utils import shell_entry\n\nif __name__ == '__main__':\n    shell_entry()\n"
  },
  {
    "path": "wxpy/api/__init__.py",
    "content": ""
  },
  {
    "path": "wxpy/api/bot.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport atexit\nimport functools\nimport logging\nimport os.path\nimport tempfile\nimport time\nfrom pprint import pformat\nfrom threading import Thread\n\ntry:\n    import queue\nexcept ImportError:\n    # noinspection PyUnresolvedReferences,PyPep8Naming\n    import Queue as queue\n\nimport itchat\n\nfrom ..api.chats import Chat, Chats, Friend, Group, MP, User\nfrom ..api.consts import SYSTEM\nfrom ..api.messages import Message, MessageConfig, Messages, Registered\nfrom ..compatible import PY2\nfrom ..compatible.utils import force_encoded_string_output\nfrom ..utils import PuidMap\nfrom ..utils import enhance_connection, enhance_webwx_request, ensure_list, get_user_name, handle_response, \\\n    start_new_thread, wrap_user_name\n\nlogger = logging.getLogger(__name__)\n\n\nclass Bot(object):\n    \"\"\"\n    机器人对象，用于登陆和操作微信账号，涵盖大部分 Web 微信的功能::\n    \n        from wxpy import *\n        bot = Bot()\n        \n        # 机器人账号自身\n        myself = bot.self\n        \n        # 向文件传输助手发送消息\n        bot.file_helper.send('Hello from wxpy!')\n        \n\n    \"\"\"\n\n    def __init__(\n            self, cache_path=None, console_qr=False, qr_path=None,\n            qr_callback=None, login_callback=None, logout_callback=None\n    ):\n        \"\"\"\n        :param cache_path:\n            * 设置当前会话的缓存路径，并开启缓存功能；为 `None` (默认) 则不开启缓存功能。\n            * 开启缓存后可在短时间内避免重复扫码，缓存失效时会重新要求登陆。\n            * 设为 `True` 时，使用默认的缓存路径 'wxpy.pkl'。\n        :param console_qr:\n            * 在终端中显示登陆二维码，需要安装 pillow 模块 (`pip3 install pillow`)。\n            * 可为整数(int)，表示二维码单元格的宽度，通常为 2 (当被设为 `True` 时，也将在内部当作 2)。\n            * 也可为负数，表示以反色显示二维码，适用于浅底深字的命令行界面。\n            * 例如: 在大部分 Linux 系统中可设为 `True` 或 2，而在 macOS Terminal 的默认白底配色中，应设为 -2。\n        :param qr_path: 保存二维码的路径\n        :param qr_callback: 获得二维码后的回调，可以用来定义二维码的处理方式，接收参数: uuid, status, qrcode\n        :param login_callback: 登陆成功后的回调，若不指定，将进行清屏操作，并删除二维码文件\n        :param logout_callback: 登出时的回调\n        \"\"\"\n\n        self.core = itchat.Core()\n        itchat.instanceList.append(self)\n\n        enhance_connection(self.core.s)\n\n        if cache_path is True:\n            cache_path = 'wxpy.pkl'\n\n        self.cache_path = cache_path\n\n        if console_qr is True:\n            console_qr = 2\n\n        try:\n            self.core.auto_login(\n                hotReload=bool(cache_path), statusStorageDir=cache_path,\n                enableCmdQR=console_qr, picDir=qr_path, qrCallback=qr_callback,\n                loginCallback=login_callback, exitCallback=logout_callback\n            )\n        except FileNotFoundError as e:\n            if 'xdg-open' in e.strerror:\n                raise Exception('use `console_qr` arg while under pure console environment')\n            raise\n\n        # 用于 \"synccheck\" 请求的 \"_\" 参数，每次请求时 + 1\n        self._sync_check_iterations = int(time.time() * 1000)\n\n        enhance_webwx_request(self)\n\n        self.self = User(self.core.loginInfo['User'], self)\n        self.file_helper = Chat(wrap_user_name('filehelper'), self)\n\n        self.messages = Messages()\n        self.registered = Registered(self)\n\n        self.puid_map = None\n        self.auto_mark_as_read = False\n\n        self.is_listening = False\n        self.listening_thread = None\n\n        if PY2:\n            from wxpy.compatible.utils import TemporaryDirectory\n            self.temp_dir = TemporaryDirectory(prefix='wxpy_')\n        else:\n            self.temp_dir = tempfile.TemporaryDirectory(prefix='wxpy_')\n\n        self.start()\n\n        atexit.register(self._cleanup)\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return '<{}: {}>'.format(self.__class__.__name__, self.self.name)\n\n    def __unicode__(self):\n        return '<{}: {}>'.format(self.__class__.__name__, self.self.name)\n\n    @handle_response()\n    def logout(self):\n        \"\"\"\n        登出当前账号\n        \"\"\"\n\n        logger.info('{}: logging out'.format(self))\n\n        return self.core.logout()\n\n    @property\n    def alive(self):\n        \"\"\"\n        若为登陆状态，则为 True，否则为 False\n        \"\"\"\n\n        return self.core.alive\n\n    @alive.setter\n    def alive(self, value):\n        self.core.alive = value\n\n    def dump_login_status(self, cache_path=None):\n        logger.debug('{}: dumping login status'.format(self))\n        return self.core.dump_login_status(cache_path or self.cache_path)\n\n    # chats\n\n    def enable_puid(self, path='wxpy_puid.pkl'):\n        \"\"\"\n        **可选操作:** 启用聊天对象的 :any:`puid <Chat.puid>` 属性::\n            \n            # 启用 puid 属性，并指定 puid 所需的映射数据保存/载入路径\n            bot.enable_puid('wxpy_puid.pkl')\n            \n            # 指定一个好友\n            my_friend = bot.friends().search('游否')[0]\n            \n            # 查看他的 puid\n            print(my_friend.puid)\n            # 'edfe8468'\n\n        ..  tip::\n        \n            | :any:`puid <Chat.puid>` 是 **wxpy 特有的聊天对象/用户ID**\n            | 不同于其他 ID 属性，**puid** 可始终被获取到，且具有稳定的唯一性\n\n        :param path: puid 所需的映射数据保存/载入路径\n        \"\"\"\n\n        self.puid_map = PuidMap(path)\n        return self.puid_map\n\n    def except_self(self, chats_or_dicts):\n        \"\"\"\n        从聊天对象合集或用户字典列表中排除自身\n\n        :param chats_or_dicts: 聊天对象合集或用户字典列表\n        :return: 排除自身后的列表\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n        return list(filter(lambda x: get_user_name(x) != self.self.user_name, chats_or_dicts))\n\n    # Todo: itchat 中更新本地聊天对象时，缺少线程锁，导致 AttributeError: 'NoneType' object has no attribute 'get'\n\n    def chats(self, update=False):\n        \"\"\"\n        获取所有聊天对象\n\n        :param update: 是否更新\n        :return: 聊天对象合集\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n        return Chats(self.friends(update) + self.groups(update) + self.mps(update), self)\n\n    def _retrieve_itchat_storage(self, attr):\n        with self.core.storageClass.updateLock:\n            return getattr(self.core.storageClass, attr)\n\n    @handle_response(Friend)\n    def friends(self, update=False):\n        \"\"\"\n        获取所有好友\n\n        :param update: 是否更新\n        :return: 聊天对象合集\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n\n        if update:\n            logger.info('{}: updating friends'.format(self))\n            return self.core.get_friends(update=update)\n        else:\n            return self._retrieve_itchat_storage('memberList')\n\n    @handle_response(Group)\n    def groups(self, update=False, contact_only=False):\n        \"\"\"\n        获取所有群聊对象\n\n        一些不活跃的群可能无法被获取到，可通过在群内发言，或修改群名称的方式来激活\n\n        :param update: 是否更新\n        :param contact_only: 是否限于保存为联系人的群聊\n        :return: 群聊合集\n        :rtype: :class:`wxpy.Groups`\n        \"\"\"\n\n        # itchat 原代码有些难懂，似乎 itchat 中的 get_contact() 所获取的内容视其 update 参数而变化\n        # 如果 update=False 获取所有类型的本地聊天对象\n        # 反之如果 update=True，变为获取收藏的聊天室\n\n        if update or contact_only:\n            logger.info('{}: updating groups'.format(self))\n            return self.core.get_chatrooms(update=update, contactOnly=contact_only)\n        else:\n            return self._retrieve_itchat_storage('chatroomList')\n\n    @handle_response(MP)\n    def mps(self, update=False):\n        \"\"\"\n        获取所有公众号\n\n        :param update: 是否更新\n        :return: 聊天对象合集\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n\n        if update:\n            logger.info('{}: updating mps'.format(self))\n            return self.core.get_mps(update=update)\n        else:\n            return self._retrieve_itchat_storage('mpList')\n\n    @handle_response(User)\n    def user_details(self, user_or_users, chunk_size=50):\n        \"\"\"\n        获取单个或批量获取多个用户的详细信息(地区、性别、签名等)，但不可用于群聊成员\n\n        :param user_or_users: 单个或多个用户对象或 user_name\n        :param chunk_size: 分配请求时的单批数量，目前为 50\n        :return: 单个或多个用户用户的详细信息\n        \"\"\"\n\n        def chunks():\n            total = ensure_list(user_or_users)\n            for i in range(0, len(total), chunk_size):\n                yield total[i:i + chunk_size]\n\n        @handle_response()\n        def process_one_chunk(_chunk):\n            return self.core.update_friend(userName=get_user_name(_chunk))\n\n        if isinstance(user_or_users, (list, tuple)):\n            ret = list()\n            for chunk in chunks():\n                chunk_ret = process_one_chunk(chunk)\n                if isinstance(chunk_ret, list):\n                    ret += chunk_ret\n                else:\n                    ret.append(chunk_ret)\n            return ret\n        else:\n            return process_one_chunk(user_or_users)\n\n    def search(self, keywords=None, **attributes):\n        \"\"\"\n        在所有类型的聊天对象中进行搜索\n        \n        ..  note:: \n        \n            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象\n            | 建议搭配 :any:`ensure_one()` 使用\n\n        :param keywords: 聊天对象的名称关键词\n        :param attributes: 属性键值对，键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东'\n        :return: 匹配的聊天对象合集\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n\n        return self.chats().search(keywords, **attributes)\n\n    # add / create\n\n    @handle_response()\n    def add_friend(self, user, verify_content=''):\n        \"\"\"\n        添加用户为好友\n\n        :param user: 用户对象，或 user_name\n        :param verify_content: 验证说明信息\n        \"\"\"\n\n        logger.info('{}: adding {} (verify_content: {})'.format(self, user, verify_content))\n        user_name = get_user_name(user)\n\n        return self.core.add_friend(\n            userName=user_name,\n            status=2,\n            verifyContent=verify_content,\n            autoUpdate=True\n        )\n\n    @handle_response()\n    def add_mp(self, user):\n\n        \"\"\"\n        添加/关注 公众号\n        \n        :param user: 公众号对象，或 user_name\n        \"\"\"\n\n        logger.info('{}: adding {}'.format(self, user))\n        user_name = get_user_name(user)\n\n        return self.core.add_friend(\n            userName=user_name,\n            status=1,\n            autoUpdate=True\n        )\n\n    def accept_friend(self, user, verify_content=''):\n        \"\"\"\n        接受用户为好友\n\n        :param user: 用户对象或 user_name\n        :param verify_content: 验证说明信息\n        :return: 新的好友对象\n        :rtype: :class:`wxpy.Friend`\n        \"\"\"\n\n        logger.info('{}: accepting {} (verify_content: {})'.format(self, user, verify_content))\n\n        @handle_response()\n        def do():\n            return self.core.add_friend(\n                userName=get_user_name(user),\n                status=3,\n                verifyContent=verify_content,\n                autoUpdate=True\n            )\n\n        do()\n        # 若上一步没有抛出异常，则返回该好友\n        for friend in self.friends():\n            if friend == user:\n                return friend\n\n    def create_group(self, users, topic=None):\n        \"\"\"\n        创建一个新的群聊\n\n        :param users: 用户列表 (不含自己，至少 2 位)\n        :param topic: 群名称\n        :return: 若建群成功，返回一个新的群聊对象\n        :rtype: :class:`wxpy.Group`\n        \"\"\"\n\n        logger.info('{}: creating group (topic: {}), with users:\\n{}'.format(\n            self, topic, pformat(users)))\n\n        @handle_response()\n        def request():\n            return self.core.create_chatroom(\n                memberList=dict_list,\n                topic=topic or ''\n            )\n\n        dict_list = wrap_user_name(self.except_self(ensure_list(users)))\n        ret = request()\n        user_name = ret.get('ChatRoomName')\n        if user_name:\n            return Group(self.core.update_chatroom(userName=user_name), self)\n        else:\n            from wxpy.utils import decode_text_from_webwx\n            ret = decode_text_from_webwx(pformat(ret))\n            raise Exception('Failed to create group:\\n{}'.format(ret))\n\n    # upload\n\n    def upload_file(self, path):\n        \"\"\"\n        | 上传文件，并获取 media_id\n        | 可用于重复发送图片、表情、视频，和文件\n\n        :param path: 文件路径\n        :return: media_id\n        :rtype: str\n        \"\"\"\n\n        logger.info('{}: uploading file: {}'.format(self, path))\n\n        @handle_response()\n        def do():\n            upload = functools.partial(self.core.upload_file, fileDir=path)\n            ext = os.path.splitext(path)[1].lower()\n\n            if ext in ('.bmp', '.png', '.jpeg', '.jpg', '.gif'):\n                return upload(isPicture=True)\n            elif ext == '.mp4':\n                return upload(isVideo=True)\n            else:\n                return upload()\n\n        return do().get('MediaId')\n\n    # messages / register\n\n    def _process_message(self, msg):\n        \"\"\"\n        处理接收到的消息\n        \"\"\"\n\n        if not self.alive:\n            return\n\n        config = self.registered.get_config(msg)\n\n        logger.debug('{}: new message (func: {}):\\n{}'.format(\n            self, config.func.__name__ if config else None, msg))\n\n        if config:\n\n            def process():\n                # noinspection PyBroadException\n                try:\n                    ret = config.func(msg)\n                    if ret is not None:\n                        msg.reply(ret)\n                except:\n                    logger.exception('an error occurred in {}.'.format(config.func))\n\n                if self.auto_mark_as_read and not msg.type == SYSTEM and msg.sender != self.self:\n                    from wxpy import ResponseError\n                    try:\n                        msg.chat.mark_as_read()\n                    except ResponseError as e:\n                        logger.warning('failed to mark as read: {}'.format(e))\n\n            if config.run_async:\n                start_new_thread(process, use_caller_name=True)\n            else:\n                process()\n\n    def register(\n            self, chats=None, msg_types=None,\n            except_self=True, run_async=True, enabled=True\n    ):\n        \"\"\"\n        装饰器：用于注册消息配置\n\n        :param chats: 消息所在的聊天对象：单个或列表形式的多个聊天对象或聊天类型，为空时匹配所有聊天对象\n        :param msg_types: 消息的类型：单个或列表形式的多个消息类型，为空时匹配所有消息类型 (SYSTEM 类消息除外)\n        :param except_self: 排除由自己发送的消息\n        :param run_async: 是否异步执行所配置的函数：可提高响应速度\n        :param enabled: 当前配置的默认开启状态，可事后动态开启或关闭\n        \"\"\"\n\n        def do_register(func):\n            self.registered.append(MessageConfig(\n                bot=self, func=func, chats=chats, msg_types=msg_types,\n                except_self=except_self, run_async=run_async, enabled=enabled\n            ))\n\n            return func\n\n        return do_register\n\n    def _listen(self):\n        # Todo: 在短时间内收到多条消息时，会偶尔漏收消息(Web 微信没有问题)\n        try:\n            logger.info('{}: started'.format(self))\n            self.is_listening = True\n\n            while self.alive and self.is_listening:\n\n                try:\n                    msg = Message(self.core.msgList.get(timeout=0.5), self)\n                except queue.Empty:\n                    continue\n\n                if msg.type != SYSTEM:\n                    self.messages.append(msg)\n\n                # noinspection PyBroadException\n                try:\n                    self._process_message(msg)\n                except:\n                    logger.exception('an error occurred while processing msg:\\n{}'.format(msg))\n        finally:\n            self.is_listening = False\n            logger.info('{}: stopped'.format(self))\n\n    def start(self):\n        \"\"\"\n        开始消息监听和处理 (登陆后会自动开始)\n        \"\"\"\n\n        if not self.alive:\n            logger.warning('{} has been logged out!'.format(self))\n        elif self.is_listening:\n            logger.warning('{} is already running, no need to start again.'.format(self))\n        else:\n            self.listening_thread = start_new_thread(self._listen)\n\n    def stop(self):\n        \"\"\"\n        停止消息监听和处理 (登出后会自动停止)\n        \"\"\"\n\n        if self.is_listening:\n            self.is_listening = False\n            self.listening_thread.join()\n        else:\n            logger.warning('{} is not running.'.format(self))\n\n    def join(self):\n        \"\"\"\n        堵塞进程，直到结束消息监听 (例如，机器人被登出时)\n        \"\"\"\n\n        if isinstance(self.listening_thread, Thread):\n            try:\n                logger.info('{}: joined'.format(self))\n                self.listening_thread.join()\n            except KeyboardInterrupt:\n                pass\n\n    def _cleanup(self):\n        if self.is_listening:\n            self.stop()\n        if self.alive and self.core.useHotReload:\n            self.dump_login_status()\n        self.temp_dir.cleanup()\n"
  },
  {
    "path": "wxpy/api/chats/__init__.py",
    "content": "from .chat import Chat\nfrom .chats import Chats\nfrom .friend import Friend\nfrom .group import Group\nfrom .groups import Groups\nfrom .member import Member\nfrom .mp import MP\nfrom .user import User\n"
  },
  {
    "path": "wxpy/api/chats/chat.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport datetime\nimport logging\nimport re\nimport time\nfrom functools import partial, wraps\n\nfrom wxpy.api.consts import ATTACHMENT, PICTURE, TEXT, VIDEO\nfrom wxpy.compatible import *\nfrom wxpy.compatible.utils import force_encoded_string_output\nfrom wxpy.utils import handle_response\n\nlogger = logging.getLogger(__name__)\n\n\ndef wrapped_send(msg_type):\n    \"\"\"\n    send() 系列方法较为雷同，因此采用装饰器方式完成发送，并返回 SentMessage 对象\n    \"\"\"\n\n    def decorator(func):\n        @wraps(func)\n        def wrapped(self, *args, **kwargs):\n\n            # 用于初始化 SentMessage 的属性\n            sent_attrs = dict(\n                type=msg_type, receiver=self,\n                create_time=datetime.datetime.now()\n            )\n\n            # 被装饰函数需要返回两个部分:\n            # itchat_call_or_ret: 请求 itchat 原函数的参数字典 (或返回值字典)\n            # sent_attrs_from_method: 方法中需要添加到 SentMessage 的属性字典\n            itchat_call_or_ret, sent_attrs_from_method = func(self, *args, **kwargs)\n\n            if msg_type:\n                # 找到原 itchat 中的同名函数，并转化为指定了 `toUserName` 的偏函数\n                itchat_partial_func = partial(\n                    getattr(self.bot.core, func.__name__),\n                    toUserName=self.user_name\n                )\n\n                logger.info('sending {} to {}:\\n{}'.format(\n                    func.__name__[5:], self,\n                    sent_attrs_from_method.get('text') or sent_attrs_from_method.get('path')\n                ))\n\n                @handle_response()\n                def do_send():\n                    return itchat_partial_func(**itchat_call_or_ret)\n\n                ret = do_send()\n            else:\n                # send_raw_msg 会直接返回结果\n                ret = itchat_call_or_ret\n\n            sent_attrs['receive_time'] = datetime.datetime.now()\n\n            try:\n                sent_attrs['id'] = int(ret.get('MsgID'))\n            except (ValueError, TypeError):\n                pass\n\n            sent_attrs['local_id'] = ret.get('LocalID')\n\n            # 加入被装饰函数返回值中的属性字典\n            sent_attrs.update(sent_attrs_from_method)\n\n            from wxpy import SentMessage\n            sent = SentMessage(attributes=sent_attrs)\n            self.bot.messages.append(sent)\n\n            return sent\n\n        return wrapped\n\n    return decorator\n\n\nclass Chat(object):\n    \"\"\"\n    单个用户 (:class:`User`) 和群聊 (:class:`Group`) 的基础类\n    \"\"\"\n\n    def __init__(self, raw, bot):\n\n        self.raw = raw\n        self.bot = bot\n\n    @property\n    def puid(self):\n        \"\"\"\n        持续有效，且稳定唯一的聊天对象/用户ID，适用于持久保存\n        \n        请使用 :any:`Bot.enable_puid()` 来启用 puid 属性\n        \n        ..  tip::\n        \n            | :any:`puid <Chat.puid>` 是 **wxpy 特有的聊天对象/用户ID**\n            | 不同于其他 ID 属性，**puid** 可始终被获取到，且具有稳定的唯一性\n\n        ..  attention::\n        \n            puid 映射数据 **不可跨机器人使用**\n\n        \"\"\"\n\n        if self.bot.puid_map:\n            return self.bot.puid_map.get_puid(self)\n        else:\n            raise TypeError('puid is not enabled, you can enable it by `bot.enable_puid()`')\n\n    @property\n    def nick_name(self):\n        \"\"\"\n        该聊天对象的昵称 (好友、群员的昵称，或群名称)\n        \"\"\"\n        if self.user_name == 'filehelper':\n            return '文件传输助手'\n        elif self.user_name == 'fmessage':\n            return '好友请求'\n        else:\n            return self.raw.get('NickName')\n\n    @property\n    def name(self):\n        \"\"\"\n        | 该聊天对象的友好名称\n        | 具体为: 从 备注名称、群聊显示名称、昵称(或群名称)，或微信号中\n        | 按序选取第一个可用的\n        \"\"\"\n        for attr in 'remark_name', 'display_name', 'nick_name', 'wxid':\n            _name = getattr(self, attr, None)\n            if _name:\n                return _name\n\n    def send(self, content=None, media_id=None):\n        \"\"\"\n        动态发送不同类型的消息，具体类型取决于 `msg` 的前缀。\n\n        :param content:\n            * 由 **前缀** 和 **内容** 两个部分组成，若 **省略前缀**，将作为纯文本消息发送\n            * **前缀** 部分可为: '@fil@', '@img@', '@msg@', '@vid@' (不含引号)\n            * 分别表示: 文件，图片，纯文本，视频\n            * **内容** 部分可为: 文件、图片、视频的路径，或纯文本的内容\n        :param media_id: 填写后可省略上传过程\n        :rtype: :class:`wxpy.SentMessage`\n        \"\"\"\n\n        method_map = dict(fil=self.send_file, img=self.send_image, vid=self.send_video)\n        content = str('' if content is None else content)\n\n        try:\n            method, content = re.match(r'@(\\w{3})@(.+)', content).groups()\n        except AttributeError:\n            method = None\n\n        if method:\n            return method_map[method](path=content, media_id=media_id)\n        else:\n            return self.send_msg(msg=content)\n\n    @wrapped_send(TEXT)\n    def send_msg(self, msg=None):\n        \"\"\"\n        发送文本消息\n\n        :param msg: 文本内容\n        :rtype: :class:`wxpy.SentMessage`\n        \"\"\"\n\n        if msg is None:\n            msg = 'Hello from wxpy!'\n        else:\n            msg = str(msg)\n\n        return dict(msg=msg), dict(text=msg)\n\n    # Todo: 发送后可获取到 media_id\n\n    @wrapped_send(PICTURE)\n    def send_image(self, path, media_id=None):\n        \"\"\"\n        发送图片\n\n        :param path: 文件路径\n        :param media_id: 设置后可省略上传\n        :rtype: :class:`wxpy.SentMessage`\n        \"\"\"\n\n        return dict(fileDir=path, mediaId=media_id), locals()\n\n    @wrapped_send(ATTACHMENT)\n    def send_file(self, path, media_id=None):\n        \"\"\"\n        发送文件\n\n        :param path: 文件路径\n        :param media_id: 设置后可省略上传\n        :rtype: :class:`wxpy.SentMessage`\n        \"\"\"\n\n        return dict(fileDir=path, mediaId=media_id), locals()\n\n    @wrapped_send(VIDEO)\n    def send_video(self, path=None, media_id=None):\n        \"\"\"\n        发送视频\n\n        :param path: 文件路径\n        :param media_id: 设置后可省略上传\n        :rtype: :class:`wxpy.SentMessage`\n        \"\"\"\n\n        return dict(fileDir=path, mediaId=media_id), locals()\n\n    @wrapped_send(None)\n    def send_raw_msg(self, raw_type, raw_content, uri=None, msg_ext=None):\n        \"\"\"\n        以原始格式发送其他类型的消息。\n\n        :param int raw_type: 原始的整数消息类型\n        :param str raw_content: 原始的消息内容\n        :param str uri: 请求路径，默认为 '/webwxsendmsg'\n        :param dict msg_ext: 消息的扩展属性 (会被更新到 `Msg` 键中)\n        :rtype: :class:`wxpy.SentMessage`\n\n        例如，发送好友或公众号的名片::\n\n            my_friend.send_raw_msg(\n                # 名片的原始消息类型\n                raw_type=42,\n                # 注意 `username` 在这里应为微信 ID，且被发送的名片必须为自己的好友\n                raw_content='<msg username=\"wxpy_bot\" nickname=\"wxpy 机器人\"/>'\n            )\n        \"\"\"\n\n        logger.info('sending raw msg to {}'.format(self))\n\n        uri = uri or '/webwxsendmsg'\n\n        from wxpy.utils import BaseRequest\n        req = BaseRequest(self.bot, uri=uri)\n\n        msg = {\n            'Type': raw_type,\n            'Content': raw_content,\n            'FromUserName': self.bot.self.user_name,\n            'ToUserName': self.user_name,\n            'LocalID': int(time.time() * 1e4),\n            'ClientMsgId': int(time.time() * 1e4),\n        }\n\n        if msg_ext:\n            msg.update(msg_ext)\n\n        req.data.update({'Msg': msg, 'Scene': 0})\n\n        # noinspection PyUnresolvedReferences\n        return req.post(), {\n            'raw_type': raw_type,\n            'raw_content': raw_content,\n            'uri': uri,\n            'msg_ext': msg_ext,\n        }\n\n    @handle_response()\n    def mark_as_read(self):\n        \"\"\"\n        消除当前聊天对象的未读提示小红点\n        \"\"\"\n\n        from wxpy.utils import BaseRequest\n        req = BaseRequest(\n            bot=self.bot,\n            # itchat 中的 pass_ticket 已经预先编码了\n            uri='/webwxstatusnotify?pass_ticket={}'.format(self.bot.core.loginInfo['pass_ticket'])\n        )\n\n        req.data.update({\n            'ClientMsgId': int(time.time() * 1000),\n            'Code': 1,\n            'FromUserName': self.bot.self.user_name,\n            'ToUserName': self.user_name,\n        })\n\n        logger.debug('marking {} as read'.format(self))\n\n        return req.request('POST')\n\n    @handle_response()\n    def pin(self):\n        \"\"\"\n        将聊天对象置顶\n        \"\"\"\n        logger.info('pinning {}'.format(self))\n        return self.bot.core.set_pinned(userName=self.user_name, isPinned=True)\n\n    @handle_response()\n    def unpin(self):\n        \"\"\"\n        取消聊天对象的置顶状态\n        \"\"\"\n        logger.info('unpinning {}'.format(self))\n        return self.bot.core.set_pinned(userName=self.user_name, isPinned=False)\n\n    @handle_response()\n    def get_avatar(self, save_path=None):\n        \"\"\"\n        获取头像\n\n        :param save_path: 保存路径(后缀通常为.jpg)，若为 `None` 则返回字节数据\n        \"\"\"\n\n        logger.info('getting avatar of {}'.format(self))\n\n        from .group import Group\n        from .member import Member\n        from .friend import User\n\n        if isinstance(self, Group):\n            kwargs = dict(userName=None, chatroomUserName=self.user_name)\n        elif isinstance(self, Member):\n            kwargs = dict(userName=self.user_name, chatroomUserName=self.group.user_name)\n        elif isinstance(self, User):\n            kwargs = dict(userName=self.user_name, chatroomUserName=None)\n        else:\n            raise TypeError('expected `Chat`, got`{}`'.format(type(self)))\n\n        kwargs.update(dict(picDir=save_path))\n\n        return self.bot.core.get_head_img(**kwargs)\n\n    @property\n    def uin(self):\n        \"\"\"\n        微信中的聊天对象ID，固定且唯一\n\n        | 因微信的隐私策略，该属性有时无法被获取到\n        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID\n        \"\"\"\n        return self.raw.get('Uin')\n\n    @property\n    def alias(self):\n        \"\"\"\n        若用户进行过一次性的 \"设置微信号\" 操作，则该值为用户设置的\"微信号\"，固定且唯一\n\n        | 因微信的隐私策略，该属性有时无法被获取到\n        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID\n        \"\"\"\n        return self.raw.get('Alias')\n\n    @property\n    def wxid(self):\n        \"\"\"\n        聊天对象的微信ID (实际为 .alias 或 .uin)\n\n        | 因微信的隐私策略，该属性有时无法被获取到\n        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID\n        \"\"\"\n\n        return self.alias or self.uin or None\n\n    @property\n    def user_name(self):\n        \"\"\"\n        该聊天对象的内部 ID，通常不需要用到\n\n        ..  attention::\n\n            同个聊天对象在不同用户中，此 ID **不一致** ，且可能在新会话中 **被改变**！\n        \"\"\"\n        return self.raw.get('UserName')\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return '<{}: {}>'.format(self.__class__.__name__, self.name)\n\n    def __unicode__(self):\n        return '<{}: {}>'.format(self.__class__.__name__, self.name)\n\n    def __eq__(self, other):\n        return hash(self) == hash(other)\n\n    def __cmp__(self, other):\n        if hash(self) == hash(other):\n            return 0\n        return 1\n\n    def __hash__(self):\n        return hash((Chat, self.user_name))\n"
  },
  {
    "path": "wxpy/api/chats/chats.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport time\nfrom collections import Counter\n\nfrom wxpy.utils import match_attributes, match_name\nfrom wxpy.compatible import *\n\nlogger = logging.getLogger(__name__)\n\n\nclass Chats(list):\n    \"\"\"\n    多个聊天对象的合集，可用于搜索或统计\n    \"\"\"\n\n    def __init__(self, chat_list=None, source=None):\n        if chat_list:\n            super(Chats, self).__init__(chat_list)\n        self.source = source\n\n    def __add__(self, other):\n        return Chats(super(Chats, self).__add__(other or list()))\n\n    def search(self, keywords=None, **attributes):\n        \"\"\"\n        在聊天对象合集中进行搜索\n        \n        ..  note:: \n    \n            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象\n            | 建议搭配 :any:`ensure_one()` 使用\n\n        :param keywords: 聊天对象的名称关键词\n        :param attributes: 属性键值对，键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东'\n        :return: 匹配的聊天对象合集\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n\n        def match(chat):\n\n            if not match_name(chat, keywords):\n                return\n            if not match_attributes(chat, **attributes):\n                return\n            return True\n\n        return Chats(filter(match, self), self.source)\n\n    def stats(self, attribs=('sex', 'province', 'city')):\n        \"\"\"\n        统计各属性的分布情况\n\n        :param attribs: 需统计的属性列表或元组\n        :return: 统计结果\n        \"\"\"\n\n        def attr_stat(objects, attr_name):\n            return Counter(list(map(lambda x: getattr(x, attr_name), objects)))\n\n        from wxpy.utils import ensure_list\n        attribs = ensure_list(attribs)\n        ret = dict()\n        for attr in attribs:\n            ret[attr] = attr_stat(self, attr)\n        return ret\n\n    def stats_text(self, total=True, sex=True, top_provinces=10, top_cities=10):\n        \"\"\"\n        简单的统计结果的文本\n\n        :param total: 总体数量\n        :param sex: 性别分布\n        :param top_provinces: 省份分布\n        :param top_cities: 城市分布\n        :return: 统计结果文本\n        \"\"\"\n\n        from .group import Group\n        from wxpy.api.consts import FEMALE\n        from wxpy.api.consts import MALE\n        from wxpy.api.bot import Bot\n\n        def top_n_text(attr, n):\n            top_n = list(filter(lambda x: x[0], stats[attr].most_common()))[:n]\n            top_n = ['{}: {} ({:.2%})'.format(k, v, v / len(self)) for k, v in top_n]\n            return '\\n'.join(top_n)\n\n        stats = self.stats()\n\n        text = str()\n\n        if total:\n            if self.source:\n                if isinstance(self.source, Bot):\n                    user_title = '微信好友'\n                    nick_name = self.source.self.nick_name\n                elif isinstance(self.source, Group):\n                    user_title = '群成员'\n                    nick_name = self.source.nick_name\n                else:\n                    raise TypeError('source should be Bot or Group')\n                text += '{nick_name} 共有 {total} 位{user_title}\\n\\n'.format(\n                    nick_name=nick_name,\n                    total=len(self),\n                    user_title=user_title\n                )\n            else:\n                text += '共有 {} 位用户\\n\\n'.format(len(self))\n\n        if sex and self:\n            males = stats['sex'].get(MALE, 0)\n            females = stats['sex'].get(FEMALE, 0)\n\n            text += '男性: {males} ({male_rate:.1%})\\n女性: {females} ({female_rate:.1%})\\n\\n'.format(\n                males=males,\n                male_rate=males / len(self),\n                females=females,\n                female_rate=females / len(self),\n            )\n\n        if top_provinces and self:\n            text += 'TOP {} 省份\\n{}\\n\\n'.format(\n                top_provinces,\n                top_n_text('province', top_provinces)\n            )\n\n        if top_cities and self:\n            text += 'TOP {} 城市\\n{}\\n\\n'.format(\n                top_cities,\n                top_n_text('city', top_cities)\n            )\n\n        return text\n\n    def add_all(self, interval=3, verify_content=''):\n        \"\"\"\n        将合集中的所有用户加为好友，请小心应对调用频率限制！\n\n        :param interval: 间隔时间(秒)\n        :param verify_content: 验证说明文本\n        \"\"\"\n        to_add = self[:]\n\n        while to_add:\n            adding = to_add.pop(0)\n            logger.info('Adding {}'.format(adding))\n            ret = adding.add(verify_content=verify_content)\n            logger.info(ret)\n            logger.info('Waiting for {} seconds'.format(interval))\n            if to_add:\n                time.sleep(interval)\n"
  },
  {
    "path": "wxpy/api/chats/friend.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom .user import User\n\nlogger = logging.getLogger(__name__)\n\n\nclass Friend(User):\n    \"\"\"\n    好友对象\n    \"\"\"\n\n    pass\n"
  },
  {
    "path": "wxpy/api/chats/group.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import ensure_list, get_user_name, handle_response, wrap_user_name\nfrom .chat import Chat\nfrom .chats import Chats\nfrom .member import Member\n\nlogger = logging.getLogger(__name__)\n\n\nclass Group(Chat):\n    \"\"\"\n    群聊对象\n    \"\"\"\n\n    def __init__(self, raw, bot):\n        super(Group, self).__init__(raw, bot)\n\n    @property\n    def members(self):\n        \"\"\"\n        群聊的成员列表\n        \"\"\"\n\n        def raw_member_list(update=False):\n            if update:\n                self.update_group()\n            return self.raw.get('MemberList', list())\n\n        ret = Chats(source=self)\n        ret.extend(map(\n            lambda x: Member(x, self),\n            raw_member_list() or raw_member_list(True)\n        ))\n        return ret\n\n    def __contains__(self, user):\n        user_name = get_user_name(user)\n        for member in self.members:\n            if member.user_name == user_name:\n                return member\n\n    def __iter__(self):\n        for member in self.members:\n            yield member\n\n    def __len__(self):\n        return len(self.members)\n\n    def search(self, keywords=None, **attributes):\n        \"\"\"\n        在群聊中搜索成员\n        \n        ..  note:: \n        \n            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象\n            | 建议搭配 :any:`ensure_one()` 使用\n\n        :param keywords: 成员名称关键词\n        :param attributes: 属性键值对\n        :return: 匹配的群聊成员\n        :rtype: :class:`wxpy.Chats`\n        \"\"\"\n        return self.members.search(keywords, **attributes)\n\n    @property\n    def owner(self):\n        \"\"\"\n        返回群主对象\n        \"\"\"\n        owner_user_name = self.raw.get('ChatRoomOwner')\n        if owner_user_name:\n            for member in self:\n                if member.user_name == owner_user_name:\n                    return member\n        elif self.members:\n            return self.members[0]\n\n    @property\n    def is_owner(self):\n        \"\"\"\n        判断所属 bot 是否为群管理员\n        \"\"\"\n        return self.raw.get('IsOwner') == 1 or self.owner == self.bot.self\n\n    @property\n    def self(self):\n        \"\"\"\n        机器人自身 (作为群成员)\n        \"\"\"\n        for member in self.members:\n            if member == self.bot.self:\n                return member\n        return Member(self.bot.core.loginInfo['User'], self)\n\n    def update_group(self, members_details=False):\n        \"\"\"\n        更新群聊的信息\n\n        :param members_details: 是否包括群聊成员的详细信息 (地区、性别、签名等)\n        \"\"\"\n\n        logger.info('updating {} (members_details={})'.format(self, members_details))\n\n        @handle_response()\n        def do():\n            return self.bot.core.update_chatroom(self.user_name, members_details)\n\n        super(Group, self).__init__(do(), self.bot)\n\n    @handle_response()\n    def add_members(self, users, use_invitation=False):\n        \"\"\"\n        向群聊中加入用户\n\n        :param users: 待加入的用户列表或单个用户\n        :param use_invitation: 使用发送邀请的方式\n        \"\"\"\n\n        logger.info('adding {} into {} (use_invitation={}))'.format(users, self, use_invitation))\n\n        return self.bot.core.add_member_into_chatroom(\n            self.user_name,\n            ensure_list(wrap_user_name(users)),\n            use_invitation\n        )\n\n    @handle_response()\n    def remove_members(self, members):\n        \"\"\"\n        从群聊中移除用户\n\n        :param members: 待移除的用户列表或单个用户\n        \"\"\"\n\n        logger.info('removing {} from {}'.format(members, self))\n\n        return self.bot.core.delete_member_from_chatroom(\n            self.user_name,\n            ensure_list(wrap_user_name(members))\n        )\n\n    def rename_group(self, name):\n        \"\"\"\n        修改群聊名称\n\n        :param name: 新的名称，超长部分会被截断 (最长32字节)\n        \"\"\"\n\n        encodings = ('gbk', 'utf-8')\n\n        trimmed = False\n\n        for ecd in encodings:\n            for length in range(32, 24, -1):\n                try:\n                    name = bytes(name.encode(ecd))[:length].decode(ecd)\n                except (UnicodeEncodeError, UnicodeDecodeError):\n                    continue\n                else:\n                    trimmed = True\n                    break\n            if trimmed:\n                break\n\n        @handle_response()\n        def do():\n            logger.info('renaming group: {} => {}'.format(self.name, name))\n            return self.bot.core.set_chatroom_name(get_user_name(self), name)\n\n        ret = do()\n        self.update_group()\n        return ret\n"
  },
  {
    "path": "wxpy/api/chats/groups.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom wxpy.utils import ensure_list, match_attributes, match_name\nfrom .user import User\n\n\nclass Groups(list):\n    \"\"\"\n    群聊的合集，可用于按条件搜索\n    \"\"\"\n\n    # 记录已知的 shadow group 和 valid group\n    # shadow group 直接抛弃\n    # valid group 直接通过\n    # 其他的需要确认是否包含机器人自身，并再分类到上面两种群中\n\n    shadow_group_user_names = list()\n    valid_group_user_names = list()\n\n    def __init__(self, group_list=None):\n        if group_list:\n            # Web 微信服务端似乎有个 BUG，会返回不存在的群\n            # 具体表现为: 名称为先前退出的群，但成员列表却完全陌生\n            # 因此加一个保护逻辑: 只返回\"包含自己的群\"\n\n            groups_to_init = list()\n\n            for group in group_list:\n                if group.user_name in Groups.shadow_group_user_names:\n                    continue\n                elif group.user_name in Groups.valid_group_user_names:\n                    groups_to_init.append(group)\n                else:\n                    if group.bot.self in group:\n                        Groups.valid_group_user_names.append(group.user_name)\n                        groups_to_init.append(group)\n                    else:\n                        Groups.shadow_group_user_names.append(group.user_name)\n\n            super(Groups, self).__init__(groups_to_init)\n\n    def search(self, keywords=None, users=None, **attributes):\n        \"\"\"\n        在群聊合集中，根据给定的条件进行搜索\n\n        :param keywords: 群聊名称关键词\n        :param users: 需包含的用户\n        :param attributes: 属性键值对，键可以是 owner(群主对象), is_owner(自身是否为群主), nick_name(精准名称) 等。\n        :return: 匹配条件的群聊列表\n        :rtype: :class:`wxpy.Groups`\n        \"\"\"\n\n        users = ensure_list(users)\n        if users:\n            for user in users:\n                if not isinstance(user, User):\n                    raise TypeError('expected `User`, got {} (type: {})'.format(user, type(user)))\n\n        def match(group):\n            if not match_name(group, keywords):\n                return\n            if users:\n                for _user in users:\n                    if _user not in group:\n                        return\n            if not match_attributes(group, **attributes):\n                return\n            return True\n\n        return Groups(filter(match, self))\n"
  },
  {
    "path": "wxpy/api/chats/member.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom .user import User\n\n\n# Todo: 若尝试获取群成员信息时为空，自动更新成员信息 (并要照顾到遍历所有群成员的场景)\n\n\nclass Member(User):\n    \"\"\"\n    群聊成员对象\n    \"\"\"\n\n    def __init__(self, raw, group):\n        super(Member, self).__init__(raw, group.bot)\n        self._group_user_name = group.user_name\n\n    @property\n    def group(self):\n        for _group in self.bot.groups():\n            if _group.user_name == self._group_user_name:\n                return _group\n        raise Exception('failed to find the group belong to')\n\n    @property\n    def display_name(self):\n        \"\"\"\n        在群聊中的显示昵称\n        \"\"\"\n        return self.raw.get('DisplayName')\n\n    def remove(self):\n        \"\"\"\n        从群聊中移除该成员\n        \"\"\"\n        return self.group.remove_members(self)\n\n    @property\n    def name(self):\n        \"\"\"\n        | 该群成员的友好名称\n        | 具体为: 从 群聊显示名称、昵称(或群名称)，或微信号中，按序选取第一个可用的\n        \"\"\"\n        for attr in 'display_name', 'nick_name', 'wxid':\n            _name = getattr(self, attr, None)\n            if _name:\n                return _name\n"
  },
  {
    "path": "wxpy/api/chats/mp.py",
    "content": "# coding: utf-8\n\nfrom .user import User\n\n\nclass MP(User):\n    \"\"\"\n    公众号对象\n    \"\"\"\n    pass\n"
  },
  {
    "path": "wxpy/api/chats/user.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import handle_response\nfrom .chat import Chat\n\nlogger = logging.getLogger(__name__)\n\n\nclass User(Chat):\n    \"\"\"\n    好友(:class:`Friend`)、群聊成员(:class:`Member`)，和公众号(:class:`MP`) 的基础类\n    \"\"\"\n\n    def __init__(self, raw, bot):\n        super(User, self).__init__(raw, bot)\n\n    @property\n    def remark_name(self):\n        \"\"\"\n        备注名称\n        \"\"\"\n        return self.raw.get('RemarkName')\n\n    @handle_response()\n    def set_remark_name(self, remark_name):\n        \"\"\"\n        设置或修改好友的备注名称\n\n        :param remark_name: 新的备注名称\n        \"\"\"\n\n        logger.info('setting remark name for {}: {}'.format(self, remark_name))\n\n        return self.bot.core.set_alias(userName=self.user_name, alias=remark_name)\n\n    @property\n    def sex(self):\n        \"\"\"\n        性别，目前有::\n        \n            # 男性\n            MALE = 1\n            # 女性\n            FEMALE = 2\n        \n        未设置时为 `None`\n        \"\"\"\n        return self.raw.get('Sex')\n\n    @property\n    def province(self):\n        \"\"\"\n        省份\n        \"\"\"\n        return self.raw.get('Province')\n\n    @property\n    def city(self):\n        \"\"\"\n        城市\n        \"\"\"\n        return self.raw.get('City')\n\n    @property\n    def signature(self):\n        \"\"\"\n        个性签名\n        \"\"\"\n        return self.raw.get('Signature')\n\n    @property\n    def is_friend(self):\n        \"\"\"\n        判断当前用户是否为好友关系\n\n        :return: 若为好友关系，返回对应的好友，否则返回 False\n        \"\"\"\n        if self.bot:\n            try:\n                friends = self.bot.friends()\n                index = friends.index(self)\n                return friends[index]\n            except ValueError:\n                return False\n\n    def add(self, verify_content=''):\n        \"\"\"\n        把当前用户加为好友\n\n        :param verify_content: 验证信息(文本)\n        \"\"\"\n        return self.bot.add_friend(user=self, verify_content=verify_content)\n\n    def accept(self, verify_content=''):\n        \"\"\"\n        接受当前用户为好友\n\n        :param verify_content: 验证信息(文本)\n        :return: 新的好友对象\n        :rtype: :class:`wxpy.Friend`\n        \"\"\"\n        return self.bot.accept_friend(user=self, verify_content=verify_content)\n"
  },
  {
    "path": "wxpy/api/consts.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n# 文本\nTEXT = 'Text'\n# 位置\nMAP = 'Map'\n# 名片\nCARD = 'Card'\n# 提示\nNOTE = 'Note'\n# 分享\nSHARING = 'Sharing'\n# 图片\nPICTURE = 'Picture'\n# 语音\nRECORDING = 'Recording'\n# 文件\nATTACHMENT = 'Attachment'\n# 视频\nVIDEO = 'Video'\n# 好友请求\nFRIENDS = 'Friends'\n# 系统\nSYSTEM = 'System'\n\n# 男性\nMALE = 1\n# 女性\nFEMALE = 2\n"
  },
  {
    "path": "wxpy/api/messages/__init__.py",
    "content": "from .article import Article\nfrom .message import Message\nfrom .message_config import MessageConfig\nfrom .messages import Messages\nfrom .registered import Registered\nfrom .sent_message import SentMessage\n"
  },
  {
    "path": "wxpy/api/messages/article.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom wxpy.compatible.utils import force_encoded_string_output\n\n\nclass Article(object):\n    def __init__(self):\n        \"\"\"\n        公众号推送中的单篇文章内容 (一次可推送多篇)\n        \"\"\"\n\n        # 标题\n        self.title = None\n        # 摘要\n        self.summary = None\n        # 文章 URL\n        self.url = None\n        # 封面图片 URL\n        self.cover = None\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return self.__unicode__()\n\n    def __unicode__(self):\n        return '<{}: {}>'.format(self.__class__.__name__, self.title)\n\n    def __hash__(self):\n        return hash((Article, self.url))\n\n    def __eq__(self, other):\n        return hash(self) == hash(other)\n\n    def __cmp__(self, other):\n        if hash(self) == hash(other):\n            return 0\n        return 1\n"
  },
  {
    "path": "wxpy/api/messages/message.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport os\nimport tempfile\nimport weakref\nfrom datetime import datetime\nfrom xml.etree import ElementTree as ETree\n\ntry:\n    import html\nexcept ImportError:\n    # Python 2.6-2.7\n    # noinspection PyUnresolvedReferences,PyUnresolvedReferences,PyCompatibility\n    from HTMLParser import HTMLParser\n\n    html = HTMLParser()\n\nfrom wxpy.api.chats import Chat, Group, Member, User\nfrom wxpy.compatible.utils import force_encoded_string_output\nfrom wxpy.utils import wrap_user_name, repr_message\nfrom .article import Article\nfrom ..consts import ATTACHMENT, CARD, FRIENDS, MAP, PICTURE, RECORDING, SHARING, TEXT, VIDEO\nfrom ...compatible import *\n\nlogger = logging.getLogger(__name__)\n\n\nclass Message(object):\n    \"\"\"\n    单条消息对象，包括:\n    \n    * 来自好友、群聊、好友请求等聊天对象的消息\n    * 使用机器人账号在手机微信中发送的消息\n    \n    | 但 **不包括** 代码中通过 .send/reply() 系列方法发出的消息\n    | 此类消息请参见 :class:`SentMessage`\n    \"\"\"\n\n    def __init__(self, raw, bot):\n        self.raw = raw\n        self.bot = weakref.proxy(bot)\n\n        self._receive_time = datetime.now()\n\n        # 将 msg.chat.send* 方法绑定到 msg.reply*，例如 msg.chat.send_img => msg.reply_img\n        for method in '', '_image', '_file', '_video', '_msg', '_raw_msg':\n            setattr(self, 'reply' + method, getattr(self.chat, 'send' + method))\n\n    def __hash__(self):\n        return hash((Message, self.id))\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return repr_message(self)\n\n    def __unicode__(self):\n        return repr_message(self)\n\n    # basic\n\n    @property\n    def type(self):\n        \"\"\"\n        消息的类型，目前可为以下值::\n        \n            # 文本\n            TEXT = 'Text'\n            # 位置\n            MAP = 'Map'\n            # 名片\n            CARD = 'Card'\n            # 提示\n            NOTE = 'Note'\n            # 分享\n            SHARING = 'Sharing'\n            # 图片\n            PICTURE = 'Picture'\n            # 语音\n            RECORDING = 'Recording'\n            # 文件\n            ATTACHMENT = 'Attachment'\n            # 视频\n            VIDEO = 'Video'\n            # 好友请求\n            FRIENDS = 'Friends'\n            # 系统\n            SYSTEM = 'System'\n        \n        :rtype: str\n        \"\"\"\n        return self.raw.get('Type')\n\n    @property\n    def id(self):\n        \"\"\"\n        消息的唯一 ID (通常为大于 0 的 64 位整型)\n        \"\"\"\n        return self.raw.get('NewMsgId')\n\n    # content\n    @property\n    def text(self):\n        \"\"\"\n        消息的文本内容\n        \"\"\"\n        _type = self.type\n        _card = self.card\n\n        if _type == MAP:\n            location = self.location\n            if location:\n                return location.get('label')\n        elif _card:\n            if _type == CARD:\n                return _card.name\n            elif _type == FRIENDS:\n                return _card.raw.get('Content')\n\n        ret = self.raw.get('Text')\n        if isinstance(ret, str):\n            return ret\n\n    def get_file(self, save_path=None):\n        \"\"\"\n        下载图片、视频、语音、附件消息中的文件内容。\n        \n        可与 :any:`Message.file_name` 配合使用。\n\n        :param save_path: 文件的保存路径。若为 None，将直接返回字节数据\n        \"\"\"\n\n        _text = self.raw.get('Text')\n        if callable(_text) and self.type in (PICTURE, RECORDING, ATTACHMENT, VIDEO):\n            return _text(save_path)\n        else:\n            raise ValueError('download method not found, or invalid message type')\n\n    @property\n    def file_name(self):\n        \"\"\"\n        消息中文件的文件名\n        \"\"\"\n        return self.raw.get('FileName')\n\n    @property\n    def file_size(self):\n        \"\"\"\n        消息中文件的体积大小\n        \"\"\"\n        return self.raw.get('FileSize')\n\n    @property\n    def media_id(self):\n        \"\"\"\n        文件类消息中的文件资源 ID (但图片视频语音等其他消息中为空)\n        \"\"\"\n        return self.raw.get('MediaId')\n\n    # group\n\n    @property\n    def is_at(self):\n        \"\"\"\n        当消息来自群聊，且被 @ 时，为 True\n        \"\"\"\n        return self.raw.get('IsAt') or self.raw.get('isAt')\n\n    # misc\n\n    @property\n    def img_height(self):\n        \"\"\"\n        图片高度\n        \"\"\"\n        return self.raw.get('ImgHeight')\n\n    @property\n    def img_width(self):\n        \"\"\"\n        图片宽度\n        \"\"\"\n        return self.raw.get('ImgWidth')\n\n    @property\n    def play_length(self):\n        \"\"\"\n        视频长度\n        \"\"\"\n        return self.raw.get('PlayLength')\n\n    @property\n    def voice_length(self):\n        \"\"\"\n        语音长度\n        \"\"\"\n        return self.raw.get('VoiceLength')\n\n    @property\n    def url(self):\n        \"\"\"\n        分享类消息中的网页 URL\n        \"\"\"\n        _url = self.raw.get('Url')\n        if isinstance(_url, str):\n            _url = html.unescape(_url)\n\n        return _url\n\n    @property\n    def articles(self):\n        \"\"\"\n        公众号推送中的文章列表 (首篇的 标题/地址 与消息中的 text/url 相同)\n\n        其中，每篇文章均有以下属性:\n\n        * `title`: 标题\n        * `summary`: 摘要\n        * `url`: 文章 URL\n        * `cover`: 封面或缩略图 URL\n        \"\"\"\n\n        from wxpy import MP\n        if self.type == SHARING and isinstance(self.sender, MP):\n            tree = ETree.fromstring(self.raw['Content'])\n            # noinspection SpellCheckingInspection\n            items = tree.findall('.//mmreader/category/item')\n\n            article_list = list()\n\n            for item in items:\n                def find_text(tag):\n                    found = item.find(tag)\n                    if found is not None:\n                        return found.text\n\n                article = Article()\n                article.title = find_text('title')\n                article.summary = find_text('digest')\n                article.url = find_text('url')\n                article.cover = find_text('cover')\n                article_list.append(article)\n\n            return article_list\n\n    @property\n    def card(self):\n        \"\"\"\n        * 好友请求中的请求用户\n        * 名片消息中的推荐用户\n        \"\"\"\n        if self.type in (CARD, FRIENDS):\n            return User(self.raw.get('RecommendInfo'), self.bot)\n\n    # time\n\n    @property\n    def create_time(self):\n        \"\"\"\n        服务端发送时间\n        \"\"\"\n        # noinspection PyBroadException\n        try:\n            return datetime.fromtimestamp(self.raw.get('CreateTime'))\n        except:\n            pass\n\n    @property\n    def receive_time(self):\n        \"\"\"\n        本地接收时间\n        \"\"\"\n        return self._receive_time\n\n    @property\n    def latency(self):\n        \"\"\"\n        消息的延迟秒数 (发送时间和接收时间的差值)\n        \"\"\"\n        create_time = self.create_time\n        if create_time:\n            return (self.receive_time - create_time).total_seconds()\n\n    @property\n    def location(self):\n        \"\"\"\n        位置消息中的地理位置信息\n        \"\"\"\n        try:\n            ret = ETree.fromstring(self.raw['OriContent']).find('location').attrib\n            try:\n                ret['x'] = float(ret['x'])\n                ret['y'] = float(ret['y'])\n                ret['scale'] = int(ret['scale'])\n                ret['maptype'] = int(ret['maptype'])\n            except (KeyError, ValueError):\n                pass\n            return ret\n        except (TypeError, KeyError, ValueError, ETree.ParseError):\n            pass\n\n    # chats\n\n    @property\n    def chat(self):\n        \"\"\"\n        消息所在的聊天会话，即:\n\n        * 对于自己发送的消息，为消息的接收者\n        * 对于别人发送的消息，为消息的发送者\n        \n        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`\n        \"\"\"\n\n        if self.raw.get('FromUserName') == self.bot.self.user_name:\n            return self.receiver\n        else:\n            return self.sender\n\n    @property\n    def sender(self):\n        \"\"\"\n        消息的发送者\n        \n        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`\n        \"\"\"\n\n        return self._get_chat_by_user_name(self.raw.get('FromUserName'))\n\n    @property\n    def receiver(self):\n        \"\"\"\n        消息的接收者\n        \n        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`\n        \"\"\"\n\n        return self._get_chat_by_user_name(self.raw.get('ToUserName'))\n\n    @property\n    def member(self):\n        \"\"\"\n        * 若消息来自群聊，则此属性为消息的实际发送人(具体的群成员)\n        * 若消息来自其他聊天对象(非群聊)，则此属性为 None\n        \n        :rtype: NoneType, :class:`wxpy.Member`\n        \"\"\"\n\n        if isinstance(self.chat, Group):\n            if self.sender == self.bot.self:\n                return self.chat.self\n            else:\n                actual_user_name = self.raw.get('ActualUserName')\n                for _member in self.chat.members:\n                    if _member.user_name == actual_user_name:\n                        return _member\n                return Member(dict(\n                    UserName=actual_user_name,\n                    NickName=self.raw.get('ActualNickName')\n                ), self.chat)\n\n    def _get_chat_by_user_name(self, user_name):\n        \"\"\"\n        通过 user_name 找到对应的聊天对象\n\n        :param user_name: user_name\n        :return: 找到的对应聊天对象\n        \"\"\"\n\n        def match_in_chats(_chats):\n            for c in _chats:\n                if c.user_name == user_name:\n                    return c\n\n        _chat = None\n\n        if user_name.startswith('@@'):\n            _chat = match_in_chats(self.bot.groups())\n        elif user_name:\n            _chat = match_in_chats(self.bot.friends())\n            if not _chat:\n                _chat = match_in_chats(self.bot.mps())\n\n        if not _chat:\n            _chat = Chat(wrap_user_name(user_name), self.bot)\n\n        return _chat\n\n    def forward(self, chat, prefix=None, suffix=None, raise_for_unsupported=False):\n        \"\"\"\n        将本消息转发给其他聊天对象\n\n        支持以下消息类型\n            * 文本 (`TEXT`)\n            * 视频（`VIDEO`)\n            * 文件 (`ATTACHMENT`)\n            * 图片/自定义表情 (`PICTURE`)\n\n                * 但不支持表情商店中的表情\n\n            * 名片 (`CARD`)\n\n                * 仅支持公众号名片，以及自己发出的个人号名片\n\n            * 分享 (`SHARING`)\n\n                * 会转化为 `标题 + 链接` 形式的文本消息\n\n            * 语音 (`RECORDING`)\n\n                * 会以文件方式发送\n            \n            * 地图 (`MAP`)\n                \n                * 会转化为 `位置名称 + 地图链接` 形式的文本消息\n\n        :param Chat chat: 接收转发消息的聊天对象\n        :param str prefix: 转发时增加的 **前缀** 文本，原消息为文本时会自动换行\n        :param str suffix: 转发时增加的 **后缀** 文本，原消息为文本时会自动换行\n        :param bool raise_for_unsupported:\n            | 为 True 时，将为不支持的消息类型抛出 `NotImplementedError` 异常\n\n        例如，将公司群中的老板消息转发出来::\n\n            from wxpy import *\n\n            bot = Bot()\n\n            # 定位公司群\n            company_group = ensure_one(bot.groups().search('公司微信群'))\n\n            # 定位老板\n            boss = ensure_one(company_group.search('老板大名'))\n\n            # 将老板的消息转发到文件传输助手\n            @bot.register(company_group)\n            def forward_boss_message(msg):\n                if msg.member == boss:\n                    msg.forward(bot.file_helper, prefix='老板发言')\n\n            # 堵塞线程\n            embed()\n\n        \"\"\"\n\n        logger.info('{}: forwarding to {}: {}'.format(self.bot, chat, self))\n\n        def wrapped_send(send_type, *args, **kwargs):\n            if send_type == 'msg':\n                if args:\n                    text = args[0]\n                elif kwargs:\n                    text = kwargs['msg']\n                else:\n                    text = self.text\n                ret = chat.send_msg('{}{}{}'.format(\n                    str(prefix) + '\\n' if prefix else '',\n                    text,\n                    '\\n' + str(suffix) if suffix else '',\n                ))\n            else:\n                if prefix:\n                    chat.send_msg(prefix)\n                ret = getattr(chat, 'send_{}'.format(send_type))(*args, **kwargs)\n                if suffix:\n                    chat.send_msg(suffix)\n\n            return ret\n\n        def download_and_send():\n            fd, path = tempfile.mkstemp(\n                suffix='_{}'.format(self.file_name),\n                dir=self.bot.temp_dir.name\n            )\n\n            try:\n                self.get_file(path)\n                if self.type == PICTURE:\n                    return wrapped_send('image', path)\n                elif self.type == VIDEO:\n                    return wrapped_send('video', path)\n                else:\n                    return wrapped_send('file', path)\n            finally:\n                os.close(fd)\n\n        def raise_properly(text):\n            logger.warning(text)\n            if raise_for_unsupported:\n                raise NotImplementedError(text)\n\n        if self.type == TEXT:\n            return wrapped_send('msg')\n\n        elif self.type == SHARING:\n            return wrapped_send('msg', '{}\\n{}'.format(self.text, self.url))\n\n        elif self.type == MAP:\n            return wrapped_send('msg', '{}: {}\\n{}'.format(\n                self.location['poiname'], self.location['label'], self.url\n            ))\n\n        elif self.type == ATTACHMENT:\n\n            # noinspection SpellCheckingInspection\n            content = \\\n                \"<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''>\" \\\n                \"<title>{file_name}</title><des></des><action></action>\" \\\n                \"<type>6</type><content></content><url></url><lowurl></lowurl>\" \\\n                \"<appattach><totallen>{file_size}</totallen><attachid>{media_id}</attachid>\" \\\n                \"<fileext>{file_ext}</fileext></appattach><extinfo></extinfo></appmsg>\"\n\n            content = content.format(\n                file_name=self.file_name,\n                file_size=self.file_size,\n                media_id=self.media_id,\n                file_ext=os.path.splitext(self.file_name)[1].replace('.', '')\n            )\n\n            return wrapped_send(\n                send_type='raw_msg',\n                raw_type=self.raw['MsgType'],\n                raw_content=content,\n                uri='/webwxsendappmsg?fun=async&f=json'\n            )\n\n        elif self.type == CARD:\n            if self.card.raw.get('AttrStatus') and self.sender != self.bot.self:\n                # 为个人名片，且不为自己所发出\n                raise_properly('Personal cards sent from others are unsupported:\\n{}'.format(self))\n            else:\n                return wrapped_send(\n                    send_type='raw_msg',\n                    raw_type=self.raw['MsgType'],\n                    raw_content=self.raw['Content'],\n                    uri='/webwxsendmsg'\n                )\n\n        elif self.type == PICTURE:\n            if self.raw.get('HasProductId'):\n                # 来自表情商店的表情\n                raise_properly('Stickers from store are unsupported:\\n{}'.format(self))\n            else:\n                return download_and_send()\n\n        elif self.type == VIDEO:\n            return download_and_send()\n\n        elif self.type == RECORDING:\n            return download_and_send()\n\n        else:\n            raise_properly('Unsupported message type:\\n{}'.format(self))\n"
  },
  {
    "path": "wxpy/api/messages/message_config.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport weakref\n\nfrom wxpy.compatible.utils import force_encoded_string_output\nfrom wxpy.utils import ensure_list\n\nlogger = logging.getLogger(__name__)\n\n\nclass MessageConfig(object):\n    \"\"\"\n    单个消息注册配置\n    \"\"\"\n\n    def __init__(\n            self, bot, func,\n            chats, msg_types, except_self,\n            run_async, enabled\n    ):\n        self.bot = weakref.proxy(bot)\n        self.func = func\n\n        self.chats = ensure_list(chats)\n        self.msg_types = ensure_list(msg_types)\n        self.except_self = except_self\n\n        self.run_async = run_async\n        self._enabled = None\n        self.enabled = enabled\n\n    @property\n    def enabled(self):\n        \"\"\"\n        配置的开启状态\n        \"\"\"\n        return self._enabled\n\n    @enabled.setter\n    def enabled(self, boolean):\n        \"\"\"\n        设置配置的开启状态\n        \"\"\"\n        self._enabled = boolean\n        logger.info(self)\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return '<{}: {}: {} ({}{})>'.format(\n            self.__class__.__name__,\n            self.bot.self.name,\n            self.func.__name__,\n            'Enabled' if self.enabled else 'Disabled',\n            ', Async' if self.run_async else '',\n        )\n\n    def __unicode__(self):\n        return '<{}: {}: {} ({}{})>'.format(\n            self.__class__.__name__,\n            self.bot.self.name,\n            self.func.__name__,\n            'Enabled' if self.enabled else 'Disabled',\n            ', Async' if self.run_async else '',\n        )\n"
  },
  {
    "path": "wxpy/api/messages/messages.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport threading\n\nfrom wxpy.utils import match_attributes, match_text\n\n\nclass Messages(list):\n    \"\"\"\n    多条消息的合集，可用于记录或搜索\n    \"\"\"\n\n    def __init__(self, msg_list=None, max_history=200):\n        if msg_list:\n            super(Messages, self).__init__(msg_list)\n        self.max_history = max_history\n        self._thread_lock = threading.Lock()\n\n    def append(self, msg):\n        \"\"\"\n        仅当 self.max_history 为 int 类型，且大于 0 时才保存历史消息\n        \"\"\"\n        with self._thread_lock:\n            if isinstance(self.max_history, int) and self.max_history > 0:\n                del self[:-self.max_history + 1]\n                return super(Messages, self).append(msg)\n\n    def search(self, keywords=None, **attributes):\n        \"\"\"\n        搜索消息记录\n\n        :param keywords: 文本关键词\n        :param attributes: 属性键值对\n        :return: 所有匹配的消息\n        :rtype: :class:`wxpy.Messages`\n        \"\"\"\n\n        def match(msg):\n            if not match_text(msg.text, keywords):\n                return\n            if not match_attributes(msg, **attributes):\n                return\n            return True\n\n        return Messages(filter(match, self), max_history=self.max_history)\n"
  },
  {
    "path": "wxpy/api/messages/registered.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport weakref\n\nfrom wxpy.api.consts import SYSTEM\n\n\nclass Registered(list):\n    def __init__(self, bot):\n        \"\"\"\n        保存当前机器人所有已注册的消息配置\n\n        :param bot: 所属的机器人\n        \"\"\"\n        super(Registered, self).__init__()\n        self.bot = weakref.proxy(bot)\n\n    def get_config(self, msg):\n        \"\"\"\n        获取给定消息的注册配置。每条消息仅匹配一个注册配置，后注册的配置具有更高的匹配优先级。\n\n        :param msg: 给定的消息\n        :return: 匹配的回复配置\n        \"\"\"\n\n        for conf in self[::-1]:\n\n            if not conf.enabled or (conf.except_self and msg.sender == self.bot.self):\n                continue\n\n            if conf.msg_types and msg.type not in conf.msg_types:\n                continue\n            elif conf.msg_types is None and msg.type == SYSTEM:\n                continue\n\n            if conf.chats is None:\n                return conf\n\n            for chat in conf.chats:\n                if (isinstance(chat, type) and isinstance(msg.chat, chat)) or chat == msg.chat:\n                    return conf\n\n    def get_config_by_func(self, func):\n        \"\"\"\n        通过给定的函数找到对应的注册配置\n\n        :param func: 给定的函数\n        :return: 对应的注册配置\n        \"\"\"\n\n        for conf in self:\n            if conf.func == func:\n                return conf\n\n    def _change_status(self, func, enabled):\n        if func:\n            self.get_config_by_func(func).enabled = enabled\n        else:\n            for conf in self:\n                conf.enabled = enabled\n\n    def enable(self, func=None):\n        \"\"\"\n        开启指定函数的对应配置。若不指定函数，则开启所有已注册配置。\n\n        :param func: 指定的函数\n        \"\"\"\n        self._change_status(func, True)\n\n    def disable(self, func=None):\n        \"\"\"\n        关闭指定函数的对应配置。若不指定函数，则关闭所有已注册配置。\n\n        :param func: 指定的函数\n        \"\"\"\n        self._change_status(func, False)\n\n    def _check_status(self, enabled):\n        ret = list()\n        for conf in self:\n            if conf.enabled == enabled:\n                ret.append(conf)\n        return ret\n\n    @property\n    def enabled(self):\n        \"\"\"\n        检查处于开启状态的配置\n\n        :return: 处于开启状态的配置\n        \"\"\"\n        return self._check_status(True)\n\n    @property\n    def disabled(self):\n        \"\"\"\n        检查处于关闭状态的配置\n\n        :return: 处于关闭状态的配置\n        \"\"\"\n        return self._check_status(False)\n"
  },
  {
    "path": "wxpy/api/messages/sent_message.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.compatible.utils import force_encoded_string_output\nfrom wxpy.utils import repr_message\n\nlogger = logging.getLogger(__name__)\n\n\nclass SentMessage(object):\n    \"\"\"\n    程序中通过 .send/reply() 系列方法发出的消息\n    \n    *使用程序发送的消息也将被记录到历史消息 bot.messages 中*\n    \"\"\"\n\n    def __init__(self, attributes):\n\n        # 消息的类型 (仅可为 'Text', 'Picture', 'Video', 'Attachment')\n        self.type = None\n\n        # 消息的服务端 ID\n        self.id = None\n\n        # 消息的本地 ID (撤回时需要用到)\n        self.local_id = None\n\n        # 消息的文本内容\n        self.text = None\n\n        # 消息附件的本地路径\n        self.path = None\n\n        # 消息的附件 media_id\n        self.media_id = None\n\n        # 本地发送时间\n        self.create_time = None\n\n        # 接收服务端响应时间\n        self.receive_time = None\n\n        self.receiver = None\n\n        # send_raw_msg 的各属性\n        self.raw_type = None\n        self.raw_content = None\n        self.uri = None\n        self.msg_ext = None\n\n        for k, v in attributes.items():\n            setattr(self, k, v)\n\n    def __hash__(self):\n        return hash((SentMessage, self.id))\n\n    @force_encoded_string_output\n    def __repr__(self):\n        return repr_message(self)\n\n    def __unicode__(self):\n        return repr_message(self)\n\n    @property\n    def latency(self):\n        \"\"\"\n        消息的延迟秒数 (发送时间和响应时间的差值)\n        \"\"\"\n        if self.create_time and self.receive_time:\n            return (self.receive_time - self.create_time).total_seconds()\n\n    @property\n    def chat(self):\n        \"\"\"\n        消息所在的聊天会话 (始终为消息的接受者)\n        \"\"\"\n        return self.receiver\n\n    @property\n    def member(self):\n        \"\"\"\n        若在群聊中发送消息，则为群员\n        \"\"\"\n        from wxpy import Group\n\n        if isinstance(self.receiver, Group):\n            return self.receiver.self\n\n    @property\n    def bot(self):\n        \"\"\"\n        消息所属的机器人\n        \"\"\"\n        return self.receiver.bot\n\n    @property\n    def sender(self):\n        \"\"\"\n        消息的发送者\n        \"\"\"\n        return self.receiver.bot.self\n\n    def recall(self):\n        \"\"\"\n        撤回本条消息 (应为 2 分钟内发出的消息)\n        \"\"\"\n\n        logger.info('recalling msg:\\n{}'.format(self))\n\n        from wxpy.utils import BaseRequest\n        req = BaseRequest(self.bot, '/webwxrevokemsg')\n        req.data.update({\n            \"ClientMsgId\": self.local_id,\n            \"SvrMsgId\": str(self.id),\n            \"ToUserName\": self.receiver.user_name,\n        })\n\n        # noinspection PyUnresolvedReferences\n        return req.post()\n"
  },
  {
    "path": "wxpy/compatible/__init__.py",
    "content": "# coding: utf-8\nimport sys as _sys\n\nPY_VERSION = _sys.version\nPY2 = PY_VERSION < '3'\n\nif PY2:\n    from future.standard_library import print_function\n    from future.builtins import str, int\n\n\n\n"
  },
  {
    "path": "wxpy/compatible/utils.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function\nimport warnings as _warnings\nimport os as _os\nimport sys as _sys\n\nfrom tempfile import mkdtemp\n\nimport sys\n\nfrom . import *\n\n\nclass TemporaryDirectory(object):\n    \"\"\"Create and return a temporary directory.  This has the same\n    behavior as mkdtemp but can be used as a context manager.  For\n    example:\n\n        with TemporaryDirectory() as tmpdir:\n            ...\n\n    Upon exiting the context, the directory and everything contained\n    in it are removed.\n    \"\"\"\n\n    def __init__(self, suffix=\"\", prefix=\"tmp\", dir=None):\n        self._closed = False\n        self.name = None  # Handle mkdtemp raising an exception\n        self.name = mkdtemp(suffix, prefix, dir)\n\n    def __repr__(self):\n        return \"<{} {!r}>\".format(self.__class__.__name__, self.name)\n\n    def __enter__(self):\n        return self.name\n\n    def cleanup(self, _warn=False):\n        if self.name and not self._closed:\n            try:\n                self._rmtree(self.name)\n            except (TypeError, AttributeError) as ex:\n                # Issue #10188: Emit a warning on stderr\n                # if the directory could not be cleaned\n                # up due to missing globals\n                if \"None\" not in str(ex):\n                    raise\n                print(\"ERROR: {!r} while cleaning up {!r}\".format(ex, self, ), file=_sys.stderr)\n                return\n            self._closed = True\n            if _warn:\n                self._warn(\"Implicitly cleaning up {!r}\".format(self),\n                           ResourceWarning)\n\n    def __exit__(self, exc, value, tb):\n        self.cleanup()\n\n    def __del__(self):\n        # Issue a ResourceWarning if implicit cleanup needed\n        self.cleanup(_warn=True)\n\n    # XXX (ncoghlan): The following code attempts to make\n    # this class tolerant of the module nulling out process\n    # that happens during CPython interpreter shutdown\n    # Alas, it doesn't actually manage it. See issue #10188\n    _listdir = staticmethod(_os.listdir)\n    _path_join = staticmethod(_os.path.join)\n    _isdir = staticmethod(_os.path.isdir)\n    _islink = staticmethod(_os.path.islink)\n    _remove = staticmethod(_os.remove)\n    _rmdir = staticmethod(_os.rmdir)\n    _warn = _warnings.warn\n\n    def _rmtree(self, path):\n        # Essentially a stripped down version of shutil.rmtree.  We can't\n        # use globals because they may be None'ed out at shutdown.\n        for name in self._listdir(path):\n            fullname = self._path_join(path, name)\n            try:\n                isdir = self._isdir(fullname) and not self._islink(fullname)\n            except OSError:\n                isdir = False\n            if isdir:\n                self._rmtree(fullname)\n            else:\n                try:\n                    self._remove(fullname)\n                except OSError:\n                    pass\n        try:\n            self._rmdir(path)\n        except OSError:\n            pass\n\n\ndef force_encoded_string_output(func):\n\n    if sys.version_info.major < 3:\n\n        def _func(*args, **kwargs):\n            return func(*args, **kwargs).encode(sys.stdout.encoding or 'utf-8')\n\n        return _func\n\n    else:\n        return func"
  },
  {
    "path": "wxpy/exceptions.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\n\nclass ResponseError(Exception):\n    \"\"\"\n    当 BaseResponse 的返回值不为 0 时抛出的异常\n    \"\"\"\n\n    def __init__(self, err_code, err_msg):\n        super(ResponseError, self).__init__(\n            'err_code: {}; err_msg: {}'.format(err_code, err_msg))\n        self.err_code = err_code\n        self.err_msg = err_msg\n"
  },
  {
    "path": "wxpy/ext/__init__.py",
    "content": "from .logging_with_wechat import WeChatLoggingHandler, get_wechat_logger\nfrom .sync_message_in_groups import sync_message_in_groups\nfrom .tuling import Tuling\nfrom .xiaoi import XiaoI\n"
  },
  {
    "path": "wxpy/ext/logging_with_wechat.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import get_receiver\n\nlogger = logging.getLogger(__name__)\n\n\nclass WeChatLoggingHandler(logging.Handler):\n    def __init__(self, receiver=None):\n        \"\"\"\n        可向指定微信聊天对象发送日志的 Logging Handler\n\n        :param receiver:\n            * 当为 `None`, `True` 或字符串时，将以该值作为 `cache_path` 参数启动一个新的机器人，并发送到该机器人的\"文件传输助手\"\n            * 当为 :class:`机器人 <Bot>` 时，将发送到该机器人的\"文件传输助手\"\n            * 当为 :class:`聊天对象 <Chat>` 时，将发送到该聊天对象\n        \"\"\"\n\n        super(WeChatLoggingHandler, self).__init__()\n        self.receiver = get_receiver(receiver)\n\n    def emit(self, record):\n        if record.name.startswith('wxpy.'):\n            # 排除 wxpy 的日志\n            return\n\n        # noinspection PyBroadException\n        try:\n            self.receiver.send(self.format(record))\n        except:\n            # Todo: 将异常输出到屏幕\n            pass\n\n\ndef get_wechat_logger(receiver=None, name=None, level=logging.WARNING):\n    \"\"\"\n    获得一个可向指定微信聊天对象发送日志的 Logger\n\n    :param receiver:\n        * 当为 `None`, `True` 或字符串时，将以该值作为 `cache_path` 参数启动一个新的机器人，并发送到该机器人的\"文件传输助手\"\n        * 当为 :class:`机器人 <Bot>` 时，将发送到该机器人的\"文件传输助手\"\n        * 当为 :class:`聊天对象 <Chat>` 时，将发送到该聊天对象\n    :param name: Logger 名称\n    :param level: Logger 等级，默认为 `logging.WARNING`\n    :return: Logger\n    \"\"\"\n\n    _logger = logging.getLogger(name=name)\n    _logger.setLevel(level=level)\n    _logger.addHandler(WeChatLoggingHandler(receiver=receiver))\n\n    return _logger\n"
  },
  {
    "path": "wxpy/ext/sync_message_in_groups.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom binascii import crc32\n\nfrom wxpy.utils import start_new_thread\n\nemojis = \\\n    '😀😁😂🤣😃😄😅😆😉😊😋😎😍😘😗😙😚🙂🤗🤔😐😑😶🙄😏😣😥😮🤐😯' \\\n    '😪😫😴😌🤓😛😜😝🤤😒😓😔😕🙃🤑😲😇🤠🤡🤥😺😸😹😻😼😽🙀😿😾🙈' \\\n    '🙉🙊🌱🌲🌳🌴🌵🌾🌿🍀🍁🍂🍃🍇🍈🍉🍊🍋🍌🍍🍏🍐🍑🍒🍓🥝🍅🥑🍆🥔' \\\n    '🥕🌽🥒🍄🥜🌰🍞🥐🥖🥞🧀🍖🍗🥓🍔🍟🍕🌭🌮🌯🥙🥚🍳🥘🍲🥗🍿🍱🍘🍙' \\\n    '🍚🍛🍜🍝🍠🍢🍣🍤🍥🍡🍦🍧🍨🍩🍪🎂🍰🍫🍬🍭🍮🍯🍼🥛☕🍵🍶🍾🍷🍸' \\\n    '🍹🍺🍻🥂🥃🍴🥄🔪🏺🌍🌎🌏🌐🗾🌋🗻🏠🏡🏢🏣🏤🏥🏦🏨🏩🏪🏫🏬🏭🏯' \\\n    '🏰💒🗼🗽⛪🕌🕍🕋⛲⛺🌁🌃🌄🌅🌆🌇🌉🌌🎠🎡🎢💈🎪🎭🎨🎰🚂🚃🚄🚅' \\\n    '🚆🚇🚈🚉🚊🚝🚞🚋🚌🚍🚎🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚲🛴🛵🚏⛽🚨' \\\n    '🚥🚦🚧⚓⛵🛶🚤🚢🛫🛬💺🚁🚟🚠🚡🚀🚪🛌🚽🚿🛀🛁⌛⏳⌚⏰🌑🌒🌓🌔' \\\n    '🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞⭐🌟🌠⛅🌀🌈🌂☔⚡⛄🔥💧🌊🎃🎄🎆🎇✨🎈🎉' \\\n    '🎊🎋🎍🎎🎏🎐🎑🎁🎫🏆🏅🥇🥈🥉⚽⚾🏀🏐🏈🏉🎾🎱🎳🏏🏑🏒🏓🏸🥊🥋' \\\n    '🥅🎯⛳🎣🎽🎿🎮🎲🃏🎴🔇🔈🔉🔊📢📣📯🔔🔕🎼🎵🎶🎤🎧📻🎷🎸🎹🎺🎻' \\\n    '🥁📱📲📞📟📠🔋🔌💻💽💾💿📀🎥🎬📺📷📸📹📼🔍🔎🔬🔭📡💡🔦📔📕📖' \\\n    '📗📘📙📚📓📒📃📜📄📰📑🔖💰💴💵💶💷💸💳💱💲📧📨📩📤📥📦📫📪📬' \\\n    '📭📮📝💼📁📂📅📆📇📋📌📍📎📏📐🔒🔓🔏🔐🔑🔨🔫🏹🔧🔩🔗🚬🗿🔮🛒'\n\n\ndef assign_emoji(chat):\n    n = crc32(str(chat.wxid or chat.nick_name).encode()) & 0xffffffff\n    return emojis[n % len(emojis)]\n\n\ndef forward_prefix(user):\n    # represent for avatar\n    avatar_repr = assign_emoji(user)\n    return '{} · {}'.format(avatar_repr, user.name)\n\n\ndef sync_message_in_groups(\n        msg, groups, prefix=None, suffix=None,\n        raise_for_unsupported=False, run_async=True\n):\n    \"\"\"\n    将消息同步到多个微信群中\n\n    支持以下消息类型\n        * 文本 (`TEXT`)\n        * 视频（`VIDEO`)\n        * 文件 (`ATTACHMENT`)\n        * 图片/自定义表情 (`PICTURE`)\n\n            * 但不支持表情商店中的表情\n\n        * 名片 (`CARD`)\n\n            * 仅支持公众号名片，以及自己发出的个人号名片\n\n        * 分享 (`SHARING`)\n\n            * 会被转化为 `标题 + 链接` 形式的纯文本\n\n        * 语音 (`RECORDING`)\n\n            * 会以文件方式发送\n\n        * 地图 (`MAP`)\n            \n            * 会转化为 `位置名称 + 地图链接` 形式的文本消息\n\n    :param Message msg: 需同步的消息对象\n    :param Group groups: 需同步的群列表\n    :param str prefix:\n        * 转发时的 **前缀** 文本，原消息为文本时会自动换行\n        * 若不设定，则使用默认前缀作为提示\n    :param str suffix:\n        * 转发时的 **后缀** 文本，原消息为文本时会自动换行\n        * 默认为空\n    :param bool raise_for_unsupported:\n        | 为 True 时，将为不支持的消息类型抛出 `NotImplementedError` 异常\n    :param bool run_async: 是否异步执行，为 True 时不堵塞线程\n\n\n    ::\n\n        my_groups = [group1, group2, group3 ...]\n\n        @bot.register(my_groups, except_self=False)\n        def sync_my_groups(msg):\n            sync_message_in_groups(msg, my_groups)\n\n    \"\"\"\n\n    def process():\n        for group in groups:\n            if group == msg.chat:\n                continue\n\n            msg.forward(\n                chat=group, prefix=prefix, suffix=suffix,\n                raise_for_unsupported=raise_for_unsupported\n            )\n\n    if not prefix:\n        prefix = forward_prefix(msg.member)\n\n    if run_async:\n        start_new_thread(process, use_caller_name=True)\n    else:\n        process()\n"
  },
  {
    "path": "wxpy/ext/talk_bot_utils.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport random\nimport re\n\n\ndef get_context_user_id(msg, max_len=32, re_sub=r'[^a-zA-Z\\d]'):\n    \"\"\"\n    | 通过消息对象获取 Tuling, XiaoI 等聊天机器人的上下文用户 ID\n    | 上下文用户 ID: 为群聊时，取群员的 user_name；非群聊时，取聊天对象的 user_name\n\n    :param msg: 消息对象\n    :param max_len: 最大长度 (从末尾截取)\n    :param re_sub: 需要移除的字符的正则表达式 (为符合聊天机器人的 API 规范)\n    :return: 上下文用户 ID\n    \"\"\"\n\n    from wxpy.api.messages import Message\n    from wxpy.api.chats import Group\n\n    # 当 msg 不为消息对象时，返回 None\n    if not isinstance(msg, Message):\n        return\n\n    if isinstance(msg.sender, Group):\n        user = msg.member\n    else:\n        user = msg.sender\n\n    user_id = re.sub(re_sub, '', user.user_name)\n\n    return user_id[-max_len:]\n\n\ndef next_topic():\n    \"\"\"\n    聊天机器人无法获取回复时的备用回复\n    \"\"\"\n\n    return random.choice((\n        '换个话题吧',\n        '聊点别的吧',\n        '下一个话题吧',\n        '无言以对呢',\n        '这话我接不了呢'\n    ))\n"
  },
  {
    "path": "wxpy/ext/tuling.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport logging\nimport pprint\n\nimport requests\n\nfrom wxpy.ext.talk_bot_utils import get_context_user_id, next_topic\nfrom wxpy.utils.misc import get_text_without_at_bot\nfrom wxpy.utils import enhance_connection\nfrom wxpy.compatible import *\n\nlogger = logging.getLogger(__name__)\n\n\nclass Tuling(object):\n    \"\"\"\n    与 wxpy 深度整合的图灵机器人\n    \"\"\"\n\n    'API 文档: http://tuling123.com/help/h_cent_webapi.jhtml'\n\n    # 考虑升级 API 版本: http://doc.tuling123.com/openapi2/263611\n\n    url = 'http://www.tuling123.com/openapi/api'\n\n    def __init__(self, api_key=None):\n        \"\"\"\n        | 内置的 api key 存在调用限制，建议自行申请。\n        | 免费申请: http://www.tuling123.com/\n\n        :param api_key: 你申请的 api key\n        \"\"\"\n\n        self.session = requests.Session()\n        enhance_connection(self.session)\n\n        # noinspection SpellCheckingInspection\n        self.api_key = api_key or '7c8cdb56b0dc4450a8deef30a496bd4c'\n        self.last_member = dict()\n\n    def is_last_member(self, msg):\n        if msg.member == self.last_member.get(msg.chat):\n            return True\n        else:\n            self.last_member[msg.chat] = msg.member\n\n    def do_reply(self, msg, at_member=True):\n        \"\"\"\n        回复消息，并返回答复文本\n\n        :param msg: Message 对象\n        :param at_member: 若消息来自群聊，回复时 @发消息的群成员\n        :return: 答复文本\n        :rtype: str\n        \"\"\"\n        ret = self.reply_text(msg, at_member)\n        msg.reply(ret)\n        return ret\n\n    def reply_text(self, msg, at_member=True):\n        \"\"\"\n        仅返回消息的答复文本\n\n        :param msg: Message 对象\n        :param at_member: 若消息来自群聊，回复时 @发消息的群成员\n        :return: 答复文本\n        :rtype: str\n        \"\"\"\n\n        def process_answer():\n\n            logger.debug('Tuling answer:\\n' + pprint.pformat(answer))\n\n            ret = str()\n            if at_member:\n                if len(msg.chat) > 2 and msg.member.name and not self.is_last_member(msg):\n                    ret += '@{} '.format(msg.member.name)\n\n            code = -1\n            if answer:\n                code = answer.get('code', -1)\n\n            if code >= 100000:\n                text = answer.get('text')\n                if not text or (text == msg.text and len(text) > 3):\n                    text = next_topic()\n                url = answer.get('url')\n                items = answer.get('list', list())\n\n                ret += str(text)\n                if url:\n                    ret += '\\n{}'.format(url)\n                for item in items:\n                    ret += '\\n\\n{}\\n{}'.format(\n                        item.get('article') or item.get('name'),\n                        item.get('detailurl')\n                    )\n\n            else:\n                ret += next_topic()\n\n            return ret\n\n        def get_location(_chat):\n\n            province = getattr(_chat, 'province', None) or ''\n            city = getattr(_chat, 'city', None) or ''\n\n            if province in ('北京', '上海', '天津', '重庆'):\n                return '{}市{}区'.format(province, city)\n            elif province and city:\n                return '{}省{}市'.format(province, city)\n\n        if not msg.bot:\n            raise ValueError('bot not found: {}'.format(msg))\n\n        if not msg.text:\n            return\n\n        from wxpy.api.chats import Group\n        if at_member and isinstance(msg.chat, Group) and msg.member:\n            location = get_location(msg.member)\n        else:\n            # 使该选项失效，防止错误 @ 人\n            at_member = False\n            location = get_location(msg.chat)\n\n        user_id = get_context_user_id(msg)\n\n        if location:\n            location = location[:30]\n\n        info = str(get_text_without_at_bot(msg))[-30:]\n\n        payload = dict(\n            key=self.api_key,\n            info=info,\n            userid=user_id,\n            loc=location\n        )\n\n        logger.debug('Tuling payload:\\n' + pprint.pformat(payload))\n\n        # noinspection PyBroadException\n        try:\n            r = self.session.post(self.url, json=payload)\n            answer = r.json()\n        except:\n            answer = None\n        finally:\n            return process_answer()\n"
  },
  {
    "path": "wxpy/ext/xiaoi.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n# created by: Han Feng (https://github.com/hanx11)\n\nimport collections\nimport hashlib\nimport logging\n\nimport requests\n\nfrom wxpy.api.messages import Message\nfrom wxpy.ext.talk_bot_utils import get_context_user_id, next_topic\nfrom wxpy.utils.misc import get_text_without_at_bot\nfrom wxpy.utils import enhance_connection\n\nlogger = logging.getLogger(__name__)\n\nfrom wxpy.compatible import *\n\nclass XiaoI(object):\n    \"\"\"\n    与 wxpy 深度整合的小 i 机器人\n    \"\"\"\n\n    # noinspection SpellCheckingInspection\n    def __init__(self, key, secret):\n        \"\"\"\n        | 需要通过注册获得 key 和 secret\n        | 免费申请: http://cloud.xiaoi.com/\n\n        :param key: 你申请的 key\n        :param secret: 你申请的 secret\n        \"\"\"\n\n        self.key = key\n        self.secret = secret\n\n        self.realm = \"xiaoi.com\"\n        self.http_method = \"POST\"\n        self.uri = \"/ask.do\"\n        self.url = \"http://nlp.xiaoi.com/ask.do?platform=custom\"\n\n        xauth = self._make_http_header_xauth()\n\n        headers = {\n            \"Content-type\": \"application/x-www-form-urlencoded\",\n            \"Accept\": \"text/plain\",\n        }\n\n        headers.update(xauth)\n\n        self.session = requests.Session()\n        self.session.headers.update(headers)\n        enhance_connection(self.session)\n\n    def _make_signature(self):\n        \"\"\"\n        生成请求签名\n        \"\"\"\n\n        # 40位随机字符\n        # nonce = \"\".join([str(randint(0, 9)) for _ in range(40)])\n        nonce = \"4103657107305326101203516108016101205331\"\n\n        sha1 = \"{0}:{1}:{2}\".format(self.key, self.realm, self.secret).encode(\"utf-8\")\n        sha1 = hashlib.sha1(sha1).hexdigest()\n        sha2 = \"{0}:{1}\".format(self.http_method, self.uri).encode(\"utf-8\")\n        sha2 = hashlib.sha1(sha2).hexdigest()\n\n        signature = \"{0}:{1}:{2}\".format(sha1, nonce, sha2).encode(\"utf-8\")\n        signature = hashlib.sha1(signature).hexdigest()\n\n        ret = collections.namedtuple(\"signature_return\", \"signature nonce\")\n        ret.signature = signature\n        ret.nonce = nonce\n\n        return ret\n\n    def _make_http_header_xauth(self):\n        \"\"\"\n        生成请求认证\n        \"\"\"\n\n        sign = self._make_signature()\n\n        ret = {\n            \"X-Auth\": \"app_key=\\\"{0}\\\",nonce=\\\"{1}\\\",signature=\\\"{2}\\\"\".format(\n                self.key, sign.nonce, sign.signature)\n        }\n\n        return ret\n\n    def do_reply(self, msg):\n        \"\"\"\n        回复消息，并返回答复文本\n\n        :param msg: Message 对象\n        :return: 答复文本\n        \"\"\"\n\n        ret = self.reply_text(msg)\n        msg.reply(ret)\n        return ret\n\n    def reply_text(self, msg):\n        \"\"\"\n        仅返回答复文本\n\n        :param msg: Message 对象，或消息文本\n        :return: 答复文本\n        \"\"\"\n\n        error_response = (\n            \"主人还没给我设置这类话题的回复\",\n        )\n\n        if isinstance(msg, Message):\n            user_id = get_context_user_id(msg)\n            question = get_text_without_at_bot(msg)\n        else:\n            user_id = \"abc\"\n            question = msg or \"\"\n\n        params = {\n            \"question\": question,\n            \"format\": \"json\",\n            \"platform\": \"custom\",\n            \"userId\": user_id,\n        }\n\n        resp = self.session.post(self.url, data=params)\n        text = resp.text\n\n        for err in error_response:\n            if err in text:\n                return next_topic()\n\n        return text\n"
  },
  {
    "path": "wxpy/utils/__init__.py",
    "content": "from .base_request import BaseRequest\nfrom .console import embed, shell_entry\nfrom .misc import decode_text_from_webwx, enhance_connection, enhance_webwx_request, ensure_list, get_receiver, \\\n    get_text_without_at_bot, get_user_name, handle_response, match_attributes, match_name, match_text, repr_message, \\\n    smart_map, start_new_thread, wrap_user_name\nfrom .puid_map import PuidMap\nfrom .tools import detect_freq_limit, dont_raise_response_error, ensure_one, mutual_friends\n"
  },
  {
    "path": "wxpy/utils/base_request.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport functools\nimport json\n\nimport itchat.config\nimport itchat.returnvalues\n\nfrom .misc import handle_response\n\n\nclass BaseRequest(object):\n    def __init__(self, bot, uri, params=None):\n        \"\"\"\n        基本的 Web 微信请求模板，可用于修改后发送请求\n        \n        可修改属性包括:\n        \n        * url (会通过 url 参数自动拼接好)\n        * data (默认仅包含 BaseRequest 部分)\n        * headers\n        \n        :param bot: 所使用的机器人对象\n        :param uri: API 路径，将与基础 URL 进行拼接\n        \"\"\"\n        self.bot = bot\n        self.url = self.bot.core.loginInfo['url'] + uri\n        self.params = params\n        self.data = {'BaseRequest': self.bot.core.loginInfo['BaseRequest']}\n        self.headers = {\n            'ContentType': 'application/json; charset=UTF-8',\n            'User-Agent': itchat.config.USER_AGENT\n        }\n\n        for method in 'get', 'post', 'put', 'delete':\n            setattr(self, method, functools.partial(\n                self.request, method=method.upper()\n            ))\n\n    def request(self, method, to_class=None):\n        \"\"\"\n        (在完成修改后) 发送请求\n        \n        :param method: 请求方法: 'GET', 'POST'，'PUT', 'DELETE' 等\n        :param to_class: 使用 `@handle_response(to_class)` 把结果转化为相应的类\n        \"\"\"\n\n        if self.data:\n            self.data = json.dumps(self.data, ensure_ascii=False).encode('utf-8')\n        else:\n            self.data = None\n\n        @handle_response(to_class)\n        def do():\n            return itchat.returnvalues.ReturnValue(\n                rawResponse=self.bot.core.s.request(\n                    method=method,\n                    url=self.url,\n                    params=self.params,\n                    data=self.data,\n                    headers=self.headers\n                ))\n\n        return do()\n"
  },
  {
    "path": "wxpy/utils/console.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport inspect\n\nfrom wxpy.compatible import PY2\n\n\ndef _ipython(local, banner):\n    from IPython.terminal.embed import InteractiveShellEmbed\n    from IPython.terminal.ipapp import load_default_config\n\n    InteractiveShellEmbed.clear_instance()\n    shell = InteractiveShellEmbed.instance(\n        banner1=banner,\n        user_ns=local,\n        config=load_default_config()\n    )\n    shell()\n\n\ndef _bpython(local, banner):\n    # noinspection PyUnresolvedReferences,PyPackageRequirements\n    import bpython\n\n    bpython.embed(locals_=local, banner=banner)\n\n\ndef _python(local, banner):\n    import code\n\n    try:\n        # noinspection PyUnresolvedReferences\n        import readline\n    except ImportError:\n        pass\n    else:\n        import rlcompleter\n        readline.parse_and_bind('tab:complete')\n    if PY2:\n        banner = banner.encode('utf-8')\n\n    code.interact(local=local, banner=banner)\n\n\ndef embed(local=None, banner='', shell=None):\n    \"\"\"\n    | 进入交互式的 Python 命令行界面，并堵塞当前线程\n    | 支持使用 ipython, bpython 以及原生 python\n\n    :param str shell:\n        | 指定命令行类型，可设为 'ipython'，'bpython'，'python'，或它们的首字母；\n        | 若为 `None`，则按上述优先级进入首个可用的 Python 命令行。\n    :param dict local: 设定本地变量环境，若为 `None`，则获取进入之前的变量环境。\n    :param str banner: 设定欢迎内容，将在进入命令行后展示。\n    \"\"\"\n\n    import inspect\n\n    if not local:\n        local = inspect.currentframe().f_back.f_locals\n\n    if isinstance(shell, str):\n        shell = shell.strip().lower()\n        if shell.startswith('b'):\n            shell = _bpython\n        elif shell.startswith('i'):\n            shell = _ipython\n        elif shell.startswith('p') or not shell:\n            shell = _python\n\n    for _shell in shell, _ipython, _bpython, _python:\n        try:\n            _shell(local=local, banner=banner)\n        except (TypeError, ImportError):\n            continue\n        except KeyboardInterrupt:\n            break\n        else:\n            break\n\n\ndef get_arg_parser():\n    import argparse\n\n    ap = argparse.ArgumentParser(\n        description='Run a wxpy-ready python console.')\n\n    ap.add_argument(\n        'bot', type=str, nargs='*',\n        help='One or more variable name(s) for bot(s) to init (default: None).')\n\n    ap.add_argument(\n        '-c', '--cache', action='store_true',\n        help='Cache session(s) for a short time, or load session(s) from cache '\n             '(default: disabled).')\n\n    ap.add_argument(\n        '-q', '--console_qr', type=int, default=False, metavar='width',\n        help='The width for console_qr (default: None).')\n\n    ap.add_argument(\n        '-l', '--logging_level', type=str, default='INFO', metavar='level',\n        help='Logging level (default: INFO).')\n\n    ap.add_argument(\n        '-s', '--shell', type=str, default=None, metavar='shell',\n        help='Specify which shell to use: ipython, bpython, or python '\n             '(default: the first available).')\n\n    ap.add_argument(\n        '-v', '--version', action='store_true',\n        help='Show version and exit.')\n\n    return ap\n\n\ndef shell_entry():\n    import re\n\n    import logging\n    import wxpy\n\n    arg_parser = get_arg_parser()\n    args = arg_parser.parse_args()\n\n    if args.bot:\n        def get_logging_level():\n            logging_level = args.logging_level.upper()\n            for level in 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET':\n                if level.startswith(logging_level):\n                    return getattr(logging, level)\n            else:\n                return logging.INFO\n\n        logging.basicConfig(level=get_logging_level())\n\n        try:\n            bots = dict()\n            for name in args.bot:\n                if not re.match(r'\\w+$', name):\n                    continue\n                cache_path = 'wxpy_{}.pkl'.format(name) if args.cache else None\n                bots[name] = wxpy.Bot(cache_path=cache_path, console_qr=args.console_qr)\n        except KeyboardInterrupt:\n            return\n\n        banner = 'from wxpy import *\\n'\n\n        for k, v in bots.items():\n            banner += '{}: {}\\n'.format(k, v)\n\n        module_members = dict(inspect.getmembers(wxpy))\n\n        embed(\n            local=dict(module_members, **bots),\n            banner=banner,\n            shell=args.shell\n        )\n    elif args.version:\n        print(wxpy.version_details)\n    else:\n        arg_parser.print_help()\n"
  },
  {
    "path": "wxpy/utils/misc.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport inspect\nimport logging\nimport random\nimport re\nimport threading\nimport weakref\nfrom functools import wraps\n\nimport requests\nfrom requests.adapters import HTTPAdapter\n\nfrom wxpy.compatible import PY2\nfrom wxpy.exceptions import ResponseError\n\nif PY2:\n    from future.builtins import str\n\n\ndef decode_text_from_webwx(text):\n    \"\"\"\n    解码从 Web 微信获得到的中文乱码\n\n    :param text: 从 Web 微信获得到的中文乱码\n    \"\"\"\n    if isinstance(text, str):\n        try:\n            text = text.encode('raw_unicode_escape').decode()\n        except UnicodeDecodeError:\n            pass\n    return text\n\n\ndef check_response_body(response_body):\n    \"\"\"\n    检查 response body: err_code 不为 0 时抛出 :class:`ResponseError` 异常\n\n    :param response_body: response body\n    \"\"\"\n\n    try:\n        base_response = response_body['BaseResponse']\n        err_code = base_response['Ret']\n        err_msg = base_response['ErrMsg']\n    except (KeyError, TypeError):\n        pass\n    else:\n        if err_code != 0:\n            if int(err_code) > 0:\n                err_msg = decode_text_from_webwx(err_msg)\n            raise ResponseError(err_code=err_code, err_msg=err_msg)\n\n\ndef handle_response(to_class=None):\n    \"\"\"\n    装饰器：检查从 itchat 返回的字典对象，并将其转化为指定类的实例\n    若返回值不为0，会抛出 ResponseError 异常\n\n    :param to_class: 需转化成的类，若为None则不转换\n    \"\"\"\n\n    def decorator(func):\n        @wraps(func)\n        def wrapped(*args, **kwargs):\n            ret = func(*args, **kwargs)\n\n            if ret is None:\n                return\n\n            smart_map(check_response_body, ret)\n\n            if to_class:\n                if args:\n                    self = args[0]\n                else:\n                    self = inspect.currentframe().f_back.f_locals.get('self')\n\n                from wxpy.api.bot import Bot\n                if isinstance(self, Bot):\n                    bot = weakref.proxy(self)\n                else:\n                    bot = getattr(self, 'bot', None)\n                    if not bot:\n                        raise ValueError('bot not found:m\\nmethod: {}\\nself: {}\\nbot: {}'.format(\n                            func, self, bot\n                        ))\n\n                ret = smart_map(to_class, ret, bot)\n\n                if isinstance(ret, list):\n                    from wxpy.api.chats import Group\n                    if to_class == Group:\n                        from wxpy.api.chats import Groups\n                        ret = Groups(ret)\n                    else:\n                        from wxpy.api.chats import Chats\n                        ret = Chats(ret, bot)\n\n            return ret\n\n        return wrapped\n\n    return decorator\n\n\ndef ensure_list(x, except_false=True):\n    \"\"\"\n    若传入的对象不为列表，则转化为列表\n\n    :param x: 输入对象\n    :param except_false: None, False 等例外，会直接返回原值\n    :return: 列表，或 None, False 等\n    :rtype: list\n    \"\"\"\n\n    if isinstance(x, (list, tuple)) or (not x and except_false):\n        return x\n    return [x]\n\n\ndef prepare_keywords(keywords):\n    \"\"\"\n    准备关键词\n    \"\"\"\n\n    if not keywords:\n        keywords = ''\n    if isinstance(keywords, str):\n        # noinspection PyTypeChecker\n        keywords = re.split(r'\\s+', keywords)\n    return map(lambda x: x.lower(), keywords)\n\n\ndef match_text(text, keywords):\n    \"\"\"\n    判断文本内容中是否包含了所有的关键词 (不区分大小写)\n\n    :param text: 文本内容\n    :param keywords: 关键词，可以是空白分割的 str，或是多个精准关键词组成的 list\n    :return: 若包含了所有的关键词则为 True，否则为 False\n    \"\"\"\n\n    if not text:\n        text = ''\n    else:\n        text = text.lower()\n\n    keywords = prepare_keywords(keywords)\n\n    for kw in keywords:\n        if kw not in text:\n            return False\n    return True\n\n\ndef match_attributes(obj, **attributes):\n    \"\"\"\n    判断对象是否匹配输入的属性条件\n\n    :param obj: 对象\n    :param attributes: 属性键值对\n    :return: 若匹配则为 True，否则为 False\n    \"\"\"\n\n    has_raw = hasattr(obj, 'raw')\n\n    for attr, value in attributes.items():\n        if (getattr(obj, attr, None) or (obj.raw.get(attr) if has_raw else None)) != value:\n            return False\n    return True\n\n\ndef match_name(chat, keywords):\n    \"\"\"\n    判断一个 Chat 对象的名称是否包含了所有的关键词 (不区分大小写)\n\n    :param chat: Chat 对象\n    :param keywords: 关键词，可以是空白分割的 str，或是多个精准关键词组成的 list\n    :return: 若包含了所有的关键词则为 True，否则为 False\n    \"\"\"\n    keywords = prepare_keywords(keywords)\n\n    for kw in keywords:\n        for attr in 'remark_name', 'display_name', 'nick_name', 'wxid':\n            if kw in '{0}'.format(getattr(chat, attr, '')).lower():\n                break\n        else:\n            return False\n    return True\n\n\ndef smart_map(func, i, *args, **kwargs):\n    \"\"\"\n    将单个对象或列表中的每个项传入给定的函数，并返回单个结果或列表结果，类似于 map 函数\n\n    :param func: 传入到的函数\n    :param i: 列表或单个对象\n    :param args: func 函数所需的 args\n    :param kwargs: func 函数所需的 kwargs\n    :return: 若传入的为列表，则以列表返回每个结果，反之为单个结果\n    \"\"\"\n    if isinstance(i, (list, tuple, set)):\n        return list(map(lambda x: func(x, *args, **kwargs), i))\n    else:\n        return func(i, *args, **kwargs)\n\n\ndef wrap_user_name(user_or_users):\n    \"\"\"\n    确保将用户转化为带有 UserName 键的用户字典\n\n    :param user_or_users: 单个用户，或列表形式的多个用户\n    :return: 单个用户字典，或列表形式的多个用户字典\n    \"\"\"\n\n    from wxpy.api.chats import Chat\n\n    def wrap_one(x):\n        if isinstance(x, dict):\n            return x\n        elif isinstance(x, Chat):\n            return x.raw\n        elif isinstance(x, str):\n            return {'UserName': user_or_users}\n        else:\n            if PY2:\n                # noinspection PyUnresolvedReferences\n                if isinstance(x, unicode):\n                    return {'UserName': user_or_users}\n            raise TypeError('Unsupported type: {}'.format(type(x)))\n\n    return smart_map(wrap_one, user_or_users)\n\n\ndef get_user_name(user_or_users):\n    \"\"\"\n    确保将用户转化为 user_name 字串\n\n    :param user_or_users: 单个用户，或列表形式的多个用户\n    :return: 返回单个 user_name 字串，或列表形式的多个 user_name 字串\n    \"\"\"\n\n    from wxpy.api.chats import Chat\n\n    def get_one(x):\n        if isinstance(x, Chat):\n            return x.user_name\n        elif isinstance(x, dict):\n            return x['UserName']\n        elif isinstance(x, str):\n            return x\n        else:\n            if PY2:\n                # noinspection PyUnresolvedReferences\n                if isinstance(x, unicode):\n                    return x\n            raise TypeError('Unsupported type: {}'.format(type(x)))\n\n    return smart_map(get_one, user_or_users)\n\n\ndef get_receiver(receiver=None):\n    \"\"\"\n    获得作为接收者的聊天对象\n\n    :param receiver:\n        * 当为 `None`, `True` 或字符串时，将以该值作为 `cache_path` 参数启动一个新的机器人，并返回该机器人的\"文件传输助手\"\n        * 当为 :class:`机器人 <Bot>` 时，将返回该机器人的\"文件传输助手\"\n        * 当为 :class:`聊天对象 <Chat>` 时，将返回该聊天对象\n    :return: 作为接收者的聊天对象\n    :rtype: :class:`wxpy.Chat`\n    \"\"\"\n\n    from wxpy.api.chats import Chat\n    from wxpy.api.bot import Bot\n\n    if isinstance(receiver, Chat):\n        return receiver\n    elif isinstance(receiver, Bot):\n        return receiver.file_helper\n    elif receiver in (None, True) or isinstance(receiver, str):\n        return Bot(cache_path=receiver).file_helper\n    else:\n        raise TypeError('expected Chat, Bot, str, True or None')\n\n\ndef enhance_connection(session, pool_connections=30, pool_maxsize=30, max_retries=30):\n    \"\"\"\n    增强 requests.Session 对象的网络连接性能\n\n    :param session: 需增强的 requests.Session 对象\n    :param pool_connections: 最大的连接池缓存数量\n    :param pool_maxsize: 连接池中的最大连接保存数量\n    :param max_retries: 最大的连接重试次数 (仅处理 DNS 查询, socket 连接，以及连接超时)\n    \"\"\"\n\n    for p in 'http', 'https':\n        session.mount(\n            '{}://'.format(p),\n            HTTPAdapter(\n                pool_connections=pool_connections,\n                pool_maxsize=pool_maxsize,\n                max_retries=max_retries,\n                pool_block=False\n            ))\n\n\ndef enhance_webwx_request(bot, sync_check_timeout=(10, 30), webwx_sync_timeout=(10, 20)):\n    \"\"\"\n    针对 Web 微信增强机器人的网络请求\n\n    :param bot: 需优化的机器人实例\n    :param sync_check_timeout: 请求 \"synccheck\" 时的超时秒数\n    :param webwx_sync_timeout: 请求 \"webwxsync\" 时的超时秒数\n    \"\"\"\n\n    login_info = bot.core.loginInfo\n    session = bot.core.s\n\n    # get: 用于检查是否有新消息\n    sync_check_url = '{}/synccheck'.format(login_info.get('syncUrl', login_info['url']))\n\n    # post: 用于获取消息和更新联系人\n    webwx_sync_url = '{li[url]}/webwxsync?sid={li[wxsid]}&skey={li[skey]}' \\\n                     '&pass_ticket={li[pass_ticket]}'.format(li=login_info)\n\n    # noinspection PyProtectedMember\n    def customized_request(method, url, **kwargs):\n        \"\"\"\n        根据 请求方法 和 url 灵活调整各种参数\n        \"\"\"\n\n        if method.upper() == 'GET':\n            if url == sync_check_url:\n                # 设置一个超时，避免无尽等待而停止发送心跳，导致出现 1101 错误\n                kwargs['timeout'] = sync_check_timeout\n\n                # deviceid 应每次都变化，否则会导致该连接断开不及时，接收消息变慢\n                kwargs['params']['deviceid'] = 'e{}'.format(str(random.random())[2:17])\n\n                bot._sync_check_iterations += 1\n                kwargs['params']['_'] = bot._sync_check_iterations\n\n        elif method.upper() == 'POST':\n            if url == webwx_sync_url:\n                # 同上方设置超时\n                kwargs['timeout'] = webwx_sync_timeout\n\n        return requests.Session.request(session, method, url, **kwargs)\n\n    session.request = customized_request\n\n\ndef repr_message(msg):\n    \"\"\"\n    用于 Message 和 SentMessage 对象的 __repr__ 和 __unicode__\n    \"\"\"\n\n    from wxpy.api.chats import Group\n\n    text = (str(msg.text or '')).replace('\\n', ' ↩ ')\n    text += ' ' if text else ''\n\n    if msg.sender == msg.bot.self:\n        ret = '↪ {self.receiver.name}'\n    elif isinstance(msg.chat, Group) and msg.member != msg.receiver:\n        ret = '{self.sender.name} › {self.member.name}'\n    else:\n        ret = '{self.sender.name}'\n\n    ret += ' : {text}({self.type})'\n\n    return ret.format(self=msg, text=text)\n\n\ndef get_text_without_at_bot(msg):\n    \"\"\"\n    获得 Message 对象中的消息内容，并清理 @ 机器人的部分\n\n    :param msg: Message 对象\n    :return: 清理 @ 机器人部分后的文本内容\n    :rtype: str\n    \"\"\"\n\n    from wxpy.api.chats import Group\n\n    text = msg.text\n\n    if isinstance(msg.chat, Group):\n        name = msg.chat.self.name\n        text = re.sub(r'\\s*@' + re.escape(name) + r'\\u2005?\\s*', '', text)\n\n    return text\n\n\ndef start_new_thread(target, args=(), kwargs=None, daemon=True, use_caller_name=False):\n    \"\"\"\n    启动一个新的进程，需要时自动为进程命名，并返回这个线程\n    \n    :param target: 调用目标\n    :param args: 调用位置参数\n    :param kwargs: 调用命名参数\n    :param daemon: 作为守护进程\n    :param use_caller_name: 为 True 则以调用者为名称，否则以目标为名称\n\n    :return: 新的进程\n    :rtype: threading.Thread\n    \"\"\"\n\n    if use_caller_name:\n        # 使用调用者的名称\n        name = inspect.stack()[1][3]\n    else:\n        name = target.__name__\n\n    logging.getLogger(\n        # 使用外层的 logger\n        inspect.currentframe().f_back.f_globals.get('__name__')\n    ).debug('new thread: {}'.format(name))\n    if PY2:\n        _thread = threading.Thread(\n            target=target, args=args, kwargs=kwargs,\n            name=name)\n        _thread.setDaemon(daemon)\n    else:\n        _thread = threading.Thread(\n            target=target, args=args, kwargs=kwargs,\n            name=name, daemon=daemon\n        )\n    _thread.start()\n\n    return _thread\n"
  },
  {
    "path": "wxpy/utils/puid_map.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport atexit\nimport os\nimport pickle\n\nimport threading\nfrom wxpy.compatible import PY2\nif PY2:\n    from UserDict import UserDict\nelse:\n    from collections import UserDict\n\n\"\"\"\n\n# puid\n\n尝试用聊天对象已知的属性，来查找对应的持久固定并且唯一的 用户 id\n\n\n## 数据结构\n\nPuidMap 中包含 4 个 dict，分别为\n\n1. user_name -> puid\n2. wxid -> puid\n3. remark_name -> puid\n4. caption (昵称, 性别, 省份, 城市) -> puid\n\n\n## 查询逻辑\n\n当给定一个 Chat 对象，需要获取对应的 puid 时，将按顺序，使用自己的对应属性，轮询以上 4 个 dict\n\n* 若匹配任何一个，则获取到 puid，并将其他属性更新到其他的 dict\n* 如果没有一个匹配，则创建一个新的 puid，并加入到以上的 4 个 dict\n\n\n\"\"\"\n\n\nclass PuidMap(object):\n    def __init__(self, path):\n        \"\"\"\n        用于获取聊天对象的 puid (持续有效，并且稳定唯一的用户ID)，和保存映射关系\n\n        :param path: 映射数据的保存/载入路径\n        \"\"\"\n        self.path = path\n\n        self.user_names = TwoWayDict()\n        self.wxids = TwoWayDict()\n        self.remark_names = TwoWayDict()\n\n        self.captions = TwoWayDict()\n\n        self._thread_lock = threading.Lock()\n\n        if os.path.exists(self.path):\n            self.load()\n\n        atexit.register(self.dump)\n\n    @property\n    def attr_dicts(self):\n        return self.user_names, self.wxids, self.remark_names\n\n    def __len__(self):\n        return len(self.user_names)\n\n    def __bool__(self):\n        return bool(self.path)\n\n    def __nonzero__(self):\n        return bool(self.path)\n\n    def get_puid(self, chat):\n        \"\"\"\n        获取指定聊天对象的 puid\n\n        :param chat: 指定的聊天对象\n        :return: puid\n        :rtype: str\n        \"\"\"\n\n        with self._thread_lock:\n\n            if not (chat.user_name and chat.nick_name):\n                return\n\n            chat_attrs = (\n                chat.user_name,\n                chat.wxid,\n                getattr(chat, 'remark_name', None),\n            )\n\n            chat_caption = get_caption(chat)\n\n            puid = None\n\n            for i in range(3):\n                puid = self.attr_dicts[i].get(chat_attrs[i])\n                if puid:\n                    break\n            else:\n                if PY2:\n                    captions = self.captions.keys()\n                else:\n                    captions = self.captions\n                for caption in captions:\n                    if match_captions(caption, chat_caption):\n                        puid = self.captions[caption]\n                        break\n\n            if puid:\n                new_caption = merge_captions(self.captions.get_key(puid), chat_caption)\n            else:\n                puid = chat.user_name[-8:]\n                new_caption = get_caption(chat)\n\n            for i in range(3):\n                chat_attr = chat_attrs[i]\n                if chat_attr:\n                    self.attr_dicts[i][chat_attr] = puid\n\n            self.captions[new_caption] = puid\n\n            return puid\n\n    def dump(self):\n        \"\"\"\n        保存映射数据\n        \"\"\"\n        with open(self.path, 'wb') as fp:\n            pickle.dump((self.user_names, self.wxids, self.remark_names, self.captions), fp)\n\n    def load(self):\n        \"\"\"\n        载入映射数据\n        \"\"\"\n        with open(self.path, 'rb') as fp:\n            self.user_names, self.wxids, self.remark_names, self.captions = pickle.load(fp)\n\n\nclass TwoWayDict(UserDict):\n    \"\"\"\n    可双向查询，且 key, value 均为唯一的 dict\n    限制: key, value 均须为不可变对象，且不支持 .update() 方法\n    \"\"\"\n\n    def __init__(self):\n        if PY2:\n            UserDict.__init__(self)\n        else:\n            super(TwoWayDict, self).__init__()\n        self._reversed = dict()\n\n    def get_key(self, value):\n        \"\"\"\n        通过 value 查找 key\n        \"\"\"\n        return self._reversed.get(value)\n\n    def del_value(self, value):\n        \"\"\"\n        删除 value 及对应的 key\n        \"\"\"\n        del self[self._reversed[value]]\n\n    def __setitem__(self, key, value):\n        if self.get(key) != value:\n            if key in self:\n                self.del_value(self[key])\n            if value in self._reversed:\n                del self[self.get_key(value)]\n            self._reversed[value] = key\n            if PY2:\n                return UserDict.__setitem__(self, key, value)\n            else:\n                return super(TwoWayDict, self).__setitem__(key, value)\n\n    def __delitem__(self, key):\n        del self._reversed[self[key]]\n        if PY2:\n            return UserDict.__delitem__(self, key)\n        else:\n            return super(TwoWayDict, self).__delitem__(key)\n\n    def update(*args, **kwargs):\n        raise NotImplementedError\n\n\ndef get_caption(chat):\n    return (\n        chat.nick_name,\n        getattr(chat, 'sex', None),\n        getattr(chat, 'province', None),\n        getattr(chat, 'city', None),\n    )\n\n\ndef match_captions(old, new):\n    if new[0]:\n        for i in range(4):\n            if old[i] and new[i] and old[i] != new[i]:\n                return False\n        return True\n\n\ndef merge_captions(old, new):\n    return tuple(new[i] or old[i] for i in range(4))\n"
  },
  {
    "path": "wxpy/utils/tools.py",
    "content": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport time\nfrom functools import wraps\n\nfrom wxpy.exceptions import ResponseError\n\nlogger = logging.getLogger(__name__)\n\n\ndef dont_raise_response_error(func):\n    \"\"\"\n    装饰器：用于避免被装饰的函数在运行过程中抛出 ResponseError 错误\n    \"\"\"\n\n    @wraps(func)\n    def wrapped(*args, **kwargs):\n        try:\n            return func(*args, **kwargs)\n        except ResponseError as e:\n            logger.warning('{0.__class__.__name__}: {0}'.format(e))\n\n    return wrapped\n\n\ndef ensure_one(found):\n    \"\"\"\n    确保列表中仅有一个项，并返回这个项，否则抛出 `ValueError` 异常\n\n    通常可用在查找聊天对象时，确保查找结果的唯一性，并直接获取唯一项\n\n    :param found: 列表\n    :return: 唯一项\n    \"\"\"\n    if not isinstance(found, list):\n        raise TypeError('expected list, {} found'.format(type(found)))\n    elif not found:\n        raise ValueError('not found')\n    elif len(found) > 1:\n        raise ValueError('more than one found')\n    else:\n        return found[0]\n\n\ndef mutual_friends(*args):\n    \"\"\"\n    找到多个微信用户的共同好友\n\n    :param args: 每个参数为一个微信用户的机器人(Bot)，或是聊天对象合集(Chats)\n    :return: 共同好友列表\n    :rtype: :class:`wxpy.Chats`\n    \"\"\"\n\n    from wxpy.api.bot import Bot\n    from wxpy.api.chats import Chats, User\n\n    class FuzzyUser(User):\n        def __init__(self, user):\n            super(FuzzyUser, self).__init__(user.raw, user.bot)\n\n        def __hash__(self):\n            return hash((self.nick_name, self.sex, self.province, self.city, self.raw['AttrStatus']))\n\n    mutual = set()\n\n    for arg in args:\n        if isinstance(arg, Bot):\n            friends = map(FuzzyUser, arg.friends())\n        elif isinstance(arg, Chats):\n            friends = map(FuzzyUser, arg)\n        else:\n            raise TypeError\n\n        if mutual:\n            mutual &= set(friends)\n        else:\n            mutual.update(friends)\n\n    return Chats(mutual)\n\n\ndef detect_freq_limit(func, *args, **kwargs):\n    \"\"\"\n    检测各类 Web 微信操作的频率限制，获得限制次数和周期\n    \n    :param func: 需要执行的操作函数\n    :param args: 操作函数的位置参数\n    :param kwargs: 操作函数的命名参数\n    :return: 限制次数, 限制周期(秒数)\n    \"\"\"\n\n    start = time.time()\n    count = 0\n\n    while True:\n        try:\n            func(*args, **kwargs)\n        except ResponseError as e:\n            logger.info('freq limit reached: {} requests passed, error_info: {}'.format(count, e))\n            break\n        else:\n            count += 1\n            logger.debug('{} passed'.format(count))\n\n    while True:\n        period = time.time() - start\n        try:\n            func(*args, **kwargs)\n        except ResponseError:\n            logger.debug('blocking: {:.0f} secs'.format(period))\n            time.sleep(1)\n        else:\n            logger.info('freq limit detected: {} requests / {:.0f} secs'.format(count, period))\n            return count, period\n"
  }
]