Full Code of youfou/wxpy for AI

master ab63e12da822 cached
75 files
151.7 KB
45.3k tokens
296 symbols
1 requests
Download .txt
Repository: youfou/wxpy
Branch: master
Commit: ab63e12da822
Files: 75
Total size: 151.7 KB

Directory structure:
gitextract_ckd9lwhg/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── bot.rst
│   ├── chats.rst
│   ├── conf.py
│   ├── console.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── itchat.rst
│   ├── logging_with_wechat.rst
│   ├── make.bat
│   ├── messages.rst
│   ├── response_error.rst
│   └── utils.rst
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── chats/
│   │   │   ├── __init__.py
│   │   │   ├── test_chat.py
│   │   │   ├── test_chats.py
│   │   │   ├── test_friend.py
│   │   │   ├── test_group.py
│   │   │   ├── test_groups.py
│   │   │   ├── test_member.py
│   │   │   ├── test_mp.py
│   │   │   └── test_user.py
│   │   ├── messages/
│   │   │   ├── __init__.py
│   │   │   └── test_message.py
│   │   └── test_bot.py
│   ├── attachments/
│   │   └── file.txt
│   ├── conftest.py
│   ├── ext/
│   │   └── __init__.py
│   └── utils/
│       └── __init__.py
└── wxpy/
    ├── __compat__.py
    ├── __init__.py
    ├── __main__.py
    ├── api/
    │   ├── __init__.py
    │   ├── bot.py
    │   ├── chats/
    │   │   ├── __init__.py
    │   │   ├── chat.py
    │   │   ├── chats.py
    │   │   ├── friend.py
    │   │   ├── group.py
    │   │   ├── groups.py
    │   │   ├── member.py
    │   │   ├── mp.py
    │   │   └── user.py
    │   ├── consts.py
    │   └── messages/
    │       ├── __init__.py
    │       ├── article.py
    │       ├── message.py
    │       ├── message_config.py
    │       ├── messages.py
    │       ├── registered.py
    │       └── sent_message.py
    ├── compatible/
    │   ├── __init__.py
    │   └── utils.py
    ├── exceptions.py
    ├── ext/
    │   ├── __init__.py
    │   ├── logging_with_wechat.py
    │   ├── sync_message_in_groups.py
    │   ├── talk_bot_utils.py
    │   ├── tuling.py
    │   └── xiaoi.py
    └── utils/
        ├── __init__.py
        ├── base_request.py
        ├── console.py
        ├── misc.py
        ├── puid_map.py
        └── tools.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.idea
*.pkl
_installed_files.txt
_private

### Linux template
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml

# Gradle:
.idea/**/gradle.xml
.idea/**/libraries

# Mongo Explorer plugin:
.idea/**/mongoSettings.xml

## File-based project format:
*.iws

## Plugin-specific files:

# IntelliJ
/out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### macOS template
*.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

wxpy/main.py



================================================
FILE: LICENSE
================================================
**The MIT License**

Copyright 2017 [Youfou](https://github.com/youfou)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include README.rst LICENSE


================================================
FILE: README.rst
================================================
wxpy: 用 Python 玩微信
==============================

.. image:: https://badge.fury.io/py/wxpy.svg
    :target: https://badge.fury.io/py/wxpy

.. image:: https://img.shields.io/pypi/pyversions/wxpy.svg
        :target: https://github.com/youfou/wxpy

.. image:: https://readthedocs.org/projects/wxpy/badge/?version=latest
    :target: http://wxpy.readthedocs.io/zh/latest/?badge=latest

微信机器人 / 可能是最优雅的微信个人号 API
    wxpy 在 itchat 的基础上,通过大量接口优化提升了模块的易用性,并进行丰富的功能扩展


..  attention::

    | **强烈建议仅使用小号运行机器人!**

    | 从近期 (17年6月下旬) 反馈来看,使用机器人存在一定概率被限制登录的可能性。
    | 主要表现为无法登陆 Web 微信 (但不影响手机等其他平台)。



用来干啥
----------------

一些常见的场景

* 控制路由器、智能家居等具有开放接口的玩意儿
* 运行脚本时自动把日志发送到你的微信
* 加群主为好友,自动拉进群中
* 跨号或跨群转发消息
* 自动陪人聊天
* 逗人玩
* ...

总而言之,可用来实现各种微信个人号的自动化操作


..
    体验一下
    ----------------

    **这有一个现成的微信机器人,想不想调戏一下?**

    记得填写入群口令 👉 [ **wxpy** ],与群里的大神们谈笑风生 😏

    ..  image:: https://github.com/youfou/wxpy/raw/master/docs/wechat-group.png


轻松安装
----------------

wxpy 支持 Python 3.4-3.6,以及 2.7 版本

将下方命令中的 "pip" 替换为 "pip3" 或 "pip2",可确保安装到对应的 Python 版本中

1. 从 PYPI 官方源下载安装 (在国内可能比较慢或不稳定):

..  code:: shell

    pip install -U wxpy

2. 从豆瓣 PYPI 镜像源下载安装 (**推荐国内用户选用**):

..  code:: shell

    pip install -U wxpy -i "https://pypi.doubanio.com/simple/"


简单上手
----------------


登陆微信:

..  code:: python

    # 导入模块
    from wxpy import *
    # 初始化机器人,扫码登陆
    bot = Bot()

找到好友:

..  code:: python

    # 搜索名称含有 "游否" 的男性深圳好友
    my_friend = bot.friends().search('游否', sex=MALE, city="深圳")[0]

发送消息:

..  code:: python

    # 发送文本给好友
    my_friend.send('Hello WeChat!')
    # 发送图片
    my_friend.send_image('my_picture.jpg')

自动响应各类消息:

..  code:: python

    # 打印来自其他好友、群聊和公众号的消息
    @bot.register()
    def print_others(msg):
        print(msg)

    # 回复 my_friend 的消息 (优先匹配后注册的函数!)
    @bot.register(my_friend)
    def reply_my_friend(msg):
        return 'received: {} ({})'.format(msg.text, msg.type)

    # 自动接受新的好友请求
    @bot.register(msg_types=FRIENDS)
    def auto_accept_friends(msg):
        # 接受好友请求
        new_friend = msg.card.accept()
        # 向新的好友发送消息
        new_friend.send('哈哈,我自动接受了你的好友请求')

保持登陆/运行:

..  code:: python

    # 进入 Python 命令行、让程序保持运行
    embed()

    # 或者仅仅堵塞线程
    # bot.join()


模块特色
----------------

* 全面对象化接口,调用更优雅
* 默认多线程响应消息,回复更快
* 包含 聊天机器人、共同好友 等 `实用组件 <http://wxpy.readthedocs.io/zh/latest/utils.html>`_
* 只需两行代码,在其他项目中用微信接收警告
* `愉快的探索和调试 <http://wxpy.readthedocs.io/zh/latest/console.html>`_,无需涂涂改改
* 可混合使用 itchat 的原接口
* 当然,还覆盖了各类常见基本功能:

    * 发送文本、图片、视频、文件
    * 通过关键词或用户属性搜索 好友、群聊、群成员等
    * 获取好友/群成员的昵称、备注、性别、地区等信息
    * 加好友,建群,邀请入群,移出群

说明文档
----------------

http://wxpy.readthedocs.io

更新日志
----------------

https://github.com/youfou/wxpy/releases

项目主页
----------------

https://github.com/youfou/wxpy


================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the shell_entry line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
SPHINXPROJ    = wxpy
SOURCEDIR     = .
BUILDDIR      = _build

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

================================================
FILE: docs/bot.rst
================================================
机器人对象
==============================

..  module:: wxpy

机器人 :class:`Bot` 对象可被理解为一个 Web 微信客户端。


..  note::

    | 关于发送消息,请参见 :doc:`chats`。
    | 关于消息对象和自动处理,请参见 :doc:`messages`。


初始化/登陆
----------------

..  note::

    :class:`Bot` 在初始化时便会执行登陆操作,需要手机扫描登陆。

..  autoclass:: Bot

..  automethod:: Bot.enable_puid


..  attribute:: Bot.auto_mark_as_read

    为 True 时,将自动消除手机端的新消息小红点提醒 (默认为 False)


获取聊天对象
----------------

..  attribute:: Bot.self

    机器人自身 (作为一个聊天对象)

    若需要给自己发送消息,请先进行以下一次性操作::

        # 在 Web 微信中把自己加为好友
        bot.self.add()
        bot.self.accept()

        # 发送消息给自己
        bot.self.send('能收到吗?')


..  attribute:: Bot.file_helper

    文件传输助手

..  automethod:: Bot.friends

..  automethod:: Bot.groups

..  automethod:: Bot.mps

..  automethod:: Bot.chats


搜索聊天对象
----------------

..  note::

    * 通过 `.search()` 获得的搜索结果 **均为列表**
    * 若希望找到唯一结果,可使用 :any:`ensure_one()`

搜索好友::

    # 搜索名称包含 '游否' 的深圳男性好友
    found = bot.friends().search('游否', sex=MALE, city='深圳')
    # [<Friend: 游否>]
    # 确保搜索结果是唯一的,并取出唯一结果
    youfou = ensure_one(found)
    # <Friend: 游否>

搜索群聊::

    # 搜索名称包含 'wxpy',且成员中包含 `游否` 的群聊对象
    wxpy_groups = bot.groups().search('wxpy', [youfou])
    # [<Group: wxpy 交流群 1>, <Group: wxpy 交流群 2>]

在群聊中搜素::

    # 在刚刚找到的第一个群中搜索
    group = wxpy_groups[0]
    # 搜索该群中所有浙江的群友
    found = group.search(province='浙江')
    # [<Member: 浙江群友 1>, <Group: 浙江群友 2>, <Group: 浙江群友 3> ...]

搜索任何类型的聊天对象 (但不包含群内成员) ::

    # 搜索名称含有 'wxpy' 的任何聊天对象
    found = bot.search('wxpy')
    # [<Friend: wxpy 机器人>, <Group: wxpy 交流群 1>, <Group: wxpy 交流群 2>]

加好友和建群
----------------

..  automethod:: Bot.add_friend

..  automethod:: Bot.add_mp

..  automethod:: Bot.accept_friend

自动接受好友请求::

    # 注册好友请求类消息
    @bot.register(msg_types=FRIENDS)
    # 自动接受验证信息中包含 'wxpy' 的好友请求
    def auto_accept_friends(msg):
        # 判断好友请求中的验证文本
        if 'wxpy' in msg.text.lower():
            # 接受好友 (msg.card 为该请求的用户对象)
            new_friend = bot.accept_friend(msg.card)
            # 或 new_friend = msg.card.accept()
            # 向新的好友发送消息
            new_friend.send('哈哈,我自动接受了你的好友请求')

..  automethod:: Bot.create_group


其他
----------------

..  automethod:: Bot.user_details

..  automethod:: Bot.upload_file

..  automethod:: Bot.join

..  automethod:: Bot.logout


控制多个微信 (多开)
--------------------------------

仅需初始化多个 :class:`Bot` 对象,即可同时控制多个微信::

    bot1 = Bot()
    bot2 = Bot()



================================================
FILE: docs/chats.rst
================================================
聊天对象
==============================

..  module:: wxpy

通过机器人对象 :class:`Bot <Bot>` 的
:meth:`chats() <Bot.chats>`,
:meth:`friends() <Bot.friends>`,:meth:`groups() <Bot.groups>`,
:meth:`mps() <Bot.mps>` 方法,
可分别获取到当前机器人的 所有聊天对象、好友、群聊,以及公众号列表。

而获得到的聊天对象合集 :class:`Chats` 和 :class:`Groups` 具有一些合集方法,例如::meth:`Chats.search` 可用于按条件搜索聊天对象::

    from wxpy import *
    bot = Bot()
    my_friend = bot.friends().search('游否', sex=MALE, city='深圳')[0]
    # <Friend: 游否>

在找到好友(或其他聊天对象)后,还可使用该聊天对象的 :meth:`send <Chat.send>` 系列方法,对其发送消息::

    # 发送文本
    my_friend.send('Hello, WeChat!')
    # 发送图片
    my_friend.send_image('my_picture.png')
    # 发送视频
    my_friend.send_video('my_video.mov')
    # 发送文件
    my_friend.send_file('my_file.zip')
    # 以动态的方式发送图片
    my_friend.send('@img@my_picture.png')


各类型的继承关系
--------------------------------------

在继续了解各个聊天对象之前,我们需要首先 **理解各种不同类型聊天对象的继承关系**

基础类
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

所有聊天对象,均继承于以下两种基础类,并拥有相应的属性和方法。

基本聊天对象 :class:`Chat`
    * 所有的聊天对象均继承于此类型
    * 拥有 微信ID、昵称 等属性
    * 拥有 发送消息 :meth:`Chat.send`, 获取头像 :meth:`Chat.get_avatar` 等方法

单个聊天对象 :class:`User`
    * 继承于 :class:`Chat`,表示个体聊天对象 (而非群聊)。
    * 被以下聊天对象所继承
        * 好友对象 :class:`Friend`
        * 群成员对象 :class:`Member`
        * 公众号对象 :class:`MP`
    * 拥有 性别、省份、城市、是否为好友 等属性
    * 拥有 加为好友 :meth:`User.add`, 接受为好友 :meth:`User.accept` 等方法

实际类
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

在实际使用过程中,我们会更多的用到以下实际聊天对象类型。

..  tip:: 请牢记,除了自身私有的属性和方法外,它们还拥有对应基础类的属性和方法 (未重复列出)。

* 好友 :class:`Friend`
* 群聊 :class:`Group`
* 群成员 :class:`Member`
* 公众号 :class:`MP`

..  note::

    **阅读以下内容,你将了解:**

        * 如何获取他们的各种属性 (ID、昵称、性别、地区、是否为好友关系等)
        * 如何对他们进行发送消息、加为好友、加入群聊、下载头像 等操作


基本聊天对象
--------------------------------------

所有聊天对象都继承于"基本聊天对象",并拥有相应的属性和方法。

..  autoclass:: Chat
    :members:

    ..  attribute:: bot

        所属的 :class:`机器人对象 <Bot>`

    ..  attribute:: raw

        原始数据



单个聊天对象
--------------------------------------

..  autoclass:: User
    :members:

好友
-------------------

..  autoclass:: Friend
    :members:

群聊
-------------------

..  autoclass:: Group
    :members:


群成员
^^^^^^^^^^^^^^^^^^^^

..  autoclass:: Member
    :members:

实用技巧
^^^^^^^^^^^^^^^^^^^^

判断一位用户是否在群中只需用 `in` 语句::

    friend = bot.friends().search('游否')[0]
    group = bot.groups().search('wxpy 交流群')[0]

    if friend in group:
        print('是的,{} 在 {} 中!'.format(friend.name, group.name))
        # 是的,游否 在 wxpy 交流群 中!

若要遍历群成员,可直接对群对象使用 `for` 语句::

    # 打印所有群成员
    for member in group:
        print(member)

若需查看群成员数量,直接使用 `len()` 即可::

    len(group) # 这个群的成员数量

若需判断一位群成员是否就是某个好友,使用 `==` 即可::

    member = group.search('游否')[0]
    if member == friend:
        print('{} is {}'.format(member, friend))
        # <Member: 游否> is <Friend: 游否>


公众号
-------------------

..  autoclass:: MP
    :members:

聊天对象合集
-------------------

好友、公众号、群聊成员的合集
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

在 :class:`Chats` 对象中,除了最常用到的 :meth:`search() <Chats.search>` 外,还有两个特别的方法,:meth:`stats() <Chats.stats>` 与 :meth:`stats_text() <Chats.stats_text>`,可用来统计好友或群成员的性别和地区分布::

    bot.friends().stats_text()
    # 游否 共有 100 位微信好友\n\n男性: 67 (67.0%)\n女性: 23 (23.0%) ...

..  autoclass:: Chats
    :members:

群聊的合集
^^^^^^^^^^^^^^^^^^^^

..  autoclass:: Groups
    :members:


================================================
FILE: docs/conf.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# wxpy documentation build configuration file, created by
# sphinx-quickstart on Sat Feb 25 23:57:26 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys

sys.path.insert(0, os.path.abspath('..'))

import wxpy

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.coverage',
    'sphinx.ext.viewcode',
]

# For Read the Docs
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if on_rtd:
    html_theme = 'default'
else:
    import sphinx_rtd_theme

    html_theme = 'sphinx_rtd_theme'
    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
    extensions.append('sphinx.ext.githubpages')

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = 'wxpy'
copyright = wxpy.__copyright__
author = wxpy.__author__

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = wxpy.__version__
# The full version, including alpha/beta/rc tags.
release = wxpy.__version__

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the shell_entry line for these cases.
language = 'zh_CN'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False

# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
# html_theme = 'nature'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'wxpydoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'wxpy.tex', 'wxpy Documentation',
     author, 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'wxpy', 'wxpy Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'wxpy', 'wxpy 文档',
     author, 'wxpy', '微信个人号 API,用 Python 玩微信',
     'API'),
]

# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']

autoclass_content = 'both'
autodoc_member_order = 'bysource'

suppress_warnings = ['image.nonlocal_uri']


================================================
FILE: docs/console.rst
================================================
愉快的探索和调试
==============================

..  module:: wxpy

想要做点小试验,调试代码,或是探索 wxpy 的功能特性?反复修改和运行太麻烦。

试试下面两种玩法,告别涂涂改改的摸索方式。


使用 `embed()`
------------------------------

..  note:: 适用于在现有的代码中进行探索和调试

只需将 :any:`embed()` 放在代码中的任何位置。运行后,就可以从那儿开始探索和调试。

例如,初始化一个机器人,然后看看它能做些什么::

    from wxpy import *
    bot = Bot()
    embed() # 进入 Python 命令行

    # 输入对象名称并回车
    >>> bot
    # Out[1]: <Bot: 游否>
    >>> bot.friends()
    # Out[2]: [<Friend: 路人甲>, <Friend: 路人乙>, <Friend: 路人丙>]


..  autofunction:: embed
    :noindex:


使用 `wxpy` 命令
------------------------------

..  highlight:: shell

..  note:: 适用于在命令行中边写边探索

第二种情况:想要简单写几行,而不想创建脚本,那么使用 `wxpy` 命令行边写边探索,更方便。

在命令行中输入 `wxpy -h` 可快速查看使用说明。


选项
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


* bot1 bot2 bot3...
    * 一个或多个需要初始化的机器人对象的名称,以空格分割
    * 默认:不初始化机器人
    * 例子: `bot1 bot2`

* -c / --cache
    * 使用会话缓存功能,将创建 `wxpy_*.pkl` 缓存文件
    * 默认:不缓存会话
    * 例子:`-c`

* -q 宽度 / --console_qr 宽度
    * 终端二维码的单元格宽度
    * 默认:不使用终端二维码
    * 例子:`-q 2`

* -l 等级 / --logging_level 等级 (注意是小写 L,不是 I)
    * 日志等级
    * 默认:`INFO`
    * 例子:`-l DEBUG`

* -s 交互界面 / --shell 交互界面
    * 选择所需使用的 Python 交互界面
    * 可为:`ipython`,`bpython`,`python`,或它们的首字母
    * 默认:以上首个可用的 Python 命令行
    * 例子:`-s bpython`

* -v / --version
    * 展示版本信息并退出z
    * 例子:`-v`


例子
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


初始化一个名为 `bot` 的机器人::

    wxpy bot

在此基础上,使用终端二维码,且单元格宽度为 2::

    wxpy bot -q 2

分别初始化名为 `bot1` 和 `bot2` 的两个机器人::

    wxpy bot1 bot2

在此基础上,使用会话缓存功能::

    wxpy bot1 bot2 -c

在此基础上,指定使用 bpython::

    wxpy bot1 bot2 -c -s bpython



================================================
FILE: docs/faq.rst
================================================
必看: 常见问题 FAQ
==============================

..  module:: wxpy

..  hint::

    这里罗列了一些常见的问题,在提出新的问题前,请先看完本文。


启动后马上退出了?
--------------------------------

因为主线程执行完成了,程序自然会退出。

只需在代码结尾加一句 :any:`embed()` 即可堵塞线程,还能进入 Python 命令行::

    from wxpy import *

    # 你的其他代码...

    # 堵塞线程,并进入 Python 命令行
    embed()

或者,也可以使用 :any:`Bot.join()` 仅仅堵塞线程::

    bot = Bot()

    # 你的其他代码...

    # 仅仅堵塞线程
    bot.join()

    # 机器人登出后会继续往下执行


每次登陆都要扫码?
--------------------------------

可启用登陆状态缓存功能,在短时间内重新运行程序,可自动登录。

具体请见 :any:`Bot` 中的 `cache_path` 参数说明。


可以在 Linux 中使用吗?
----------------------------------------------------------------

wxpy 不依赖于图形界面,因此完全兼容各种纯终端的服务器。

但有一点需要注意,在纯终端环境中,登陆时必须使用"终端二维码"参数。

具体请见 :any:`Bot` 中的 `console_qr` 参数说明。

..  tip::

    遇到以下错误?请使用 :any:`Bot` 的 `console_qr` 参数。 ::

        FileNotFoundError: [Errno 2] No such file or directory: 'xdg-open'


支持 红包、转账、朋友圈… 吗?
--------------------------------

wxpy 使用了 Web 微信的通讯协议,因此仅能覆盖 Web 微信本身所具备的功能。

所以以下功能目前 **均不支持**

* 支付相关 - 红包、转账、收款 等都不支持
* 在群聊中@他人 - 是的,Web 微信中被人@后也不会提醒
* 发送名片 - 但可以通过 :any:`send_raw_msg()` 转发
* 发送分享链接 - 也无法转发
* 发送语音消息
* 朋友圈相关


为什么要开发 wxpy?
--------------------------------

wxpy 的初衷是帮助人们利用微信来使生活和工作更轻松。

..  note::

    希望每位使用者在使用机器人时

    * 维护良好的交流环境
    * 永远不骚扰他人
    * 遵守法律和平台规则


================================================
FILE: docs/index.rst
================================================
.. wxpy documentation master file, created by
   sphinx-quickstart on Sat Feb 25 23:57:26 2017.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.


wxpy: 用 Python 玩微信
==============================

.. image:: https://badge.fury.io/py/wxpy.svg
    :target: https://pypi.python.org/pypi/wxpy/

.. image:: https://img.shields.io/pypi/pyversions/wxpy.svg
        :target: https://github.com/youfou/wxpy

.. image:: https://readthedocs.org/projects/wxpy/badge/?version=latest
    :target: http://wxpy.readthedocs.io/zh/latest/?badge=latest

微信机器人 / 可能是最优雅的微信个人号 API
    wxpy 在 itchat 的基础上,通过大量接口优化提升了模块的易用性,并进行丰富的功能扩展


..  attention::

    | **强烈建议仅使用小号运行机器人!**

    | 从近期 (17年6月下旬) 反馈来看,使用机器人存在一定概率被限制登录的可能性。
    | 主要表现为无法登陆 Web 微信 (但不影响手机等其他平台)。


项目主页
----------------

https://github.com/youfou/wxpy


用来干啥
----------------

一些常见的场景

* 控制路由器、智能家居等具有开放接口的玩意儿
* 运行脚本时自动把日志发送到你的微信
* 加群主为好友,自动拉进群中
* 跨号或跨群转发消息
* 自动陪人聊天
* 逗人玩
* ...

总而言之,可用来实现各种微信个人号的自动化操作

..
    体验一下
    ----------------

    **这有一个现成的微信机器人,想不想调戏一下?**

    记得填写入群口令 👉 [ **wxpy** ],与群里的大神们谈笑风生 😏

    ..  image:: wechat-group.png


轻松安装
----------------

wxpy 支持 Python 3.4-3.6,以及 2.7 版本

将下方命令中的 "pip" 替换为 "pip3" 或 "pip2",可确保安装到对应的 Python 版本中

1. 从 PYPI 官方源下载安装 (在国内可能比较慢或不稳定):

..  code:: shell

    pip install -U wxpy

2. 从豆瓣 PYPI 镜像源下载安装 (**推荐国内用户选用**):

..  code:: shell

    pip install -U wxpy -i "https://pypi.doubanio.com/simple/"

简单上手
----------------

..  automodule:: wxpy


模块特色
----------------

* 全面对象化接口,调用更优雅
* 默认多线程响应消息,回复更快
* 包含 聊天机器人、共同好友 等 :doc:`实用组件 <utils>`
* 只需两行代码,在其他项目中 :doc:`用微信接收警告 <logging_with_wechat>`
* :doc:`愉快的探索和调试 <console>`,无需涂涂改改
* 可混合使用 itchat 的原接口
* 当然,还覆盖了各类常见基本功能:

    * 发送文本、图片、视频、文件
    * 通过关键词或用户属性搜索 好友、群聊、群成员等
    * 获取好友/群成员的昵称、备注、性别、地区等信息
    * 加好友,建群,邀请入群,移出群

更新日志
----------------

https://github.com/youfou/wxpy/releases


文档目录
----------------

..  toctree::
    :maxdepth: 2

    bot
    chats
    messages
    logging_with_wechat
    console
    utils
    response_error
    itchat
    faq



================================================
FILE: docs/itchat.rst
================================================
itchat 与原始数据
==============================

..  module:: wxpy


正是得益于 |itchat| 的坚实基础,wxpy 才能够在短时间内快速实现这些新的接口和功能。

感谢 itchat 维护者们的辛勤付出。

以下为如何在 wxpy 中混合使用 itchat 的原接口和原始数据。


..  |itchat| raw:: html

    <a href="https://github.com/littlecodersh/itchat" target="_blank">itchat</a>


使用 itchat 的原接口
------------------------------

只需在 wxpy 的 :class:`Bot` 对象后紧跟 `.core.*` 即可调用 itchat 的原接口。

例如,使用 itchat 的 `search_friends` 接口::

    from wxpy import *
    bot = Bot()
    found = bot.core.search_friends('游否')

..  attention:: 通过 itchat 原接口所获取到的结果为原始数据,可能无法直接传递到 wxpy 的对应方法中。


使用原始数据
------------------------------

wxpy 的所有 **聊天对象** 和 **消息对象** 均基于从 itchat 获取到的数据进行封装。若需使用原始数据,只需在对象后紧跟 `.raw`。

例如,查看一个 :class:`好友 <Friend>` 对象的原始数据::

    from wxpy import *
    bot = Bot()
    a_friend = bot.friends()[0]
    print(a_friend.raw)



================================================
FILE: docs/logging_with_wechat.rst
================================================
用微信监控你的程序
==============================

..  module:: wxpy

通过利用微信强大的通知能力,我们可以把程序中的警告/日志发到自己的微信上。

wxpy 提供以下两种方式来实现这个需求。


获得专用 Logger
------------------------------

.. autofunction:: get_wechat_logger

::

    from wxpy import get_wechat_logger

    # 获得一个专用 Logger
    # 当不设置 `receiver` 时,会将日志发送到随后扫码登陆的微信的"文件传输助手"
    logger = get_wechat_logger()

    # 发送警告
    logger.warning('这是一条 WARNING 等级的日志,你收到了吗?')

    # 接收捕获的异常
    try:
        1 / 0
    except:
        logger.exception('现在你又收到了什么?')


加入到现有的 Logger
------------------------------

.. autoclass:: WeChatLoggingHandler

::

    import logging
    from wxpy import WeChatLoggingHandler

    # 这是你现有的 Logger
    logger = logging.getLogger(__name__)

    # 初始化一个微信 Handler
    wechat_handler = WeChatLoggingHandler()
    # 加到入现有的 Logger
    logger.addHandler(wechat_handler)

    logger.warning('你有一条新的告警,请查收。')


指定接收者
------------------------------

当然,我们也可以使用其他聊天对象来接收日志。

比如,先在微信中建立一个群聊,并在里面加入需要关注这些日志的人员。然后把这个群作为接收者。

::

    from wxpy import *

    # 初始化机器人
    bot = Bot()
    # 找到需要接收日志的群 -- `ensure_one()` 用于确保找到的结果是唯一的,避免发错地方
    group_receiver = ensure_one(bot.groups().search('XX业务-告警通知'))

    # 指定这个群为接收者
    logger = get_wechat_logger(group_receiver)

    logger.error('打扰大家了,但这是一条重要的错误日志...')



================================================
FILE: docs/make.bat
================================================
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
	set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=wxpy

if "%1" == "" goto help

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
	echo.
	echo.The 'sphinx-build' shell_entry was not found. Make sure you have Sphinx
	echo.installed, then set the SPHINXBUILD environment variable to point
	echo.to the full path of the 'sphinx-build' executable. Alternatively you
	echo.may add the Sphinx directory to PATH.
	echo.
	echo.If you don't have Sphinx installed, grab it from
	echo.http://sphinx-doc.org/
	exit /b 1
)

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%

:end
popd


================================================
FILE: docs/messages.rst
================================================
消息处理
==============================

..  module:: wxpy

每当机器人接收到消息时,会自动执行以下两个步骤

1. 将消息保存到 :class:`Bot.messages` 中
2. 查找消息预先注册的函数,并执行(若有匹配的函数)


消息对象
----------------

消息对象代表每一条从微信获取到的消息。


基本属性
^^^^^^^^^^^^^^^^

..  autoattribute:: Message.type

..  attribute:: Message.bot

    接收此消息的 :class:`机器人对象 <Bot>`

..  autoattribute:: Message.id


内容数据
^^^^^^^^^^^^^^^^

..  autoattribute:: Message.text

..  automethod:: Message.get_file

..  autoattribute:: Message.file_name

..  autoattribute:: Message.file_size

..  autoattribute:: Message.media_id

..  attribute:: Message.raw

    原始数据 (dict 数据)

用户相关
^^^^^^^^^^^^^^^^


..  autoattribute:: Message.chat

..  autoattribute:: Message.sender

..  autoattribute:: Message.receiver

..  autoattribute:: Message.member

..  autoattribute:: Message.card


群聊相关
^^^^^^^^^^^^^^^^

..  autoattribute:: Message.member

..  autoattribute:: Message.is_at

时间相关
^^^^^^^^^^^^^^^^^

..  autoattribute:: Message.create_time

..  autoattribute:: Message.receive_time

..  autoattribute:: Message.latency


其他属性
^^^^^^^^^^^^^^^^

..  autoattribute:: Message.url

..  autoattribute:: Message.articles

..  autoattribute:: Message.location

..  autoattribute:: Message.img_height

..  autoattribute:: Message.img_width

..  autoattribute:: Message.play_length

..  autoattribute:: Message.voice_length


回复方法
^^^^^^^^^^^^^^^^

..  method:: Message.reply(...)

    等同于 :meth:`Message.chat.send(...) <Chat.send>`

..  method:: Message.reply_image(...)

    等同于 :meth:`Message.chat.send_image(...) <Chat.send_image>`

..  method:: Message.reply_file(...)

    等同于 :meth:`Message.chat.send_file(...) <Chat.send_file>`

..  method:: Message.reply_video(...)

    等同于 :meth:`Message.chat.send_video(...) <Chat.send_video>`

..  method:: Message.reply_msg(...)

    等同于 :meth:`Message.chat.send_msg(...) <Chat.send_msg>`

..  method:: Message.reply_raw_msg(...)

    等同于 :meth:`Message.chat.send_raw_msg(...) <Chat.send_raw_msg>`


转发消息
^^^^^^^^^^^^^^^^

..  automethod:: Message.forward


自动处理消息
---------------------

可通过 **预先注册** 的方式,实现消息的自动处理。


"预先注册" 是指
    预先将特定聊天对象的特定类型消息,注册到对应的处理函数,以实现自动回复等功能。


注册消息
^^^^^^^^^^^^^^

..  hint::

    | 每当收到新消息时,将根据注册规则找到匹配条件的执行函数。
    | 并将 :class:`消息对象 <Message>` 作为唯一参数传入该函数。

将 :meth:`Bot.register` 作为函数的装饰器,即可完成注册。

::

    # 打印所有*群聊*对象中的*文本*消息
    @bot.register(Group, TEXT)
    def print_group_msg(msg):
        print(msg)


..  attention:: 优先匹配 **后注册** 的函数,且仅匹配 **一个** 注册函数。

..  automethod:: Bot.register

..  tip::

    1.  `chats` 和 `msg_types` 参数可以接收一个列表或干脆一个单项。按需使用,方便灵活。
    2.  `chats` 参数既可以是聊天对象实例,也可以是对象类。当为类时,表示匹配该类型的所有聊天对象。
    3. 在被注册函数中,可以通过直接 `return <回复内容>` 的方式来回复消息,等同于调用 `msg.reply(<回复内容>)`。


开始运行
^^^^^^^^^^^^^^

..  note::

    | 在完成注册操作后,若没有其他操作,程序会因主线程执行完成而退出。
    | **因此务必堵塞线程以保持监听状态!**
    | wxpy 的 :any:`embed()` 可在堵塞线程的同时,进入 Python 命令行,方便调试,一举两得。


::

    from wxpy import *

    bot = Bot()

    @bot.register()
    def print_messages(msg):
        print(msg)

    # 堵塞线程,并进入 Python 命令行
    embed()


..  autofunction:: embed


示例代码
^^^^^^^^^^^^^

在以下例子中,机器人将

* 忽略 "一个无聊的群" 的所有消息
* 回复好友 "游否" 和其他群聊中被 @ 的 TEXT 类消息
* 打印所有其他消息

初始化机器人,并找到好友和群聊::

    from wxpy import *
    bot = Bot()
    my_friend = bot.friends().search('游否')[0]
    boring_group = bot.groups().search('一个无聊的群')[0]


打印所有其他消息::

    @bot.register()
    def just_print(msg):
        # 打印消息
        print(msg)


回复好友"游否"和其他群聊中被 @ 的 TEXT 类消息::

    @bot.register([my_friend, Group], TEXT)
    def auto_reply(msg):
        # 如果是群聊,但没有被 @,则不回复
        if isinstance(msg.chat, Group) and not msg.is_at:
            return
        else:
            # 回复消息内容和类型
            return '收到消息: {} ({})'.format(msg.text, msg.type)


忽略"一个无聊的群"的所有消息::

    @bot.register(boring_group)
    def ignore(msg):
        # 啥也不做
        return


堵塞线程,并进入 Python 命令行::

    embed()


动态开关注册配置
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

..  note:: 该操作需要在额外的线程中进行!


查看当前的注册配置情况::

    bot.registered
    # [<MessageConfig: just_print (Async, Enabled)>,
    #  <MessageConfig: auto_reply (Async, Enabled)>,
    #  <MessageConfig: ignore (Async, Enabled)>]


关闭所有注册配置::

    bot.registered.disable()

重新开启 `just_print` 函数::

    bot.registered.enable(just_print)


查看当前开启的注册配置::

    bot.registered.enabled
    # [<MessageConfig: just_print (Async, Enabled)>]


..  autoclass:: wxpy.api.messages.Registered
    :members:

已发送消息
----------------

..  autoclass:: SentMessage

    ..  hint:: 大部分属性与 :class:`Message` 相同

    ..  automethod:: recall

历史消息
----------------

可通过访问 `bot.messages` 来查看历史消息列表。

消息列表为 :class:`Messages` 对象,具有搜索功能。

例如,搜索所有自己在手机上发出的消息::

    sent_msgs = bot.messages.search(sender=bot.self)
    print(sent_msgs)


..  autoclass:: Messages

    ..  attribute:: max_history

        设置最大保存条数,即:仅保存最后的 n 条消息。

        ::

            bot = Bot()
            # 设置历史消息的最大保存数量为 10000 条
            bot.messages.max_history = 10000

    ..  automethod:: search

        ::

            # 搜索所有自己发送的,文本中包含 'wxpy' 的消息
            bot.messages.search('wxpy', sender=bot.self)



================================================
FILE: docs/response_error.rst
================================================
异常处理
==============================


异常的抛出和捕捉
--------------------


..  module:: wxpy

每当使用 wxpy 向微信发出请求 (例如发送消息、加好友、建群等操作),wxpy 都会在收到服务端响应后进行检查。

若响应中的错误码不为 0,程序将抛出 :class:`ResponseError` 异常。

..  autoclass:: ResponseError

    ..  attribute:: err_code

        错误码 (int)

    ..  attribute:: err_msg

        错误消息 (文本),但可能为空

捕捉异常::

    try:
        # 尝试向某个群员发送消息
        group.members[3].send('Hello')
    except ResponseError as e:
        # 若群员还不是好友,将抛出 ResponseError 错误
        print(e.err_code, e.err_msg) # 查看错误号和错误消息


已知错误码
--------------------

通常来说,每个错误码表示一种类型的错误。

但因微信未公开 (也没有义务公开) 这套错误码体系的具体说明,我们只能根据经验猜测部分错误码的定义。

以下为一些常见的已知错误码。欢迎提交 PR `进行完善`_。

    .. _进行完善: https://github.com/youfou/wxpy/blob/master/docs/response_error.rst


1205
^^^^^^^^^^^^^^^^^^^^^^^

通常因为操作频率过高。需要控制频率,避免再次引起该错误。

..  attention:: Web 微信对 **加好友、建群** 这两种操作的频率限制尤其严格!

对于微信而言,为了机器人避免打扰其他用户,以及控制服务器的负载压力,需要对各种不同的操作进行频率限制。

通常每种操作可有多层频率限制,而每层频率限制分为两个参数:

    周期、次数,分布表示: 在 x 周期内,只能发送 y 个请求。

举个例子:

    对于 **发送消息** 操作,可能会是这样 (数值为虚构):

        +----+----------+----------+
        | 层 | 限制周期 | 限制次数 |
        +----+----------+----------+
        | 1  | 2 分钟   | 120      |
        +----+----------+----------+
        | 2  | 10 分钟  |  300     |
        +----+----------+----------+
        | 3  | 1 小时   |  1000    |
        +----+----------+----------+
        | 4  | 24 小时  | 2000     |
        +----+----------+----------+

        | 可能会有用户在 1 分钟内狂发 100 条消息。
        | 但这样的频率不可能维持一整天,所以一天内 3000 条是足够的。

    通过以上方式,微信可实现较为合理的限制。


1204
^^^^^^^^^^^^^^^^^^^^^^^

通常因为操作对象不为好友关系。例如尝试向一位不为好友的群员发送消息时,会引起这个错误。

1100, 1101, 1102
^^^^^^^^^^^^^^^^^^^^^^^

通常表示机器人已经掉线,需要重新登录。

请重新初始化 :class:`Bot` 对象,并重新注册消息。

    因为重新登录后,聊天对象的 `user_name` 可能已经变化,所以原先的消息注册也会因此失效。


================================================
FILE: docs/utils.rst
================================================
实用组件
==============================

..  module:: wxpy

额外内置了一些实用的小组件,可按需使用。


聊天机器人
------------------------------

目前提供了以下两种自动聊天机器人接口。


图灵
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

..  autoclass:: Tuling
    :members:

    ::

        bot = Bot()
        my_friend = ensure_one(bot.search('游否'))
        tuling = Tuling(api_key='你申请的 API KEY')

        # 使用图灵机器人自动与指定好友聊天
        @bot.register(my_friend)
        def reply_my_friend(msg):
            tuling.do_reply(msg)


小 i
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

..  autoclass:: XiaoI
    :members:

    ::

        bot = Bot()
        my_friend = ensure_one(bot.search('寒风'))
        xiaoi = XiaoI('你申请的 Key', '你申请的 Secret')

        # 使用小 i 机器人自动与指定好友聊天
        @bot.register(my_friend)
        def reply_my_friend(msg):
            xiaoi.do_reply(msg)


查找共同好友
------------------------------

..  autofunction:: mutual_friends

    ::

        bot1 = Bot()
        bot2 = Bot()

        # 打印共同好友
        for mf in mutual_friends(bot, bot2):
            print(mf)


确保查找结果的唯一性
------------------------------

..  autofunction:: ensure_one

    ::

        bot = Bot()
        # 确保只找到了一个叫"游否"的好友,并返回这个好友
        my_friend = ensure_one(bot.search('游否'))
        # <Friend: 游否>


在多个群中同步消息
------------------------------

..  autofunction:: sync_message_in_groups


检测频率限制
------------------------------

..  autofunction:: detect_freq_limit

    例如,测试发送文本消息的频率限制::

        bot = Bot('test.pkl')

        # 定义需要检测的操作
        def action():
            bot.file_helper.send()

        # 执行检测
        result = detect_freq_limit(action)
        # 查看结果
        print(result)
        # (120, 120.111222333)


忽略 `ResponseError` 异常
------------------------------

..  autofunction:: dont_raise_response_error



================================================
FILE: requirements.txt
================================================
itchat==1.2.32
requests
future

================================================
FILE: setup.cfg
================================================
[aliases]
release=sdist build egg_info upload


================================================
FILE: setup.py
================================================
# coding: utf-8
# from __future__ import unicode_literals

import re
import codecs

from setuptools import find_packages, setup

with codecs.open('wxpy/__init__.py', encoding='utf-8') as fp:
    version = re.search(r"__version__\s*=\s*'([\w\-.]+)'", fp.read()).group(1)

with codecs.open('README.rst', encoding='utf-8') as fp:
    readme = fp.read()

setup(
    name='wxpy',
    version=version,
    packages=find_packages(),
    include_package_data=True,
    entry_points={
        'console_scripts': [
            'wxpy = wxpy.utils:shell_entry'
        ]
    },
    install_requires=[
        'itchat==1.2.32',
        'requests',
        'future',
    ],
    tests_require=[
        'pytest',
    ],
    url='https://github.com/youfou/wxpy',
    license='MIT',
    author='Youfou',
    author_email='youfou@qq.com',
    description='微信机器人 / 可能是最优雅的微信个人号 API',
    long_description=readme,
    keywords=[
        '微信',
        'WeChat',
        'API'
    ],
    classifiers=[
        'Development Status :: 4 - Beta',
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Operating System :: OS Independent',
        'Topic :: Communications :: Chat',
        'Topic :: Utilities',
    ]
)


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/api/__init__.py
================================================


================================================
FILE: tests/api/chats/__init__.py
================================================


================================================
FILE: tests/api/chats/test_chat.py
================================================
class TestChat:
    def test_pin_unpin(self, friend):
        friend.pin()
        friend.unpin()


================================================
FILE: tests/api/chats/test_chats.py
================================================
from collections import Counter

from wxpy import *


class TestChats:
    def test_search(self, group, friend):
        found = group.search('wxpy 机器人')
        assert friend in found
        assert isinstance(found, Chats)

    def test_stats(self, group):
        stats = group.members.stats()
        assert isinstance(stats, dict)
        for attr in 'province', 'city', 'sex':
            assert attr in stats
            assert isinstance(stats[attr], Counter)

    def test_stats_text(self, group):
        text = group.members.stats_text()
        assert '位群成员' in text


================================================
FILE: tests/api/chats/test_friend.py
================================================
class TestFriend:
    pass

================================================
FILE: tests/api/chats/test_group.py
================================================
import pytest

from wxpy import *


class TestGroup:
    def test_group_attributes(self, group, friend, member):
        isinstance(group.members, Chats)
        assert friend in group
        assert member in group
        assert group.self == group.bot.self
        assert group.self in group
        assert not group.is_owner
        assert group.owner == friend

    def test_update_group(self, group):
        group.update_group(members_details=True)
        assert group.members[-1].sex is not None

    def test_add_members(self, group, member):
        try:
            group.add_members(member)
        except ResponseError as e:
            if e.err_code != 1205:
                raise e

    def test_remove_members(self, member):
        with pytest.raises(ResponseError) as e:
            member.remove()
            assert e.err_code == -66


================================================
FILE: tests/api/chats/test_groups.py
================================================
class TestGroups:
    def test_search(self, bot, group, member, friend):
        found = bot.groups().search(group.name, users=[bot.self, member, friend])
        assert group in found


================================================
FILE: tests/api/chats/test_member.py
================================================


================================================
FILE: tests/api/chats/test_mp.py
================================================
class TestMP:
    pass


================================================
FILE: tests/api/chats/test_user.py
================================================
import re

from wxpy import *


class TestUser:
    def test_friend_attributes(self, friend):
        assert isinstance(friend, Friend)
        assert friend.nick_name == 'wxpy 机器人'
        assert friend.wxid in ('wxpy_bot', None)
        assert friend.province == '广东'
        assert friend.city == '深圳'
        assert friend.sex == MALE
        assert friend.signature == '如果没有正确响应,可能正在调试中…'
        assert re.match(r'@[\da-f]{32,}', friend.user_name)
        assert friend.is_friend

    # def test_add(self, member):
    #     member.add('wxpy tests: test_add')

    def test_accept(self, member):
        # 似乎只要曾经是好友,就可以调用这个方法,达到"找回已删除的好友"的效果
        member.accept()

    def test_remark_name(self, friend, member):
        new_remark_name = '__test__123__'

        for user in friend, member:
            current_remark_name = user.remark_name or ''
            for remark_name in new_remark_name, current_remark_name:
                user.set_remark_name(remark_name)


================================================
FILE: tests/api/messages/__init__.py
================================================


================================================
FILE: tests/api/messages/test_message.py
================================================
from datetime import datetime

from tests.conftest import wait_for_message
from wxpy import *


def sent_message(sent_msg, msg_type, receiver):
    assert isinstance(sent_msg, SentMessage)
    assert sent_msg.type == msg_type
    assert sent_msg.receiver == receiver
    assert sent_msg.bot == receiver.bot
    assert sent_msg.sender == receiver.bot.self
    assert isinstance(sent_msg.receive_time, datetime)
    assert isinstance(sent_msg.create_time, datetime)
    assert sent_msg.create_time < sent_msg.receive_time


class TestMessage:
    def test_text_message(self, group, friend):
        sent_message(group.send('text'), TEXT, group)
        msg = wait_for_message(group, TEXT)
        assert isinstance(msg, Message)
        assert msg.type == TEXT
        assert msg.text == 'Hello!'
        assert not msg.is_at
        assert msg.chat == group
        assert msg.sender == group
        assert msg.receiver == group.self
        assert msg.member == friend
        assert 0 < msg.latency < 30

        group.send('at')
        msg = wait_for_message(group, TEXT)
        assert msg.is_at

    def test_picture_message(self, group, image_path):
        sent = group.send_image(image_path)
        sent_message(sent, PICTURE, group)
        assert sent.path == image_path

    def test_video_message(self, group, video_path):
        sent = group.send_video(video_path)
        sent_message(sent, VIDEO, group)
        assert sent.path == video_path

    def test_raw_message(self, group):
        # 发送名片
        raw_type = 42
        raw_content = '<msg username="{}" nickname="{}"/>'.format('wxpy_bot', 'wxpy 机器人')
        sent_message(group.send_raw_msg(raw_type, raw_content), None, group)

    def test_send(self, friend, file_path, image_path, video_path):
        text_to_send = 'test sending text'
        sent = friend.send(text_to_send)
        sent_message(sent, TEXT, friend)
        assert sent.text == text_to_send

        sent = friend.send('@fil@{}'.format(file_path))
        sent_message(sent, ATTACHMENT, friend)
        assert sent.path == file_path

        sent = friend.send('@img@{}'.format(image_path))
        sent_message(sent, PICTURE, friend)
        assert sent.path == image_path

        sent = friend.send('@vid@{}'.format(video_path))
        sent_message(sent, VIDEO, friend)
        assert sent.path == video_path

        # 发送名片
        raw_type = 42
        raw_content = '<msg username="{}" nickname="{}"/>'.format('wxpy_bot', 'wxpy 机器人')
        uri = '/webwxsendmsg'
        sent = friend.send_raw_msg(raw_type, raw_content)
        sent_message(sent, None, friend)

        assert sent.type is None
        assert sent.raw_type == raw_type
        assert sent.raw_content == raw_content
        assert sent.uri == uri


================================================
FILE: tests/api/test_bot.py
================================================
import logging
import os
import time

from wxpy import *


class TestBot:
    def test_self(self, bot):
        assert bot.self.name is not None
        assert bot.self.name == bot.core.loginInfo['User']['NickName']

    def test_repr(self, bot):
        assert repr(bot) == '<Bot: {}>'.format(bot.self.name)

    def test_alive(self, bot):
        assert bot.alive

    def test_dump_login_status(self, bot):
        bot.dump_login_status()
        updated_at = os.path.getmtime(bot.cache_path)
        assert time.time() - updated_at < 1

    def test_enable_puid(self, bot, base_dir):
        from wxpy.utils.puid_map import PuidMap
        puid_path = os.path.join(base_dir, 'wxpy_bot_puid.pkl')
        puid_map = bot.enable_puid(puid_path)
        assert isinstance(puid_map, PuidMap)

    def test_chats(self, bot):
        chats = bot.chats()
        assert isinstance(chats, Chats)
        assert set(chats) == set(bot.friends() + bot.groups() + bot.mps())

    def test_friends(self, bot):
        friends = bot.friends()
        assert isinstance(friends, Chats)
        assert bot.self in friends
        for friend in friends:
            assert isinstance(friend, Friend)

    def test_groups(self, bot):
        groups = bot.groups()
        assert isinstance(groups, Groups)
        for group in groups:
            assert isinstance(group, Group)
            assert bot.self in group

    def test_mps(self, bot):
        mps = bot.mps()
        assert isinstance(mps, Chats)
        for mp in mps:
            assert isinstance(mp, MP)

    def test_search(self, bot):
        found_1 = bot.search(bot.self.name, sex=bot.self.sex or None)
        assert bot.self in found_1
        found_2 = bot.search(nick_name='__!#@$#%$__')
        assert not found_2

        for found in found_1, found_2:
            assert isinstance(found, Chats)
            assert found.source == bot

    def test_create_group(self, bot):
        users = bot.friends()[:3]
        topic = 'test creating group'
        try:
            new_group = bot.create_group(users, topic)
        except ResponseError as e:
            logging.warning('Failed to create group: {}'.format(e))
        except Exception as e:
            if 'Failed to create group:' in str(e):
                logging.warning(e)
            else:
                raise e
        else:
            assert new_group.name == topic
            assert new_group in bot.groups()
            assert set(users) == set(new_group.members)

            new_name = 'testing'
            new_group.rename_group(new_name)
            assert new_group.name == new_name

    def test_upload_file(self, bot, file_path, friend):
        media_id = bot.upload_file(file_path)
        friend.send_file(file_path, media_id=media_id)


================================================
FILE: tests/attachments/file.txt
================================================
Hello from wxpy!


================================================
FILE: tests/conftest.py
================================================
# coding: utf-8

"""
部分用例需要与 "wxpy 机器人" 进行互动
"""

import os
import time
from functools import partial
from queue import Queue

import pytest

from wxpy import *

_base_dir = os.path.dirname(os.path.realpath(__file__))

print('logging in test bot...')
_bot = Bot(os.path.join(_base_dir, 'wxpy_bot.pkl'))
_friend = ensure_one(_bot.friends().search('wxpy 机器人'))
_group = ensure_one(_bot.groups().search('wxpy test'))
_member = ensure_one(_group.search('游否'))

_shared_dict = dict()

attachments_dir = os.path.join(_base_dir, 'attachments')
gen_attachment_path = partial(os.path.join, attachments_dir)

global_use = partial(pytest.fixture, scope='session', autouse=True)


@global_use()
def base_dir():
    return _base_dir


@global_use()
def bot():
    return _bot


@global_use()
def friend():
    yield _friend
    while True:
        try:
            _friend.set_remark_name('')
        except ResponseError as e:
            if e.err_code == 1205:
                time.sleep(10)
                continue
        else:
            break


@global_use()
def group():
    return _group


@global_use()
def shared_dict():
    return _shared_dict


@global_use()
def member():
    return _member


@global_use()
def image_path():
    return gen_attachment_path('image.png')


@global_use()
def file_path():
    return gen_attachment_path('file.txt')


@global_use()
def video_path():
    return gen_attachment_path('video.mp4')


def wait_for_message(chats=None, msg_types=None, except_self=True, timeout=30):
    """
    等待一条指定的消息,并返回这条消息

    :param chats: 所需等待消息所在的聊天会话
    :param msg_types: 所需等待的消息类型
    :param except_self: 是否排除自己发送的消息
    :param timeout: 等待的超时秒数,若为 None 则一直等待,直到收到所需的消息
    :return: 若在超时内等到了消息,则返回此消息,否则抛出 `queue.Empty` 异常
    """

    received = Queue()

    @_bot.register(chats=chats, msg_types=msg_types, except_self=except_self)
    def _func(msg):
        received.put(msg)

    _config = _bot.registered.get_config_by_func(_func)

    ret = received.get(timeout=timeout)

    _bot.registered.remove(_config)

    return ret


================================================
FILE: tests/ext/__init__.py
================================================


================================================
FILE: tests/utils/__init__.py
================================================


================================================
FILE: wxpy/__compat__.py
================================================


================================================
FILE: wxpy/__init__.py
================================================
#!/usr/bin/env python3
# coding: utf-8

"""


登陆微信::

    # 导入模块
    from wxpy import *
    # 初始化机器人,扫码登陆
    bot = Bot()

找到好友::

    # 搜索名称含有 "游否" 的男性深圳好友
    my_friend = bot.friends().search('游否', sex=MALE, city="深圳")[0]

发送消息::

    # 发送文本给好友
    my_friend.send('Hello WeChat!')
    # 发送图片
    my_friend.send_image('my_picture.jpg')

自动响应各类消息::

    # 打印来自其他好友、群聊和公众号的消息
    @bot.register()
    def print_others(msg):
        print(msg)

    # 回复 my_friend 的消息 (优先匹配后注册的函数!)
    @bot.register(my_friend)
    def reply_my_friend(msg):
        return 'received: {} ({})'.format(msg.text, msg.type)

    # 自动接受新的好友请求
    @bot.register(msg_types=FRIENDS)
    def auto_accept_friends(msg):
        # 接受好友请求
        new_friend = msg.card.accept()
        # 向新的好友发送消息
        new_friend.send('哈哈,我自动接受了你的好友请求')

保持登陆/运行::

    # 进入 Python 命令行、让程序保持运行
    embed()

    # 或者仅仅堵塞线程
    # bot.join()


"""

import logging
import sys

from .api.bot import Bot
from .api.chats import Chat, Chats, Friend, Group, Groups, MP, Member, User
from .api.consts import ATTACHMENT, CARD, FRIENDS, MAP, NOTE, PICTURE, RECORDING, SHARING, SYSTEM, TEXT, VIDEO
from .api.consts import FEMALE, MALE
from .api.messages import Article, Message, Messages, SentMessage
from .exceptions import ResponseError
from .ext import Tuling, WeChatLoggingHandler, XiaoI, get_wechat_logger, sync_message_in_groups
from .utils import BaseRequest, detect_freq_limit, dont_raise_response_error, embed, ensure_one, mutual_friends

__title__ = 'wxpy'
__version__ = '0.3.9.8'
__author__ = 'Youfou'
__license__ = 'MIT'
__copyright__ = '2017, Youfou'

version_details = 'wxpy {ver} from {path} (python {pv.major}.{pv.minor}.{pv.micro})'.format(
    ver=__version__, path=__path__[0], pv=sys.version_info)

try:
    # Python 2.7+
    from logging import NullHandler
except ImportError:
    class NullHandler(logging.Handler):
        def emit(self, record):
            pass

logging.getLogger(__name__).addHandler(NullHandler())


================================================
FILE: wxpy/__main__.py
================================================
# coding: utf-8

from .utils import shell_entry

if __name__ == '__main__':
    shell_entry()


================================================
FILE: wxpy/api/__init__.py
================================================


================================================
FILE: wxpy/api/bot.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import atexit
import functools
import logging
import os.path
import tempfile
import time
from pprint import pformat
from threading import Thread

try:
    import queue
except ImportError:
    # noinspection PyUnresolvedReferences,PyPep8Naming
    import Queue as queue

import itchat

from ..api.chats import Chat, Chats, Friend, Group, MP, User
from ..api.consts import SYSTEM
from ..api.messages import Message, MessageConfig, Messages, Registered
from ..compatible import PY2
from ..compatible.utils import force_encoded_string_output
from ..utils import PuidMap
from ..utils import enhance_connection, enhance_webwx_request, ensure_list, get_user_name, handle_response, \
    start_new_thread, wrap_user_name

logger = logging.getLogger(__name__)


class Bot(object):
    """
    机器人对象,用于登陆和操作微信账号,涵盖大部分 Web 微信的功能::
    
        from wxpy import *
        bot = Bot()
        
        # 机器人账号自身
        myself = bot.self
        
        # 向文件传输助手发送消息
        bot.file_helper.send('Hello from wxpy!')
        

    """

    def __init__(
            self, cache_path=None, console_qr=False, qr_path=None,
            qr_callback=None, login_callback=None, logout_callback=None
    ):
        """
        :param cache_path:
            * 设置当前会话的缓存路径,并开启缓存功能;为 `None` (默认) 则不开启缓存功能。
            * 开启缓存后可在短时间内避免重复扫码,缓存失效时会重新要求登陆。
            * 设为 `True` 时,使用默认的缓存路径 'wxpy.pkl'。
        :param console_qr:
            * 在终端中显示登陆二维码,需要安装 pillow 模块 (`pip3 install pillow`)。
            * 可为整数(int),表示二维码单元格的宽度,通常为 2 (当被设为 `True` 时,也将在内部当作 2)。
            * 也可为负数,表示以反色显示二维码,适用于浅底深字的命令行界面。
            * 例如: 在大部分 Linux 系统中可设为 `True` 或 2,而在 macOS Terminal 的默认白底配色中,应设为 -2。
        :param qr_path: 保存二维码的路径
        :param qr_callback: 获得二维码后的回调,可以用来定义二维码的处理方式,接收参数: uuid, status, qrcode
        :param login_callback: 登陆成功后的回调,若不指定,将进行清屏操作,并删除二维码文件
        :param logout_callback: 登出时的回调
        """

        self.core = itchat.Core()
        itchat.instanceList.append(self)

        enhance_connection(self.core.s)

        if cache_path is True:
            cache_path = 'wxpy.pkl'

        self.cache_path = cache_path

        if console_qr is True:
            console_qr = 2

        try:
            self.core.auto_login(
                hotReload=bool(cache_path), statusStorageDir=cache_path,
                enableCmdQR=console_qr, picDir=qr_path, qrCallback=qr_callback,
                loginCallback=login_callback, exitCallback=logout_callback
            )
        except FileNotFoundError as e:
            if 'xdg-open' in e.strerror:
                raise Exception('use `console_qr` arg while under pure console environment')
            raise

        # 用于 "synccheck" 请求的 "_" 参数,每次请求时 + 1
        self._sync_check_iterations = int(time.time() * 1000)

        enhance_webwx_request(self)

        self.self = User(self.core.loginInfo['User'], self)
        self.file_helper = Chat(wrap_user_name('filehelper'), self)

        self.messages = Messages()
        self.registered = Registered(self)

        self.puid_map = None
        self.auto_mark_as_read = False

        self.is_listening = False
        self.listening_thread = None

        if PY2:
            from wxpy.compatible.utils import TemporaryDirectory
            self.temp_dir = TemporaryDirectory(prefix='wxpy_')
        else:
            self.temp_dir = tempfile.TemporaryDirectory(prefix='wxpy_')

        self.start()

        atexit.register(self._cleanup)

    @force_encoded_string_output
    def __repr__(self):
        return '<{}: {}>'.format(self.__class__.__name__, self.self.name)

    def __unicode__(self):
        return '<{}: {}>'.format(self.__class__.__name__, self.self.name)

    @handle_response()
    def logout(self):
        """
        登出当前账号
        """

        logger.info('{}: logging out'.format(self))

        return self.core.logout()

    @property
    def alive(self):
        """
        若为登陆状态,则为 True,否则为 False
        """

        return self.core.alive

    @alive.setter
    def alive(self, value):
        self.core.alive = value

    def dump_login_status(self, cache_path=None):
        logger.debug('{}: dumping login status'.format(self))
        return self.core.dump_login_status(cache_path or self.cache_path)

    # chats

    def enable_puid(self, path='wxpy_puid.pkl'):
        """
        **可选操作:** 启用聊天对象的 :any:`puid <Chat.puid>` 属性::
            
            # 启用 puid 属性,并指定 puid 所需的映射数据保存/载入路径
            bot.enable_puid('wxpy_puid.pkl')
            
            # 指定一个好友
            my_friend = bot.friends().search('游否')[0]
            
            # 查看他的 puid
            print(my_friend.puid)
            # 'edfe8468'

        ..  tip::
        
            | :any:`puid <Chat.puid>` 是 **wxpy 特有的聊天对象/用户ID**
            | 不同于其他 ID 属性,**puid** 可始终被获取到,且具有稳定的唯一性

        :param path: puid 所需的映射数据保存/载入路径
        """

        self.puid_map = PuidMap(path)
        return self.puid_map

    def except_self(self, chats_or_dicts):
        """
        从聊天对象合集或用户字典列表中排除自身

        :param chats_or_dicts: 聊天对象合集或用户字典列表
        :return: 排除自身后的列表
        :rtype: :class:`wxpy.Chats`
        """
        return list(filter(lambda x: get_user_name(x) != self.self.user_name, chats_or_dicts))

    # Todo: itchat 中更新本地聊天对象时,缺少线程锁,导致 AttributeError: 'NoneType' object has no attribute 'get'

    def chats(self, update=False):
        """
        获取所有聊天对象

        :param update: 是否更新
        :return: 聊天对象合集
        :rtype: :class:`wxpy.Chats`
        """
        return Chats(self.friends(update) + self.groups(update) + self.mps(update), self)

    def _retrieve_itchat_storage(self, attr):
        with self.core.storageClass.updateLock:
            return getattr(self.core.storageClass, attr)

    @handle_response(Friend)
    def friends(self, update=False):
        """
        获取所有好友

        :param update: 是否更新
        :return: 聊天对象合集
        :rtype: :class:`wxpy.Chats`
        """

        if update:
            logger.info('{}: updating friends'.format(self))
            return self.core.get_friends(update=update)
        else:
            return self._retrieve_itchat_storage('memberList')

    @handle_response(Group)
    def groups(self, update=False, contact_only=False):
        """
        获取所有群聊对象

        一些不活跃的群可能无法被获取到,可通过在群内发言,或修改群名称的方式来激活

        :param update: 是否更新
        :param contact_only: 是否限于保存为联系人的群聊
        :return: 群聊合集
        :rtype: :class:`wxpy.Groups`
        """

        # itchat 原代码有些难懂,似乎 itchat 中的 get_contact() 所获取的内容视其 update 参数而变化
        # 如果 update=False 获取所有类型的本地聊天对象
        # 反之如果 update=True,变为获取收藏的聊天室

        if update or contact_only:
            logger.info('{}: updating groups'.format(self))
            return self.core.get_chatrooms(update=update, contactOnly=contact_only)
        else:
            return self._retrieve_itchat_storage('chatroomList')

    @handle_response(MP)
    def mps(self, update=False):
        """
        获取所有公众号

        :param update: 是否更新
        :return: 聊天对象合集
        :rtype: :class:`wxpy.Chats`
        """

        if update:
            logger.info('{}: updating mps'.format(self))
            return self.core.get_mps(update=update)
        else:
            return self._retrieve_itchat_storage('mpList')

    @handle_response(User)
    def user_details(self, user_or_users, chunk_size=50):
        """
        获取单个或批量获取多个用户的详细信息(地区、性别、签名等),但不可用于群聊成员

        :param user_or_users: 单个或多个用户对象或 user_name
        :param chunk_size: 分配请求时的单批数量,目前为 50
        :return: 单个或多个用户用户的详细信息
        """

        def chunks():
            total = ensure_list(user_or_users)
            for i in range(0, len(total), chunk_size):
                yield total[i:i + chunk_size]

        @handle_response()
        def process_one_chunk(_chunk):
            return self.core.update_friend(userName=get_user_name(_chunk))

        if isinstance(user_or_users, (list, tuple)):
            ret = list()
            for chunk in chunks():
                chunk_ret = process_one_chunk(chunk)
                if isinstance(chunk_ret, list):
                    ret += chunk_ret
                else:
                    ret.append(chunk_ret)
            return ret
        else:
            return process_one_chunk(user_or_users)

    def search(self, keywords=None, **attributes):
        """
        在所有类型的聊天对象中进行搜索
        
        ..  note:: 
        
            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象
            | 建议搭配 :any:`ensure_one()` 使用

        :param keywords: 聊天对象的名称关键词
        :param attributes: 属性键值对,键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东'
        :return: 匹配的聊天对象合集
        :rtype: :class:`wxpy.Chats`
        """

        return self.chats().search(keywords, **attributes)

    # add / create

    @handle_response()
    def add_friend(self, user, verify_content=''):
        """
        添加用户为好友

        :param user: 用户对象,或 user_name
        :param verify_content: 验证说明信息
        """

        logger.info('{}: adding {} (verify_content: {})'.format(self, user, verify_content))
        user_name = get_user_name(user)

        return self.core.add_friend(
            userName=user_name,
            status=2,
            verifyContent=verify_content,
            autoUpdate=True
        )

    @handle_response()
    def add_mp(self, user):

        """
        添加/关注 公众号
        
        :param user: 公众号对象,或 user_name
        """

        logger.info('{}: adding {}'.format(self, user))
        user_name = get_user_name(user)

        return self.core.add_friend(
            userName=user_name,
            status=1,
            autoUpdate=True
        )

    def accept_friend(self, user, verify_content=''):
        """
        接受用户为好友

        :param user: 用户对象或 user_name
        :param verify_content: 验证说明信息
        :return: 新的好友对象
        :rtype: :class:`wxpy.Friend`
        """

        logger.info('{}: accepting {} (verify_content: {})'.format(self, user, verify_content))

        @handle_response()
        def do():
            return self.core.add_friend(
                userName=get_user_name(user),
                status=3,
                verifyContent=verify_content,
                autoUpdate=True
            )

        do()
        # 若上一步没有抛出异常,则返回该好友
        for friend in self.friends():
            if friend == user:
                return friend

    def create_group(self, users, topic=None):
        """
        创建一个新的群聊

        :param users: 用户列表 (不含自己,至少 2 位)
        :param topic: 群名称
        :return: 若建群成功,返回一个新的群聊对象
        :rtype: :class:`wxpy.Group`
        """

        logger.info('{}: creating group (topic: {}), with users:\n{}'.format(
            self, topic, pformat(users)))

        @handle_response()
        def request():
            return self.core.create_chatroom(
                memberList=dict_list,
                topic=topic or ''
            )

        dict_list = wrap_user_name(self.except_self(ensure_list(users)))
        ret = request()
        user_name = ret.get('ChatRoomName')
        if user_name:
            return Group(self.core.update_chatroom(userName=user_name), self)
        else:
            from wxpy.utils import decode_text_from_webwx
            ret = decode_text_from_webwx(pformat(ret))
            raise Exception('Failed to create group:\n{}'.format(ret))

    # upload

    def upload_file(self, path):
        """
        | 上传文件,并获取 media_id
        | 可用于重复发送图片、表情、视频,和文件

        :param path: 文件路径
        :return: media_id
        :rtype: str
        """

        logger.info('{}: uploading file: {}'.format(self, path))

        @handle_response()
        def do():
            upload = functools.partial(self.core.upload_file, fileDir=path)
            ext = os.path.splitext(path)[1].lower()

            if ext in ('.bmp', '.png', '.jpeg', '.jpg', '.gif'):
                return upload(isPicture=True)
            elif ext == '.mp4':
                return upload(isVideo=True)
            else:
                return upload()

        return do().get('MediaId')

    # messages / register

    def _process_message(self, msg):
        """
        处理接收到的消息
        """

        if not self.alive:
            return

        config = self.registered.get_config(msg)

        logger.debug('{}: new message (func: {}):\n{}'.format(
            self, config.func.__name__ if config else None, msg))

        if config:

            def process():
                # noinspection PyBroadException
                try:
                    ret = config.func(msg)
                    if ret is not None:
                        msg.reply(ret)
                except:
                    logger.exception('an error occurred in {}.'.format(config.func))

                if self.auto_mark_as_read and not msg.type == SYSTEM and msg.sender != self.self:
                    from wxpy import ResponseError
                    try:
                        msg.chat.mark_as_read()
                    except ResponseError as e:
                        logger.warning('failed to mark as read: {}'.format(e))

            if config.run_async:
                start_new_thread(process, use_caller_name=True)
            else:
                process()

    def register(
            self, chats=None, msg_types=None,
            except_self=True, run_async=True, enabled=True
    ):
        """
        装饰器:用于注册消息配置

        :param chats: 消息所在的聊天对象:单个或列表形式的多个聊天对象或聊天类型,为空时匹配所有聊天对象
        :param msg_types: 消息的类型:单个或列表形式的多个消息类型,为空时匹配所有消息类型 (SYSTEM 类消息除外)
        :param except_self: 排除由自己发送的消息
        :param run_async: 是否异步执行所配置的函数:可提高响应速度
        :param enabled: 当前配置的默认开启状态,可事后动态开启或关闭
        """

        def do_register(func):
            self.registered.append(MessageConfig(
                bot=self, func=func, chats=chats, msg_types=msg_types,
                except_self=except_self, run_async=run_async, enabled=enabled
            ))

            return func

        return do_register

    def _listen(self):
        # Todo: 在短时间内收到多条消息时,会偶尔漏收消息(Web 微信没有问题)
        try:
            logger.info('{}: started'.format(self))
            self.is_listening = True

            while self.alive and self.is_listening:

                try:
                    msg = Message(self.core.msgList.get(timeout=0.5), self)
                except queue.Empty:
                    continue

                if msg.type != SYSTEM:
                    self.messages.append(msg)

                # noinspection PyBroadException
                try:
                    self._process_message(msg)
                except:
                    logger.exception('an error occurred while processing msg:\n{}'.format(msg))
        finally:
            self.is_listening = False
            logger.info('{}: stopped'.format(self))

    def start(self):
        """
        开始消息监听和处理 (登陆后会自动开始)
        """

        if not self.alive:
            logger.warning('{} has been logged out!'.format(self))
        elif self.is_listening:
            logger.warning('{} is already running, no need to start again.'.format(self))
        else:
            self.listening_thread = start_new_thread(self._listen)

    def stop(self):
        """
        停止消息监听和处理 (登出后会自动停止)
        """

        if self.is_listening:
            self.is_listening = False
            self.listening_thread.join()
        else:
            logger.warning('{} is not running.'.format(self))

    def join(self):
        """
        堵塞进程,直到结束消息监听 (例如,机器人被登出时)
        """

        if isinstance(self.listening_thread, Thread):
            try:
                logger.info('{}: joined'.format(self))
                self.listening_thread.join()
            except KeyboardInterrupt:
                pass

    def _cleanup(self):
        if self.is_listening:
            self.stop()
        if self.alive and self.core.useHotReload:
            self.dump_login_status()
        self.temp_dir.cleanup()


================================================
FILE: wxpy/api/chats/__init__.py
================================================
from .chat import Chat
from .chats import Chats
from .friend import Friend
from .group import Group
from .groups import Groups
from .member import Member
from .mp import MP
from .user import User


================================================
FILE: wxpy/api/chats/chat.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import datetime
import logging
import re
import time
from functools import partial, wraps

from wxpy.api.consts import ATTACHMENT, PICTURE, TEXT, VIDEO
from wxpy.compatible import *
from wxpy.compatible.utils import force_encoded_string_output
from wxpy.utils import handle_response

logger = logging.getLogger(__name__)


def wrapped_send(msg_type):
    """
    send() 系列方法较为雷同,因此采用装饰器方式完成发送,并返回 SentMessage 对象
    """

    def decorator(func):
        @wraps(func)
        def wrapped(self, *args, **kwargs):

            # 用于初始化 SentMessage 的属性
            sent_attrs = dict(
                type=msg_type, receiver=self,
                create_time=datetime.datetime.now()
            )

            # 被装饰函数需要返回两个部分:
            # itchat_call_or_ret: 请求 itchat 原函数的参数字典 (或返回值字典)
            # sent_attrs_from_method: 方法中需要添加到 SentMessage 的属性字典
            itchat_call_or_ret, sent_attrs_from_method = func(self, *args, **kwargs)

            if msg_type:
                # 找到原 itchat 中的同名函数,并转化为指定了 `toUserName` 的偏函数
                itchat_partial_func = partial(
                    getattr(self.bot.core, func.__name__),
                    toUserName=self.user_name
                )

                logger.info('sending {} to {}:\n{}'.format(
                    func.__name__[5:], self,
                    sent_attrs_from_method.get('text') or sent_attrs_from_method.get('path')
                ))

                @handle_response()
                def do_send():
                    return itchat_partial_func(**itchat_call_or_ret)

                ret = do_send()
            else:
                # send_raw_msg 会直接返回结果
                ret = itchat_call_or_ret

            sent_attrs['receive_time'] = datetime.datetime.now()

            try:
                sent_attrs['id'] = int(ret.get('MsgID'))
            except (ValueError, TypeError):
                pass

            sent_attrs['local_id'] = ret.get('LocalID')

            # 加入被装饰函数返回值中的属性字典
            sent_attrs.update(sent_attrs_from_method)

            from wxpy import SentMessage
            sent = SentMessage(attributes=sent_attrs)
            self.bot.messages.append(sent)

            return sent

        return wrapped

    return decorator


class Chat(object):
    """
    单个用户 (:class:`User`) 和群聊 (:class:`Group`) 的基础类
    """

    def __init__(self, raw, bot):

        self.raw = raw
        self.bot = bot

    @property
    def puid(self):
        """
        持续有效,且稳定唯一的聊天对象/用户ID,适用于持久保存
        
        请使用 :any:`Bot.enable_puid()` 来启用 puid 属性
        
        ..  tip::
        
            | :any:`puid <Chat.puid>` 是 **wxpy 特有的聊天对象/用户ID**
            | 不同于其他 ID 属性,**puid** 可始终被获取到,且具有稳定的唯一性

        ..  attention::
        
            puid 映射数据 **不可跨机器人使用**

        """

        if self.bot.puid_map:
            return self.bot.puid_map.get_puid(self)
        else:
            raise TypeError('puid is not enabled, you can enable it by `bot.enable_puid()`')

    @property
    def nick_name(self):
        """
        该聊天对象的昵称 (好友、群员的昵称,或群名称)
        """
        if self.user_name == 'filehelper':
            return '文件传输助手'
        elif self.user_name == 'fmessage':
            return '好友请求'
        else:
            return self.raw.get('NickName')

    @property
    def name(self):
        """
        | 该聊天对象的友好名称
        | 具体为: 从 备注名称、群聊显示名称、昵称(或群名称),或微信号中
        | 按序选取第一个可用的
        """
        for attr in 'remark_name', 'display_name', 'nick_name', 'wxid':
            _name = getattr(self, attr, None)
            if _name:
                return _name

    def send(self, content=None, media_id=None):
        """
        动态发送不同类型的消息,具体类型取决于 `msg` 的前缀。

        :param content:
            * 由 **前缀** 和 **内容** 两个部分组成,若 **省略前缀**,将作为纯文本消息发送
            * **前缀** 部分可为: '@fil@', '@img@', '@msg@', '@vid@' (不含引号)
            * 分别表示: 文件,图片,纯文本,视频
            * **内容** 部分可为: 文件、图片、视频的路径,或纯文本的内容
        :param media_id: 填写后可省略上传过程
        :rtype: :class:`wxpy.SentMessage`
        """

        method_map = dict(fil=self.send_file, img=self.send_image, vid=self.send_video)
        content = str('' if content is None else content)

        try:
            method, content = re.match(r'@(\w{3})@(.+)', content).groups()
        except AttributeError:
            method = None

        if method:
            return method_map[method](path=content, media_id=media_id)
        else:
            return self.send_msg(msg=content)

    @wrapped_send(TEXT)
    def send_msg(self, msg=None):
        """
        发送文本消息

        :param msg: 文本内容
        :rtype: :class:`wxpy.SentMessage`
        """

        if msg is None:
            msg = 'Hello from wxpy!'
        else:
            msg = str(msg)

        return dict(msg=msg), dict(text=msg)

    # Todo: 发送后可获取到 media_id

    @wrapped_send(PICTURE)
    def send_image(self, path, media_id=None):
        """
        发送图片

        :param path: 文件路径
        :param media_id: 设置后可省略上传
        :rtype: :class:`wxpy.SentMessage`
        """

        return dict(fileDir=path, mediaId=media_id), locals()

    @wrapped_send(ATTACHMENT)
    def send_file(self, path, media_id=None):
        """
        发送文件

        :param path: 文件路径
        :param media_id: 设置后可省略上传
        :rtype: :class:`wxpy.SentMessage`
        """

        return dict(fileDir=path, mediaId=media_id), locals()

    @wrapped_send(VIDEO)
    def send_video(self, path=None, media_id=None):
        """
        发送视频

        :param path: 文件路径
        :param media_id: 设置后可省略上传
        :rtype: :class:`wxpy.SentMessage`
        """

        return dict(fileDir=path, mediaId=media_id), locals()

    @wrapped_send(None)
    def send_raw_msg(self, raw_type, raw_content, uri=None, msg_ext=None):
        """
        以原始格式发送其他类型的消息。

        :param int raw_type: 原始的整数消息类型
        :param str raw_content: 原始的消息内容
        :param str uri: 请求路径,默认为 '/webwxsendmsg'
        :param dict msg_ext: 消息的扩展属性 (会被更新到 `Msg` 键中)
        :rtype: :class:`wxpy.SentMessage`

        例如,发送好友或公众号的名片::

            my_friend.send_raw_msg(
                # 名片的原始消息类型
                raw_type=42,
                # 注意 `username` 在这里应为微信 ID,且被发送的名片必须为自己的好友
                raw_content='<msg username="wxpy_bot" nickname="wxpy 机器人"/>'
            )
        """

        logger.info('sending raw msg to {}'.format(self))

        uri = uri or '/webwxsendmsg'

        from wxpy.utils import BaseRequest
        req = BaseRequest(self.bot, uri=uri)

        msg = {
            'Type': raw_type,
            'Content': raw_content,
            'FromUserName': self.bot.self.user_name,
            'ToUserName': self.user_name,
            'LocalID': int(time.time() * 1e4),
            'ClientMsgId': int(time.time() * 1e4),
        }

        if msg_ext:
            msg.update(msg_ext)

        req.data.update({'Msg': msg, 'Scene': 0})

        # noinspection PyUnresolvedReferences
        return req.post(), {
            'raw_type': raw_type,
            'raw_content': raw_content,
            'uri': uri,
            'msg_ext': msg_ext,
        }

    @handle_response()
    def mark_as_read(self):
        """
        消除当前聊天对象的未读提示小红点
        """

        from wxpy.utils import BaseRequest
        req = BaseRequest(
            bot=self.bot,
            # itchat 中的 pass_ticket 已经预先编码了
            uri='/webwxstatusnotify?pass_ticket={}'.format(self.bot.core.loginInfo['pass_ticket'])
        )

        req.data.update({
            'ClientMsgId': int(time.time() * 1000),
            'Code': 1,
            'FromUserName': self.bot.self.user_name,
            'ToUserName': self.user_name,
        })

        logger.debug('marking {} as read'.format(self))

        return req.request('POST')

    @handle_response()
    def pin(self):
        """
        将聊天对象置顶
        """
        logger.info('pinning {}'.format(self))
        return self.bot.core.set_pinned(userName=self.user_name, isPinned=True)

    @handle_response()
    def unpin(self):
        """
        取消聊天对象的置顶状态
        """
        logger.info('unpinning {}'.format(self))
        return self.bot.core.set_pinned(userName=self.user_name, isPinned=False)

    @handle_response()
    def get_avatar(self, save_path=None):
        """
        获取头像

        :param save_path: 保存路径(后缀通常为.jpg),若为 `None` 则返回字节数据
        """

        logger.info('getting avatar of {}'.format(self))

        from .group import Group
        from .member import Member
        from .friend import User

        if isinstance(self, Group):
            kwargs = dict(userName=None, chatroomUserName=self.user_name)
        elif isinstance(self, Member):
            kwargs = dict(userName=self.user_name, chatroomUserName=self.group.user_name)
        elif isinstance(self, User):
            kwargs = dict(userName=self.user_name, chatroomUserName=None)
        else:
            raise TypeError('expected `Chat`, got`{}`'.format(type(self)))

        kwargs.update(dict(picDir=save_path))

        return self.bot.core.get_head_img(**kwargs)

    @property
    def uin(self):
        """
        微信中的聊天对象ID,固定且唯一

        | 因微信的隐私策略,该属性有时无法被获取到
        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID
        """
        return self.raw.get('Uin')

    @property
    def alias(self):
        """
        若用户进行过一次性的 "设置微信号" 操作,则该值为用户设置的"微信号",固定且唯一

        | 因微信的隐私策略,该属性有时无法被获取到
        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID
        """
        return self.raw.get('Alias')

    @property
    def wxid(self):
        """
        聊天对象的微信ID (实际为 .alias 或 .uin)

        | 因微信的隐私策略,该属性有时无法被获取到
        | 建议使用 :any:`puid <Chat.puid>` 作为用户的唯一 ID
        """

        return self.alias or self.uin or None

    @property
    def user_name(self):
        """
        该聊天对象的内部 ID,通常不需要用到

        ..  attention::

            同个聊天对象在不同用户中,此 ID **不一致** ,且可能在新会话中 **被改变**!
        """
        return self.raw.get('UserName')

    @force_encoded_string_output
    def __repr__(self):
        return '<{}: {}>'.format(self.__class__.__name__, self.name)

    def __unicode__(self):
        return '<{}: {}>'.format(self.__class__.__name__, self.name)

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __cmp__(self, other):
        if hash(self) == hash(other):
            return 0
        return 1

    def __hash__(self):
        return hash((Chat, self.user_name))


================================================
FILE: wxpy/api/chats/chats.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging
import time
from collections import Counter

from wxpy.utils import match_attributes, match_name
from wxpy.compatible import *

logger = logging.getLogger(__name__)


class Chats(list):
    """
    多个聊天对象的合集,可用于搜索或统计
    """

    def __init__(self, chat_list=None, source=None):
        if chat_list:
            super(Chats, self).__init__(chat_list)
        self.source = source

    def __add__(self, other):
        return Chats(super(Chats, self).__add__(other or list()))

    def search(self, keywords=None, **attributes):
        """
        在聊天对象合集中进行搜索
        
        ..  note:: 
    
            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象
            | 建议搭配 :any:`ensure_one()` 使用

        :param keywords: 聊天对象的名称关键词
        :param attributes: 属性键值对,键可以是 sex(性别), province(省份), city(城市) 等。例如可指定 province='广东'
        :return: 匹配的聊天对象合集
        :rtype: :class:`wxpy.Chats`
        """

        def match(chat):

            if not match_name(chat, keywords):
                return
            if not match_attributes(chat, **attributes):
                return
            return True

        return Chats(filter(match, self), self.source)

    def stats(self, attribs=('sex', 'province', 'city')):
        """
        统计各属性的分布情况

        :param attribs: 需统计的属性列表或元组
        :return: 统计结果
        """

        def attr_stat(objects, attr_name):
            return Counter(list(map(lambda x: getattr(x, attr_name), objects)))

        from wxpy.utils import ensure_list
        attribs = ensure_list(attribs)
        ret = dict()
        for attr in attribs:
            ret[attr] = attr_stat(self, attr)
        return ret

    def stats_text(self, total=True, sex=True, top_provinces=10, top_cities=10):
        """
        简单的统计结果的文本

        :param total: 总体数量
        :param sex: 性别分布
        :param top_provinces: 省份分布
        :param top_cities: 城市分布
        :return: 统计结果文本
        """

        from .group import Group
        from wxpy.api.consts import FEMALE
        from wxpy.api.consts import MALE
        from wxpy.api.bot import Bot

        def top_n_text(attr, n):
            top_n = list(filter(lambda x: x[0], stats[attr].most_common()))[:n]
            top_n = ['{}: {} ({:.2%})'.format(k, v, v / len(self)) for k, v in top_n]
            return '\n'.join(top_n)

        stats = self.stats()

        text = str()

        if total:
            if self.source:
                if isinstance(self.source, Bot):
                    user_title = '微信好友'
                    nick_name = self.source.self.nick_name
                elif isinstance(self.source, Group):
                    user_title = '群成员'
                    nick_name = self.source.nick_name
                else:
                    raise TypeError('source should be Bot or Group')
                text += '{nick_name} 共有 {total} 位{user_title}\n\n'.format(
                    nick_name=nick_name,
                    total=len(self),
                    user_title=user_title
                )
            else:
                text += '共有 {} 位用户\n\n'.format(len(self))

        if sex and self:
            males = stats['sex'].get(MALE, 0)
            females = stats['sex'].get(FEMALE, 0)

            text += '男性: {males} ({male_rate:.1%})\n女性: {females} ({female_rate:.1%})\n\n'.format(
                males=males,
                male_rate=males / len(self),
                females=females,
                female_rate=females / len(self),
            )

        if top_provinces and self:
            text += 'TOP {} 省份\n{}\n\n'.format(
                top_provinces,
                top_n_text('province', top_provinces)
            )

        if top_cities and self:
            text += 'TOP {} 城市\n{}\n\n'.format(
                top_cities,
                top_n_text('city', top_cities)
            )

        return text

    def add_all(self, interval=3, verify_content=''):
        """
        将合集中的所有用户加为好友,请小心应对调用频率限制!

        :param interval: 间隔时间(秒)
        :param verify_content: 验证说明文本
        """
        to_add = self[:]

        while to_add:
            adding = to_add.pop(0)
            logger.info('Adding {}'.format(adding))
            ret = adding.add(verify_content=verify_content)
            logger.info(ret)
            logger.info('Waiting for {} seconds'.format(interval))
            if to_add:
                time.sleep(interval)


================================================
FILE: wxpy/api/chats/friend.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging

from .user import User

logger = logging.getLogger(__name__)


class Friend(User):
    """
    好友对象
    """

    pass


================================================
FILE: wxpy/api/chats/group.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging

from wxpy.utils import ensure_list, get_user_name, handle_response, wrap_user_name
from .chat import Chat
from .chats import Chats
from .member import Member

logger = logging.getLogger(__name__)


class Group(Chat):
    """
    群聊对象
    """

    def __init__(self, raw, bot):
        super(Group, self).__init__(raw, bot)

    @property
    def members(self):
        """
        群聊的成员列表
        """

        def raw_member_list(update=False):
            if update:
                self.update_group()
            return self.raw.get('MemberList', list())

        ret = Chats(source=self)
        ret.extend(map(
            lambda x: Member(x, self),
            raw_member_list() or raw_member_list(True)
        ))
        return ret

    def __contains__(self, user):
        user_name = get_user_name(user)
        for member in self.members:
            if member.user_name == user_name:
                return member

    def __iter__(self):
        for member in self.members:
            yield member

    def __len__(self):
        return len(self.members)

    def search(self, keywords=None, **attributes):
        """
        在群聊中搜索成员
        
        ..  note:: 
        
            | 搜索结果为一个 :class:`Chats (列表) <Chats>` 对象
            | 建议搭配 :any:`ensure_one()` 使用

        :param keywords: 成员名称关键词
        :param attributes: 属性键值对
        :return: 匹配的群聊成员
        :rtype: :class:`wxpy.Chats`
        """
        return self.members.search(keywords, **attributes)

    @property
    def owner(self):
        """
        返回群主对象
        """
        owner_user_name = self.raw.get('ChatRoomOwner')
        if owner_user_name:
            for member in self:
                if member.user_name == owner_user_name:
                    return member
        elif self.members:
            return self.members[0]

    @property
    def is_owner(self):
        """
        判断所属 bot 是否为群管理员
        """
        return self.raw.get('IsOwner') == 1 or self.owner == self.bot.self

    @property
    def self(self):
        """
        机器人自身 (作为群成员)
        """
        for member in self.members:
            if member == self.bot.self:
                return member
        return Member(self.bot.core.loginInfo['User'], self)

    def update_group(self, members_details=False):
        """
        更新群聊的信息

        :param members_details: 是否包括群聊成员的详细信息 (地区、性别、签名等)
        """

        logger.info('updating {} (members_details={})'.format(self, members_details))

        @handle_response()
        def do():
            return self.bot.core.update_chatroom(self.user_name, members_details)

        super(Group, self).__init__(do(), self.bot)

    @handle_response()
    def add_members(self, users, use_invitation=False):
        """
        向群聊中加入用户

        :param users: 待加入的用户列表或单个用户
        :param use_invitation: 使用发送邀请的方式
        """

        logger.info('adding {} into {} (use_invitation={}))'.format(users, self, use_invitation))

        return self.bot.core.add_member_into_chatroom(
            self.user_name,
            ensure_list(wrap_user_name(users)),
            use_invitation
        )

    @handle_response()
    def remove_members(self, members):
        """
        从群聊中移除用户

        :param members: 待移除的用户列表或单个用户
        """

        logger.info('removing {} from {}'.format(members, self))

        return self.bot.core.delete_member_from_chatroom(
            self.user_name,
            ensure_list(wrap_user_name(members))
        )

    def rename_group(self, name):
        """
        修改群聊名称

        :param name: 新的名称,超长部分会被截断 (最长32字节)
        """

        encodings = ('gbk', 'utf-8')

        trimmed = False

        for ecd in encodings:
            for length in range(32, 24, -1):
                try:
                    name = bytes(name.encode(ecd))[:length].decode(ecd)
                except (UnicodeEncodeError, UnicodeDecodeError):
                    continue
                else:
                    trimmed = True
                    break
            if trimmed:
                break

        @handle_response()
        def do():
            logger.info('renaming group: {} => {}'.format(self.name, name))
            return self.bot.core.set_chatroom_name(get_user_name(self), name)

        ret = do()
        self.update_group()
        return ret


================================================
FILE: wxpy/api/chats/groups.py
================================================
# coding: utf-8
from __future__ import unicode_literals

from wxpy.utils import ensure_list, match_attributes, match_name
from .user import User


class Groups(list):
    """
    群聊的合集,可用于按条件搜索
    """

    # 记录已知的 shadow group 和 valid group
    # shadow group 直接抛弃
    # valid group 直接通过
    # 其他的需要确认是否包含机器人自身,并再分类到上面两种群中

    shadow_group_user_names = list()
    valid_group_user_names = list()

    def __init__(self, group_list=None):
        if group_list:
            # Web 微信服务端似乎有个 BUG,会返回不存在的群
            # 具体表现为: 名称为先前退出的群,但成员列表却完全陌生
            # 因此加一个保护逻辑: 只返回"包含自己的群"

            groups_to_init = list()

            for group in group_list:
                if group.user_name in Groups.shadow_group_user_names:
                    continue
                elif group.user_name in Groups.valid_group_user_names:
                    groups_to_init.append(group)
                else:
                    if group.bot.self in group:
                        Groups.valid_group_user_names.append(group.user_name)
                        groups_to_init.append(group)
                    else:
                        Groups.shadow_group_user_names.append(group.user_name)

            super(Groups, self).__init__(groups_to_init)

    def search(self, keywords=None, users=None, **attributes):
        """
        在群聊合集中,根据给定的条件进行搜索

        :param keywords: 群聊名称关键词
        :param users: 需包含的用户
        :param attributes: 属性键值对,键可以是 owner(群主对象), is_owner(自身是否为群主), nick_name(精准名称) 等。
        :return: 匹配条件的群聊列表
        :rtype: :class:`wxpy.Groups`
        """

        users = ensure_list(users)
        if users:
            for user in users:
                if not isinstance(user, User):
                    raise TypeError('expected `User`, got {} (type: {})'.format(user, type(user)))

        def match(group):
            if not match_name(group, keywords):
                return
            if users:
                for _user in users:
                    if _user not in group:
                        return
            if not match_attributes(group, **attributes):
                return
            return True

        return Groups(filter(match, self))


================================================
FILE: wxpy/api/chats/member.py
================================================
# coding: utf-8
from __future__ import unicode_literals

from .user import User


# Todo: 若尝试获取群成员信息时为空,自动更新成员信息 (并要照顾到遍历所有群成员的场景)


class Member(User):
    """
    群聊成员对象
    """

    def __init__(self, raw, group):
        super(Member, self).__init__(raw, group.bot)
        self._group_user_name = group.user_name

    @property
    def group(self):
        for _group in self.bot.groups():
            if _group.user_name == self._group_user_name:
                return _group
        raise Exception('failed to find the group belong to')

    @property
    def display_name(self):
        """
        在群聊中的显示昵称
        """
        return self.raw.get('DisplayName')

    def remove(self):
        """
        从群聊中移除该成员
        """
        return self.group.remove_members(self)

    @property
    def name(self):
        """
        | 该群成员的友好名称
        | 具体为: 从 群聊显示名称、昵称(或群名称),或微信号中,按序选取第一个可用的
        """
        for attr in 'display_name', 'nick_name', 'wxid':
            _name = getattr(self, attr, None)
            if _name:
                return _name


================================================
FILE: wxpy/api/chats/mp.py
================================================
# coding: utf-8

from .user import User


class MP(User):
    """
    公众号对象
    """
    pass


================================================
FILE: wxpy/api/chats/user.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging

from wxpy.utils import handle_response
from .chat import Chat

logger = logging.getLogger(__name__)


class User(Chat):
    """
    好友(:class:`Friend`)、群聊成员(:class:`Member`),和公众号(:class:`MP`) 的基础类
    """

    def __init__(self, raw, bot):
        super(User, self).__init__(raw, bot)

    @property
    def remark_name(self):
        """
        备注名称
        """
        return self.raw.get('RemarkName')

    @handle_response()
    def set_remark_name(self, remark_name):
        """
        设置或修改好友的备注名称

        :param remark_name: 新的备注名称
        """

        logger.info('setting remark name for {}: {}'.format(self, remark_name))

        return self.bot.core.set_alias(userName=self.user_name, alias=remark_name)

    @property
    def sex(self):
        """
        性别,目前有::
        
            # 男性
            MALE = 1
            # 女性
            FEMALE = 2
        
        未设置时为 `None`
        """
        return self.raw.get('Sex')

    @property
    def province(self):
        """
        省份
        """
        return self.raw.get('Province')

    @property
    def city(self):
        """
        城市
        """
        return self.raw.get('City')

    @property
    def signature(self):
        """
        个性签名
        """
        return self.raw.get('Signature')

    @property
    def is_friend(self):
        """
        判断当前用户是否为好友关系

        :return: 若为好友关系,返回对应的好友,否则返回 False
        """
        if self.bot:
            try:
                friends = self.bot.friends()
                index = friends.index(self)
                return friends[index]
            except ValueError:
                return False

    def add(self, verify_content=''):
        """
        把当前用户加为好友

        :param verify_content: 验证信息(文本)
        """
        return self.bot.add_friend(user=self, verify_content=verify_content)

    def accept(self, verify_content=''):
        """
        接受当前用户为好友

        :param verify_content: 验证信息(文本)
        :return: 新的好友对象
        :rtype: :class:`wxpy.Friend`
        """
        return self.bot.accept_friend(user=self, verify_content=verify_content)


================================================
FILE: wxpy/api/consts.py
================================================
# coding: utf-8
from __future__ import unicode_literals
# 文本
TEXT = 'Text'
# 位置
MAP = 'Map'
# 名片
CARD = 'Card'
# 提示
NOTE = 'Note'
# 分享
SHARING = 'Sharing'
# 图片
PICTURE = 'Picture'
# 语音
RECORDING = 'Recording'
# 文件
ATTACHMENT = 'Attachment'
# 视频
VIDEO = 'Video'
# 好友请求
FRIENDS = 'Friends'
# 系统
SYSTEM = 'System'

# 男性
MALE = 1
# 女性
FEMALE = 2


================================================
FILE: wxpy/api/messages/__init__.py
================================================
from .article import Article
from .message import Message
from .message_config import MessageConfig
from .messages import Messages
from .registered import Registered
from .sent_message import SentMessage


================================================
FILE: wxpy/api/messages/article.py
================================================
# coding: utf-8
from __future__ import unicode_literals

from wxpy.compatible.utils import force_encoded_string_output


class Article(object):
    def __init__(self):
        """
        公众号推送中的单篇文章内容 (一次可推送多篇)
        """

        # 标题
        self.title = None
        # 摘要
        self.summary = None
        # 文章 URL
        self.url = None
        # 封面图片 URL
        self.cover = None

    @force_encoded_string_output
    def __repr__(self):
        return self.__unicode__()

    def __unicode__(self):
        return '<{}: {}>'.format(self.__class__.__name__, self.title)

    def __hash__(self):
        return hash((Article, self.url))

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __cmp__(self, other):
        if hash(self) == hash(other):
            return 0
        return 1


================================================
FILE: wxpy/api/messages/message.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging
import os
import tempfile
import weakref
from datetime import datetime
from xml.etree import ElementTree as ETree

try:
    import html
except ImportError:
    # Python 2.6-2.7
    # noinspection PyUnresolvedReferences,PyUnresolvedReferences,PyCompatibility
    from HTMLParser import HTMLParser

    html = HTMLParser()

from wxpy.api.chats import Chat, Group, Member, User
from wxpy.compatible.utils import force_encoded_string_output
from wxpy.utils import wrap_user_name, repr_message
from .article import Article
from ..consts import ATTACHMENT, CARD, FRIENDS, MAP, PICTURE, RECORDING, SHARING, TEXT, VIDEO
from ...compatible import *

logger = logging.getLogger(__name__)


class Message(object):
    """
    单条消息对象,包括:
    
    * 来自好友、群聊、好友请求等聊天对象的消息
    * 使用机器人账号在手机微信中发送的消息
    
    | 但 **不包括** 代码中通过 .send/reply() 系列方法发出的消息
    | 此类消息请参见 :class:`SentMessage`
    """

    def __init__(self, raw, bot):
        self.raw = raw
        self.bot = weakref.proxy(bot)

        self._receive_time = datetime.now()

        # 将 msg.chat.send* 方法绑定到 msg.reply*,例如 msg.chat.send_img => msg.reply_img
        for method in '', '_image', '_file', '_video', '_msg', '_raw_msg':
            setattr(self, 'reply' + method, getattr(self.chat, 'send' + method))

    def __hash__(self):
        return hash((Message, self.id))

    @force_encoded_string_output
    def __repr__(self):
        return repr_message(self)

    def __unicode__(self):
        return repr_message(self)

    # basic

    @property
    def type(self):
        """
        消息的类型,目前可为以下值::
        
            # 文本
            TEXT = 'Text'
            # 位置
            MAP = 'Map'
            # 名片
            CARD = 'Card'
            # 提示
            NOTE = 'Note'
            # 分享
            SHARING = 'Sharing'
            # 图片
            PICTURE = 'Picture'
            # 语音
            RECORDING = 'Recording'
            # 文件
            ATTACHMENT = 'Attachment'
            # 视频
            VIDEO = 'Video'
            # 好友请求
            FRIENDS = 'Friends'
            # 系统
            SYSTEM = 'System'
        
        :rtype: str
        """
        return self.raw.get('Type')

    @property
    def id(self):
        """
        消息的唯一 ID (通常为大于 0 的 64 位整型)
        """
        return self.raw.get('NewMsgId')

    # content
    @property
    def text(self):
        """
        消息的文本内容
        """
        _type = self.type
        _card = self.card

        if _type == MAP:
            location = self.location
            if location:
                return location.get('label')
        elif _card:
            if _type == CARD:
                return _card.name
            elif _type == FRIENDS:
                return _card.raw.get('Content')

        ret = self.raw.get('Text')
        if isinstance(ret, str):
            return ret

    def get_file(self, save_path=None):
        """
        下载图片、视频、语音、附件消息中的文件内容。
        
        可与 :any:`Message.file_name` 配合使用。

        :param save_path: 文件的保存路径。若为 None,将直接返回字节数据
        """

        _text = self.raw.get('Text')
        if callable(_text) and self.type in (PICTURE, RECORDING, ATTACHMENT, VIDEO):
            return _text(save_path)
        else:
            raise ValueError('download method not found, or invalid message type')

    @property
    def file_name(self):
        """
        消息中文件的文件名
        """
        return self.raw.get('FileName')

    @property
    def file_size(self):
        """
        消息中文件的体积大小
        """
        return self.raw.get('FileSize')

    @property
    def media_id(self):
        """
        文件类消息中的文件资源 ID (但图片视频语音等其他消息中为空)
        """
        return self.raw.get('MediaId')

    # group

    @property
    def is_at(self):
        """
        当消息来自群聊,且被 @ 时,为 True
        """
        return self.raw.get('IsAt') or self.raw.get('isAt')

    # misc

    @property
    def img_height(self):
        """
        图片高度
        """
        return self.raw.get('ImgHeight')

    @property
    def img_width(self):
        """
        图片宽度
        """
        return self.raw.get('ImgWidth')

    @property
    def play_length(self):
        """
        视频长度
        """
        return self.raw.get('PlayLength')

    @property
    def voice_length(self):
        """
        语音长度
        """
        return self.raw.get('VoiceLength')

    @property
    def url(self):
        """
        分享类消息中的网页 URL
        """
        _url = self.raw.get('Url')
        if isinstance(_url, str):
            _url = html.unescape(_url)

        return _url

    @property
    def articles(self):
        """
        公众号推送中的文章列表 (首篇的 标题/地址 与消息中的 text/url 相同)

        其中,每篇文章均有以下属性:

        * `title`: 标题
        * `summary`: 摘要
        * `url`: 文章 URL
        * `cover`: 封面或缩略图 URL
        """

        from wxpy import MP
        if self.type == SHARING and isinstance(self.sender, MP):
            tree = ETree.fromstring(self.raw['Content'])
            # noinspection SpellCheckingInspection
            items = tree.findall('.//mmreader/category/item')

            article_list = list()

            for item in items:
                def find_text(tag):
                    found = item.find(tag)
                    if found is not None:
                        return found.text

                article = Article()
                article.title = find_text('title')
                article.summary = find_text('digest')
                article.url = find_text('url')
                article.cover = find_text('cover')
                article_list.append(article)

            return article_list

    @property
    def card(self):
        """
        * 好友请求中的请求用户
        * 名片消息中的推荐用户
        """
        if self.type in (CARD, FRIENDS):
            return User(self.raw.get('RecommendInfo'), self.bot)

    # time

    @property
    def create_time(self):
        """
        服务端发送时间
        """
        # noinspection PyBroadException
        try:
            return datetime.fromtimestamp(self.raw.get('CreateTime'))
        except:
            pass

    @property
    def receive_time(self):
        """
        本地接收时间
        """
        return self._receive_time

    @property
    def latency(self):
        """
        消息的延迟秒数 (发送时间和接收时间的差值)
        """
        create_time = self.create_time
        if create_time:
            return (self.receive_time - create_time).total_seconds()

    @property
    def location(self):
        """
        位置消息中的地理位置信息
        """
        try:
            ret = ETree.fromstring(self.raw['OriContent']).find('location').attrib
            try:
                ret['x'] = float(ret['x'])
                ret['y'] = float(ret['y'])
                ret['scale'] = int(ret['scale'])
                ret['maptype'] = int(ret['maptype'])
            except (KeyError, ValueError):
                pass
            return ret
        except (TypeError, KeyError, ValueError, ETree.ParseError):
            pass

    # chats

    @property
    def chat(self):
        """
        消息所在的聊天会话,即:

        * 对于自己发送的消息,为消息的接收者
        * 对于别人发送的消息,为消息的发送者
        
        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`
        """

        if self.raw.get('FromUserName') == self.bot.self.user_name:
            return self.receiver
        else:
            return self.sender

    @property
    def sender(self):
        """
        消息的发送者
        
        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`
        """

        return self._get_chat_by_user_name(self.raw.get('FromUserName'))

    @property
    def receiver(self):
        """
        消息的接收者
        
        :rtype: :class:`wxpy.User`, :class:`wxpy.Group`
        """

        return self._get_chat_by_user_name(self.raw.get('ToUserName'))

    @property
    def member(self):
        """
        * 若消息来自群聊,则此属性为消息的实际发送人(具体的群成员)
        * 若消息来自其他聊天对象(非群聊),则此属性为 None
        
        :rtype: NoneType, :class:`wxpy.Member`
        """

        if isinstance(self.chat, Group):
            if self.sender == self.bot.self:
                return self.chat.self
            else:
                actual_user_name = self.raw.get('ActualUserName')
                for _member in self.chat.members:
                    if _member.user_name == actual_user_name:
                        return _member
                return Member(dict(
                    UserName=actual_user_name,
                    NickName=self.raw.get('ActualNickName')
                ), self.chat)

    def _get_chat_by_user_name(self, user_name):
        """
        通过 user_name 找到对应的聊天对象

        :param user_name: user_name
        :return: 找到的对应聊天对象
        """

        def match_in_chats(_chats):
            for c in _chats:
                if c.user_name == user_name:
                    return c

        _chat = None

        if user_name.startswith('@@'):
            _chat = match_in_chats(self.bot.groups())
        elif user_name:
            _chat = match_in_chats(self.bot.friends())
            if not _chat:
                _chat = match_in_chats(self.bot.mps())

        if not _chat:
            _chat = Chat(wrap_user_name(user_name), self.bot)

        return _chat

    def forward(self, chat, prefix=None, suffix=None, raise_for_unsupported=False):
        """
        将本消息转发给其他聊天对象

        支持以下消息类型
            * 文本 (`TEXT`)
            * 视频(`VIDEO`)
            * 文件 (`ATTACHMENT`)
            * 图片/自定义表情 (`PICTURE`)

                * 但不支持表情商店中的表情

            * 名片 (`CARD`)

                * 仅支持公众号名片,以及自己发出的个人号名片

            * 分享 (`SHARING`)

                * 会转化为 `标题 + 链接` 形式的文本消息

            * 语音 (`RECORDING`)

                * 会以文件方式发送
            
            * 地图 (`MAP`)
                
                * 会转化为 `位置名称 + 地图链接` 形式的文本消息

        :param Chat chat: 接收转发消息的聊天对象
        :param str prefix: 转发时增加的 **前缀** 文本,原消息为文本时会自动换行
        :param str suffix: 转发时增加的 **后缀** 文本,原消息为文本时会自动换行
        :param bool raise_for_unsupported:
            | 为 True 时,将为不支持的消息类型抛出 `NotImplementedError` 异常

        例如,将公司群中的老板消息转发出来::

            from wxpy import *

            bot = Bot()

            # 定位公司群
            company_group = ensure_one(bot.groups().search('公司微信群'))

            # 定位老板
            boss = ensure_one(company_group.search('老板大名'))

            # 将老板的消息转发到文件传输助手
            @bot.register(company_group)
            def forward_boss_message(msg):
                if msg.member == boss:
                    msg.forward(bot.file_helper, prefix='老板发言')

            # 堵塞线程
            embed()

        """

        logger.info('{}: forwarding to {}: {}'.format(self.bot, chat, self))

        def wrapped_send(send_type, *args, **kwargs):
            if send_type == 'msg':
                if args:
                    text = args[0]
                elif kwargs:
                    text = kwargs['msg']
                else:
                    text = self.text
                ret = chat.send_msg('{}{}{}'.format(
                    str(prefix) + '\n' if prefix else '',
                    text,
                    '\n' + str(suffix) if suffix else '',
                ))
            else:
                if prefix:
                    chat.send_msg(prefix)
                ret = getattr(chat, 'send_{}'.format(send_type))(*args, **kwargs)
                if suffix:
                    chat.send_msg(suffix)

            return ret

        def download_and_send():
            fd, path = tempfile.mkstemp(
                suffix='_{}'.format(self.file_name),
                dir=self.bot.temp_dir.name
            )

            try:
                self.get_file(path)
                if self.type == PICTURE:
                    return wrapped_send('image', path)
                elif self.type == VIDEO:
                    return wrapped_send('video', path)
                else:
                    return wrapped_send('file', path)
            finally:
                os.close(fd)

        def raise_properly(text):
            logger.warning(text)
            if raise_for_unsupported:
                raise NotImplementedError(text)

        if self.type == TEXT:
            return wrapped_send('msg')

        elif self.type == SHARING:
            return wrapped_send('msg', '{}\n{}'.format(self.text, self.url))

        elif self.type == MAP:
            return wrapped_send('msg', '{}: {}\n{}'.format(
                self.location['poiname'], self.location['label'], self.url
            ))

        elif self.type == ATTACHMENT:

            # noinspection SpellCheckingInspection
            content = \
                "<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''>" \
                "<title>{file_name}</title><des></des><action></action>" \
                "<type>6</type><content></content><url></url><lowurl></lowurl>" \
                "<appattach><totallen>{file_size}</totallen><attachid>{media_id}</attachid>" \
                "<fileext>{file_ext}</fileext></appattach><extinfo></extinfo></appmsg>"

            content = content.format(
                file_name=self.file_name,
                file_size=self.file_size,
                media_id=self.media_id,
                file_ext=os.path.splitext(self.file_name)[1].replace('.', '')
            )

            return wrapped_send(
                send_type='raw_msg',
                raw_type=self.raw['MsgType'],
                raw_content=content,
                uri='/webwxsendappmsg?fun=async&f=json'
            )

        elif self.type == CARD:
            if self.card.raw.get('AttrStatus') and self.sender != self.bot.self:
                # 为个人名片,且不为自己所发出
                raise_properly('Personal cards sent from others are unsupported:\n{}'.format(self))
            else:
                return wrapped_send(
                    send_type='raw_msg',
                    raw_type=self.raw['MsgType'],
                    raw_content=self.raw['Content'],
                    uri='/webwxsendmsg'
                )

        elif self.type == PICTURE:
            if self.raw.get('HasProductId'):
                # 来自表情商店的表情
                raise_properly('Stickers from store are unsupported:\n{}'.format(self))
            else:
                return download_and_send()

        elif self.type == VIDEO:
            return download_and_send()

        elif self.type == RECORDING:
            return download_and_send()

        else:
            raise_properly('Unsupported message type:\n{}'.format(self))


================================================
FILE: wxpy/api/messages/message_config.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging
import weakref

from wxpy.compatible.utils import force_encoded_string_output
from wxpy.utils import ensure_list

logger = logging.getLogger(__name__)


class MessageConfig(object):
    """
    单个消息注册配置
    """

    def __init__(
            self, bot, func,
            chats, msg_types, except_self,
            run_async, enabled
    ):
        self.bot = weakref.proxy(bot)
        self.func = func

        self.chats = ensure_list(chats)
        self.msg_types = ensure_list(msg_types)
        self.except_self = except_self

        self.run_async = run_async
        self._enabled = None
        self.enabled = enabled

    @property
    def enabled(self):
        """
        配置的开启状态
        """
        return self._enabled

    @enabled.setter
    def enabled(self, boolean):
        """
        设置配置的开启状态
        """
        self._enabled = boolean
        logger.info(self)

    @force_encoded_string_output
    def __repr__(self):
        return '<{}: {}: {} ({}{})>'.format(
            self.__class__.__name__,
            self.bot.self.name,
            self.func.__name__,
            'Enabled' if self.enabled else 'Disabled',
            ', Async' if self.run_async else '',
        )

    def __unicode__(self):
        return '<{}: {}: {} ({}{})>'.format(
            self.__class__.__name__,
            self.bot.self.name,
            self.func.__name__,
            'Enabled' if self.enabled else 'Disabled',
            ', Async' if self.run_async else '',
        )


================================================
FILE: wxpy/api/messages/messages.py
================================================
# coding: utf-8
from __future__ import unicode_literals
import threading

from wxpy.utils import match_attributes, match_text


class Messages(list):
    """
    多条消息的合集,可用于记录或搜索
    """

    def __init__(self, msg_list=None, max_history=200):
        if msg_list:
            super(Messages, self).__init__(msg_list)
        self.max_history = max_history
        self._thread_lock = threading.Lock()

    def append(self, msg):
        """
        仅当 self.max_history 为 int 类型,且大于 0 时才保存历史消息
        """
        with self._thread_lock:
            if isinstance(self.max_history, int) and self.max_history > 0:
                del self[:-self.max_history + 1]
                return super(Messages, self).append(msg)

    def search(self, keywords=None, **attributes):
        """
        搜索消息记录

        :param keywords: 文本关键词
        :param attributes: 属性键值对
        :return: 所有匹配的消息
        :rtype: :class:`wxpy.Messages`
        """

        def match(msg):
            if not match_text(msg.text, keywords):
                return
            if not match_attributes(msg, **attributes):
                return
            return True

        return Messages(filter(match, self), max_history=self.max_history)


================================================
FILE: wxpy/api/messages/registered.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import weakref

from wxpy.api.consts import SYSTEM


class Registered(list):
    def __init__(self, bot):
        """
        保存当前机器人所有已注册的消息配置

        :param bot: 所属的机器人
        """
        super(Registered, self).__init__()
        self.bot = weakref.proxy(bot)

    def get_config(self, msg):
        """
        获取给定消息的注册配置。每条消息仅匹配一个注册配置,后注册的配置具有更高的匹配优先级。

        :param msg: 给定的消息
        :return: 匹配的回复配置
        """

        for conf in self[::-1]:

            if not conf.enabled or (conf.except_self and msg.sender == self.bot.self):
                continue

            if conf.msg_types and msg.type not in conf.msg_types:
                continue
            elif conf.msg_types is None and msg.type == SYSTEM:
                continue

            if conf.chats is None:
                return conf

            for chat in conf.chats:
                if (isinstance(chat, type) and isinstance(msg.chat, chat)) or chat == msg.chat:
                    return conf

    def get_config_by_func(self, func):
        """
        通过给定的函数找到对应的注册配置

        :param func: 给定的函数
        :return: 对应的注册配置
        """

        for conf in self:
            if conf.func == func:
                return conf

    def _change_status(self, func, enabled):
        if func:
            self.get_config_by_func(func).enabled = enabled
        else:
            for conf in self:
                conf.enabled = enabled

    def enable(self, func=None):
        """
        开启指定函数的对应配置。若不指定函数,则开启所有已注册配置。

        :param func: 指定的函数
        """
        self._change_status(func, True)

    def disable(self, func=None):
        """
        关闭指定函数的对应配置。若不指定函数,则关闭所有已注册配置。

        :param func: 指定的函数
        """
        self._change_status(func, False)

    def _check_status(self, enabled):
        ret = list()
        for conf in self:
            if conf.enabled == enabled:
                ret.append(conf)
        return ret

    @property
    def enabled(self):
        """
        检查处于开启状态的配置

        :return: 处于开启状态的配置
        """
        return self._check_status(True)

    @property
    def disabled(self):
        """
        检查处于关闭状态的配置

        :return: 处于关闭状态的配置
        """
        return self._check_status(False)


================================================
FILE: wxpy/api/messages/sent_message.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging

from wxpy.compatible.utils import force_encoded_string_output
from wxpy.utils import repr_message

logger = logging.getLogger(__name__)


class SentMessage(object):
    """
    程序中通过 .send/reply() 系列方法发出的消息
    
    *使用程序发送的消息也将被记录到历史消息 bot.messages 中*
    """

    def __init__(self, attributes):

        # 消息的类型 (仅可为 'Text', 'Picture', 'Video', 'Attachment')
        self.type = None

        # 消息的服务端 ID
        self.id = None

        # 消息的本地 ID (撤回时需要用到)
        self.local_id = None

        # 消息的文本内容
        self.text = None

        # 消息附件的本地路径
        self.path = None

        # 消息的附件 media_id
        self.media_id = None

        # 本地发送时间
        self.create_time = None

        # 接收服务端响应时间
        self.receive_time = None

        self.receiver = None

        # send_raw_msg 的各属性
        self.raw_type = None
        self.raw_content = None
        self.uri = None
        self.msg_ext = None

        for k, v in attributes.items():
            setattr(self, k, v)

    def __hash__(self):
        return hash((SentMessage, self.id))

    @force_encoded_string_output
    def __repr__(self):
        return repr_message(self)

    def __unicode__(self):
        return repr_message(self)

    @property
    def latency(self):
        """
        消息的延迟秒数 (发送时间和响应时间的差值)
        """
        if self.create_time and self.receive_time:
            return (self.receive_time - self.create_time).total_seconds()

    @property
    def chat(self):
        """
        消息所在的聊天会话 (始终为消息的接受者)
        """
        return self.receiver

    @property
    def member(self):
        """
        若在群聊中发送消息,则为群员
        """
        from wxpy import Group

        if isinstance(self.receiver, Group):
            return self.receiver.self

    @property
    def bot(self):
        """
        消息所属的机器人
        """
        return self.receiver.bot

    @property
    def sender(self):
        """
        消息的发送者
        """
        return self.receiver.bot.self

    def recall(self):
        """
        撤回本条消息 (应为 2 分钟内发出的消息)
        """

        logger.info('recalling msg:\n{}'.format(self))

        from wxpy.utils import BaseRequest
        req = BaseRequest(self.bot, '/webwxrevokemsg')
        req.data.update({
            "ClientMsgId": self.local_id,
            "SvrMsgId": str(self.id),
            "ToUserName": self.receiver.user_name,
        })

        # noinspection PyUnresolvedReferences
        return req.post()


================================================
FILE: wxpy/compatible/__init__.py
================================================
# coding: utf-8
import sys as _sys

PY_VERSION = _sys.version
PY2 = PY_VERSION < '3'

if PY2:
    from future.standard_library import print_function
    from future.builtins import str, int





================================================
FILE: wxpy/compatible/utils.py
================================================
# coding: utf-8
from __future__ import print_function
import warnings as _warnings
import os as _os
import sys as _sys

from tempfile import mkdtemp

import sys

from . import *


class TemporaryDirectory(object):
    """Create and return a temporary directory.  This has the same
    behavior as mkdtemp but can be used as a context manager.  For
    example:

        with TemporaryDirectory() as tmpdir:
            ...

    Upon exiting the context, the directory and everything contained
    in it are removed.
    """

    def __init__(self, suffix="", prefix="tmp", dir=None):
        self._closed = False
        self.name = None  # Handle mkdtemp raising an exception
        self.name = mkdtemp(suffix, prefix, dir)

    def __repr__(self):
        return "<{} {!r}>".format(self.__class__.__name__, self.name)

    def __enter__(self):
        return self.name

    def cleanup(self, _warn=False):
        if self.name and not self._closed:
            try:
                self._rmtree(self.name)
            except (TypeError, AttributeError) as ex:
                # Issue #10188: Emit a warning on stderr
                # if the directory could not be cleaned
                # up due to missing globals
                if "None" not in str(ex):
                    raise
                print("ERROR: {!r} while cleaning up {!r}".format(ex, self, ), file=_sys.stderr)
                return
            self._closed = True
            if _warn:
                self._warn("Implicitly cleaning up {!r}".format(self),
                           ResourceWarning)

    def __exit__(self, exc, value, tb):
        self.cleanup()

    def __del__(self):
        # Issue a ResourceWarning if implicit cleanup needed
        self.cleanup(_warn=True)

    # XXX (ncoghlan): The following code attempts to make
    # this class tolerant of the module nulling out process
    # that happens during CPython interpreter shutdown
    # Alas, it doesn't actually manage it. See issue #10188
    _listdir = staticmethod(_os.listdir)
    _path_join = staticmethod(_os.path.join)
    _isdir = staticmethod(_os.path.isdir)
    _islink = staticmethod(_os.path.islink)
    _remove = staticmethod(_os.remove)
    _rmdir = staticmethod(_os.rmdir)
    _warn = _warnings.warn

    def _rmtree(self, path):
        # Essentially a stripped down version of shutil.rmtree.  We can't
        # use globals because they may be None'ed out at shutdown.
        for name in self._listdir(path):
            fullname = self._path_join(path, name)
            try:
                isdir = self._isdir(fullname) and not self._islink(fullname)
            except OSError:
                isdir = False
            if isdir:
                self._rmtree(fullname)
            else:
                try:
                    self._remove(fullname)
                except OSError:
                    pass
        try:
            self._rmdir(path)
        except OSError:
            pass


def force_encoded_string_output(func):

    if sys.version_info.major < 3:

        def _func(*args, **kwargs):
            return func(*args, **kwargs).encode(sys.stdout.encoding or 'utf-8')

        return _func

    else:
        return func

================================================
FILE: wxpy/exceptions.py
================================================
# coding: utf-8
from __future__ import unicode_literals


class ResponseError(Exception):
    """
    当 BaseResponse 的返回值不为 0 时抛出的异常
    """

    def __init__(self, err_code, err_msg):
        super(ResponseError, self).__init__(
            'err_code: {}; err_msg: {}'.format(err_code, err_msg))
        self.err_code = err_code
        self.err_msg = err_msg


================================================
FILE: wxpy/ext/__init__.py
================================================
from .logging_with_wechat import WeChatLoggingHandler, get_wechat_logger
from .sync_message_in_groups import sync_message_in_groups
from .tuling import Tuling
from .xiaoi import XiaoI


================================================
FILE: wxpy/ext/logging_with_wechat.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging

from wxpy.utils import get_receiver

logger = logging.getLogger(__name__)


class WeChatLoggingHandler(logging.Handler):
    def __init__(self, receiver=None):
        """
        可向指定微信聊天对象发送日志的 Logging Handler

        :param receiver:
            * 当为 `None`, `True` 或字符串时,将以该值作为 `cache_path` 参数启动一个新的机器人,并发送到该机器人的"文件传输助手"
            * 当为 :class:`机器人 <Bot>` 时,将发送到该机器人的"文件传输助手"
            * 当为 :class:`聊天对象 <Chat>` 时,将发送到该聊天对象
        """

        super(WeChatLoggingHandler, self).__init__()
        self.receiver = get_receiver(receiver)

    def emit(self, record):
        if record.name.startswith('wxpy.'):
            # 排除 wxpy 的日志
            return

        # noinspection PyBroadException
        try:
            self.receiver.send(self.format(record))
        except:
            # Todo: 将异常输出到屏幕
            pass


def get_wechat_logger(receiver=None, name=None, level=logging.WARNING):
    """
    获得一个可向指定微信聊天对象发送日志的 Logger

    :param receiver:
        * 当为 `None`, `True` 或字符串时,将以该值作为 `cache_path` 参数启动一个新的机器人,并发送到该机器人的"文件传输助手"
        * 当为 :class:`机器人 <Bot>` 时,将发送到该机器人的"文件传输助手"
        * 当为 :class:`聊天对象 <Chat>` 时,将发送到该聊天对象
    :param name: Logger 名称
    :param level: Logger 等级,默认为 `logging.WARNING`
    :return: Logger
    """

    _logger = logging.getLogger(name=name)
    _logger.setLevel(level=level)
    _logger.addHandler(WeChatLoggingHandler(receiver=receiver))

    return _logger


================================================
FILE: wxpy/ext/sync_message_in_groups.py
================================================
# coding: utf-8
from __future__ import unicode_literals

from binascii import crc32

from wxpy.utils import start_new_thread

emojis = \
    '😀😁😂🤣😃😄😅😆😉😊😋😎😍😘😗😙😚🙂🤗🤔😐😑😶🙄😏😣😥😮🤐😯' \
    '😪😫😴😌🤓😛😜😝🤤😒😓😔😕🙃🤑😲😇🤠🤡🤥😺😸😹😻😼😽🙀😿😾🙈' \
    '🙉🙊🌱🌲🌳🌴🌵🌾🌿🍀🍁🍂🍃🍇🍈🍉🍊🍋🍌🍍🍏🍐🍑🍒🍓🥝🍅🥑🍆🥔' \
    '🥕🌽🥒🍄🥜🌰🍞🥐🥖🥞🧀🍖🍗🥓🍔🍟🍕🌭🌮🌯🥙🥚🍳🥘🍲🥗🍿🍱🍘🍙' \
    '🍚🍛🍜🍝🍠🍢🍣🍤🍥🍡🍦🍧🍨🍩🍪🎂🍰🍫🍬🍭🍮🍯🍼🥛☕🍵🍶🍾🍷🍸' \
    '🍹🍺🍻🥂🥃🍴🥄🔪🏺🌍🌎🌏🌐🗾🌋🗻🏠🏡🏢🏣🏤🏥🏦🏨🏩🏪🏫🏬🏭🏯' \
    '🏰💒🗼🗽⛪🕌🕍🕋⛲⛺🌁🌃🌄🌅🌆🌇🌉🌌🎠🎡🎢💈🎪🎭🎨🎰🚂🚃🚄🚅' \
    '🚆🚇🚈🚉🚊🚝🚞🚋🚌🚍🚎🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚲🛴🛵🚏⛽🚨' \
    '🚥🚦🚧⚓⛵🛶🚤🚢🛫🛬💺🚁🚟🚠🚡🚀🚪🛌🚽🚿🛀🛁⌛⏳⌚⏰🌑🌒🌓🌔' \
    '🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞⭐🌟🌠⛅🌀🌈🌂☔⚡⛄🔥💧🌊🎃🎄🎆🎇✨🎈🎉' \
    '🎊🎋🎍🎎🎏🎐🎑🎁🎫🏆🏅🥇🥈🥉⚽⚾🏀🏐🏈🏉🎾🎱🎳🏏🏑🏒🏓🏸🥊🥋' \
    '🥅🎯⛳🎣🎽🎿🎮🎲🃏🎴🔇🔈🔉🔊📢📣📯🔔🔕🎼🎵🎶🎤🎧📻🎷🎸🎹🎺🎻' \
    '🥁📱📲📞📟📠🔋🔌💻💽💾💿📀🎥🎬📺📷📸📹📼🔍🔎🔬🔭📡💡🔦📔📕📖' \
    '📗📘📙📚📓📒📃📜📄📰📑🔖💰💴💵💶💷💸💳💱💲📧📨📩📤📥📦📫📪📬' \
    '📭📮📝💼📁📂📅📆📇📋📌📍📎📏📐🔒🔓🔏🔐🔑🔨🔫🏹🔧🔩🔗🚬🗿🔮🛒'


def assign_emoji(chat):
    n = crc32(str(chat.wxid or chat.nick_name).encode()) & 0xffffffff
    return emojis[n % len(emojis)]


def forward_prefix(user):
    # represent for avatar
    avatar_repr = assign_emoji(user)
    return '{} · {}'.format(avatar_repr, user.name)


def sync_message_in_groups(
        msg, groups, prefix=None, suffix=None,
        raise_for_unsupported=False, run_async=True
):
    """
    将消息同步到多个微信群中

    支持以下消息类型
        * 文本 (`TEXT`)
        * 视频(`VIDEO`)
        * 文件 (`ATTACHMENT`)
        * 图片/自定义表情 (`PICTURE`)

            * 但不支持表情商店中的表情

        * 名片 (`CARD`)

            * 仅支持公众号名片,以及自己发出的个人号名片

        * 分享 (`SHARING`)

            * 会被转化为 `标题 + 链接` 形式的纯文本

        * 语音 (`RECORDING`)

            * 会以文件方式发送

        * 地图 (`MAP`)
            
            * 会转化为 `位置名称 + 地图链接` 形式的文本消息

    :param Message msg: 需同步的消息对象
    :param Group groups: 需同步的群列表
    :param str prefix:
        * 转发时的 **前缀** 文本,原消息为文本时会自动换行
        * 若不设定,则使用默认前缀作为提示
    :param str suffix:
        * 转发时的 **后缀** 文本,原消息为文本时会自动换行
        * 默认为空
    :param bool raise_for_unsupported:
        | 为 True 时,将为不支持的消息类型抛出 `NotImplementedError` 异常
    :param bool run_async: 是否异步执行,为 True 时不堵塞线程


    ::

        my_groups = [group1, group2, group3 ...]

        @bot.register(my_groups, except_self=False)
        def sync_my_groups(msg):
            sync_message_in_groups(msg, my_groups)

    """

    def process():
        for group in groups:
            if group == msg.chat:
                continue

            msg.forward(
                chat=group, prefix=prefix, suffix=suffix,
                raise_for_unsupported=raise_for_unsupported
            )

    if not prefix:
        prefix = forward_prefix(msg.member)

    if run_async:
        start_new_thread(process, use_caller_name=True)
    else:
        process()


================================================
FILE: wxpy/ext/talk_bot_utils.py
================================================
# coding: utf-8
from __future__ import unicode_literals
import random
import re


def get_context_user_id(msg, max_len=32, re_sub=r'[^a-zA-Z\d]'):
    """
    | 通过消息对象获取 Tuling, XiaoI 等聊天机器人的上下文用户 ID
    | 上下文用户 ID: 为群聊时,取群员的 user_name;非群聊时,取聊天对象的 user_name

    :param msg: 消息对象
    :param max_len: 最大长度 (从末尾截取)
    :param re_sub: 需要移除的字符的正则表达式 (为符合聊天机器人的 API 规范)
    :return: 上下文用户 ID
    """

    from wxpy.api.messages import Message
    from wxpy.api.chats import Group

    # 当 msg 不为消息对象时,返回 None
    if not isinstance(msg, Message):
        return

    if isinstance(msg.sender, Group):
        user = msg.member
    else:
        user = msg.sender

    user_id = re.sub(re_sub, '', user.user_name)

    return user_id[-max_len:]


def next_topic():
    """
    聊天机器人无法获取回复时的备用回复
    """

    return random.choice((
        '换个话题吧',
        '聊点别的吧',
        '下一个话题吧',
        '无言以对呢',
        '这话我接不了呢'
    ))


================================================
FILE: wxpy/ext/tuling.py
================================================
# coding: utf-8
from __future__ import unicode_literals
import logging
import pprint

import requests

from wxpy.ext.talk_bot_utils import get_context_user_id, next_topic
from wxpy.utils.misc import get_text_without_at_bot
from wxpy.utils import enhance_connection
from wxpy.compatible import *

logger = logging.getLogger(__name__)


class Tuling(object):
    """
    与 wxpy 深度整合的图灵机器人
    """

    'API 文档: http://tuling123.com/help/h_cent_webapi.jhtml'

    # 考虑升级 API 版本: http://doc.tuling123.com/openapi2/263611

    url = 'http://www.tuling123.com/openapi/api'

    def __init__(self, api_key=None):
        """
        | 内置的 api key 存在调用限制,建议自行申请。
        | 免费申请: http://www.tuling123.com/

        :param api_key: 你申请的 api key
        """

        self.session = requests.Session()
        enhance_connection(self.session)

        # noinspection SpellCheckingInspection
        self.api_key = api_key or '7c8cdb56b0dc4450a8deef30a496bd4c'
        self.last_member = dict()

    def is_last_member(self, msg):
        if msg.member == self.last_member.get(msg.chat):
            return True
        else:
            self.last_member[msg.chat] = msg.member

    def do_reply(self, msg, at_member=True):
        """
        回复消息,并返回答复文本

        :param msg: Message 对象
        :param at_member: 若消息来自群聊,回复时 @发消息的群成员
        :return: 答复文本
        :rtype: str
        """
        ret = self.reply_text(msg, at_member)
        msg.reply(ret)
        return ret

    def reply_text(self, msg, at_member=True):
        """
        仅返回消息的答复文本

        :param msg: Message 对象
        :param at_member: 若消息来自群聊,回复时 @发消息的群成员
        :return: 答复文本
        :rtype: str
        """

        def process_answer():

            logger.debug('Tuling answer:\n' + pprint.pformat(answer))

            ret = str()
            if at_member:
                if len(msg.chat) > 2 and msg.member.name and not self.is_last_member(msg):
                    ret += '@{} '.format(msg.member.name)

            code = -1
            if answer:
                code = answer.get('code', -1)

            if code >= 100000:
                text = answer.get('text')
                if not text or (text == msg.text and len(text) > 3):
                    text = next_topic()
                url = answer.get('url')
                items = answer.get('list', list())

                ret += str(text)
                if url:
                    ret += '\n{}'.format(url)
                for item in items:
                    ret += '\n\n{}\n{}'.format(
                        item.get('article') or item.get('name'),
                        item.get('detailurl')
                    )

            else:
                ret += next_topic()

            return ret

        def get_location(_chat):

            province = getattr(_chat, 'province', None) or ''
            city = getattr(_chat, 'city', None) or ''

            if province in ('北京', '上海', '天津', '重庆'):
                return '{}市{}区'.format(province, city)
            elif province and city:
                return '{}省{}市'.format(province, city)

        if not msg.bot:
            raise ValueError('bot not found: {}'.format(msg))

        if not msg.text:
            return

        from wxpy.api.chats import Group
        if at_member and isinstance(msg.chat, Group) and msg.member:
            location = get_location(msg.member)
        else:
            # 使该选项失效,防止错误 @ 人
            at_member = False
            location = get_location(msg.chat)

        user_id = get_context_user_id(msg)

        if location:
            location = location[:30]

        info = str(get_text_without_at_bot(msg))[-30:]

        payload = dict(
            key=self.api_key,
            info=info,
            userid=user_id,
            loc=location
        )

        logger.debug('Tuling payload:\n' + pprint.pformat(payload))

        # noinspection PyBroadException
        try:
            r = self.session.post(self.url, json=payload)
            answer = r.json()
        except:
            answer = None
        finally:
            return process_answer()


================================================
FILE: wxpy/ext/xiaoi.py
================================================
# coding: utf-8
from __future__ import unicode_literals
# created by: Han Feng (https://github.com/hanx11)

import collections
import hashlib
import logging

import requests

from wxpy.api.messages import Message
from wxpy.ext.talk_bot_utils import get_context_user_id, next_topic
from wxpy.utils.misc import get_text_without_at_bot
from wxpy.utils import enhance_connection

logger = logging.getLogger(__name__)

from wxpy.compatible import *

class XiaoI(object):
    """
    与 wxpy 深度整合的小 i 机器人
    """

    # noinspection SpellCheckingInspection
    def __init__(self, key, secret):
        """
        | 需要通过注册获得 key 和 secret
        | 免费申请: http://cloud.xiaoi.com/

        :param key: 你申请的 key
        :param secret: 你申请的 secret
        """

        self.key = key
        self.secret = secret

        self.realm = "xiaoi.com"
        self.http_method = "POST"
        self.uri = "/ask.do"
        self.url = "http://nlp.xiaoi.com/ask.do?platform=custom"

        xauth = self._make_http_header_xauth()

        headers = {
            "Content-type": "application/x-www-form-urlencoded",
            "Accept": "text/plain",
        }

        headers.update(xauth)

        self.session = requests.Session()
        self.session.headers.update(headers)
        enhance_connection(self.session)

    def _make_signature(self):
        """
        生成请求签名
        """

        # 40位随机字符
        # nonce = "".join([str(randint(0, 9)) for _ in range(40)])
        nonce = "4103657107305326101203516108016101205331"

        sha1 = "{0}:{1}:{2}".format(self.key, self.realm, self.secret).encode("utf-8")
        sha1 = hashlib.sha1(sha1).hexdigest()
        sha2 = "{0}:{1}".format(self.http_method, self.uri).encode("utf-8")
        sha2 = hashlib.sha1(sha2).hexdigest()

        signature = "{0}:{1}:{2}".format(sha1, nonce, sha2).encode("utf-8")
        signature = hashlib.sha1(signature).hexdigest()

        ret = collections.namedtuple("signature_return", "signature nonce")
        ret.signature = signature
        ret.nonce = nonce

        return ret

    def _make_http_header_xauth(self):
        """
        生成请求认证
        """

        sign = self._make_signature()

        ret = {
            "X-Auth": "app_key=\"{0}\",nonce=\"{1}\",signature=\"{2}\"".format(
                self.key, sign.nonce, sign.signature)
        }

        return ret

    def do_reply(self, msg):
        """
        回复消息,并返回答复文本

        :param msg: Message 对象
        :return: 答复文本
        """

        ret = self.reply_text(msg)
        msg.reply(ret)
        return ret

    def reply_text(self, msg):
        """
        仅返回答复文本

        :param msg: Message 对象,或消息文本
        :return: 答复文本
        """

        error_response = (
            "主人还没给我设置这类话题的回复",
        )

        if isinstance(msg, Message):
            user_id = get_context_user_id(msg)
            question = get_text_without_at_bot(msg)
        else:
            user_id = "abc"
            question = msg or ""

        params = {
            "question": question,
            "format": "json",
            "platform": "custom",
            "userId": user_id,
        }

        resp = self.session.post(self.url, data=params)
        text = resp.text

        for err in error_response:
            if err in text:
                return next_topic()

        return text


================================================
FILE: wxpy/utils/__init__.py
================================================
from .base_request import BaseRequest
from .console import embed, shell_entry
from .misc import decode_text_from_webwx, enhance_connection, enhance_webwx_request, ensure_list, get_receiver, \
    get_text_without_at_bot, get_user_name, handle_response, match_attributes, match_name, match_text, repr_message, \
    smart_map, start_new_thread, wrap_user_name
from .puid_map import PuidMap
from .tools import detect_freq_limit, dont_raise_response_error, ensure_one, mutual_friends


================================================
FILE: wxpy/utils/base_request.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import functools
import json

import itchat.config
import itchat.returnvalues

from .misc import handle_response


class BaseRequest(object):
    def __init__(self, bot, uri, params=None):
        """
        基本的 Web 微信请求模板,可用于修改后发送请求
        
        可修改属性包括:
        
        * url (会通过 url 参数自动拼接好)
        * data (默认仅包含 BaseRequest 部分)
        * headers
        
        :param bot: 所使用的机器人对象
        :param uri: API 路径,将与基础 URL 进行拼接
        """
        self.bot = bot
        self.url = self.bot.core.loginInfo['url'] + uri
        self.params = params
        self.data = {'BaseRequest': self.bot.core.loginInfo['BaseRequest']}
        self.headers = {
            'ContentType': 'application/json; charset=UTF-8',
            'User-Agent': itchat.config.USER_AGENT
        }

        for method in 'get', 'post', 'put', 'delete':
            setattr(self, method, functools.partial(
                self.request, method=method.upper()
            ))

    def request(self, method, to_class=None):
        """
        (在完成修改后) 发送请求
        
        :param method: 请求方法: 'GET', 'POST','PUT', 'DELETE' 等
        :param to_class: 使用 `@handle_response(to_class)` 把结果转化为相应的类
        """

        if self.data:
            self.data = json.dumps(self.data, ensure_ascii=False).encode('utf-8')
        else:
            self.data = None

        @handle_response(to_class)
        def do():
            return itchat.returnvalues.ReturnValue(
                rawResponse=self.bot.core.s.request(
                    method=method,
                    url=self.url,
                    params=self.params,
                    data=self.data,
                    headers=self.headers
                ))

        return do()


================================================
FILE: wxpy/utils/console.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import inspect

from wxpy.compatible import PY2


def _ipython(local, banner):
    from IPython.terminal.embed import InteractiveShellEmbed
    from IPython.terminal.ipapp import load_default_config

    InteractiveShellEmbed.clear_instance()
    shell = InteractiveShellEmbed.instance(
        banner1=banner,
        user_ns=local,
        config=load_default_config()
    )
    shell()


def _bpython(local, banner):
    # noinspection PyUnresolvedReferences,PyPackageRequirements
    import bpython

    bpython.embed(locals_=local, banner=banner)


def _python(local, banner):
    import code

    try:
        # noinspection PyUnresolvedReferences
        import readline
    except ImportError:
        pass
    else:
        import rlcompleter
        readline.parse_and_bind('tab:complete')
    if PY2:
        banner = banner.encode('utf-8')

    code.interact(local=local, banner=banner)


def embed(local=None, banner='', shell=None):
    """
    | 进入交互式的 Python 命令行界面,并堵塞当前线程
    | 支持使用 ipython, bpython 以及原生 python

    :param str shell:
        | 指定命令行类型,可设为 'ipython','bpython','python',或它们的首字母;
        | 若为 `None`,则按上述优先级进入首个可用的 Python 命令行。
    :param dict local: 设定本地变量环境,若为 `None`,则获取进入之前的变量环境。
    :param str banner: 设定欢迎内容,将在进入命令行后展示。
    """

    import inspect

    if not local:
        local = inspect.currentframe().f_back.f_locals

    if isinstance(shell, str):
        shell = shell.strip().lower()
        if shell.startswith('b'):
            shell = _bpython
        elif shell.startswith('i'):
            shell = _ipython
        elif shell.startswith('p') or not shell:
            shell = _python

    for _shell in shell, _ipython, _bpython, _python:
        try:
            _shell(local=local, banner=banner)
        except (TypeError, ImportError):
            continue
        except KeyboardInterrupt:
            break
        else:
            break


def get_arg_parser():
    import argparse

    ap = argparse.ArgumentParser(
        description='Run a wxpy-ready python console.')

    ap.add_argument(
        'bot', type=str, nargs='*',
        help='One or more variable name(s) for bot(s) to init (default: None).')

    ap.add_argument(
        '-c', '--cache', action='store_true',
        help='Cache session(s) for a short time, or load session(s) from cache '
             '(default: disabled).')

    ap.add_argument(
        '-q', '--console_qr', type=int, default=False, metavar='width',
        help='The width for console_qr (default: None).')

    ap.add_argument(
        '-l', '--logging_level', type=str, default='INFO', metavar='level',
        help='Logging level (default: INFO).')

    ap.add_argument(
        '-s', '--shell', type=str, default=None, metavar='shell',
        help='Specify which shell to use: ipython, bpython, or python '
             '(default: the first available).')

    ap.add_argument(
        '-v', '--version', action='store_true',
        help='Show version and exit.')

    return ap


def shell_entry():
    import re

    import logging
    import wxpy

    arg_parser = get_arg_parser()
    args = arg_parser.parse_args()

    if args.bot:
        def get_logging_level():
            logging_level = args.logging_level.upper()
            for level in 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET':
                if level.startswith(logging_level):
                    return getattr(logging, level)
            else:
                return logging.INFO

        logging.basicConfig(level=get_logging_level())

        try:
            bots = dict()
            for name in args.bot:
                if not re.match(r'\w+$', name):
                    continue
                cache_path = 'wxpy_{}.pkl'.format(name) if args.cache else None
                bots[name] = wxpy.Bot(cache_path=cache_path, console_qr=args.console_qr)
        except KeyboardInterrupt:
            return

        banner = 'from wxpy import *\n'

        for k, v in bots.items():
            banner += '{}: {}\n'.format(k, v)

        module_members = dict(inspect.getmembers(wxpy))

        embed(
            local=dict(module_members, **bots),
            banner=banner,
            shell=args.shell
        )
    elif args.version:
        print(wxpy.version_details)
    else:
        arg_parser.print_help()


================================================
FILE: wxpy/utils/misc.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import inspect
import logging
import random
import re
import threading
import weakref
from functools import wraps

import requests
from requests.adapters import HTTPAdapter

from wxpy.compatible import PY2
from wxpy.exceptions import ResponseError

if PY2:
    from future.builtins import str


def decode_text_from_webwx(text):
    """
    解码从 Web 微信获得到的中文乱码

    :param text: 从 Web 微信获得到的中文乱码
    """
    if isinstance(text, str):
        try:
            text = text.encode('raw_unicode_escape').decode()
        except UnicodeDecodeError:
            pass
    return text


def check_response_body(response_body):
    """
    检查 response body: err_code 不为 0 时抛出 :class:`ResponseError` 异常

    :param response_body: response body
    """

    try:
        base_response = response_body['BaseResponse']
        err_code = base_response['Ret']
        err_msg = base_response['ErrMsg']
    except (KeyError, TypeError):
        pass
    else:
        if err_code != 0:
            if int(err_code) > 0:
                err_msg = decode_text_from_webwx(err_msg)
            raise ResponseError(err_code=err_code, err_msg=err_msg)


def handle_response(to_class=None):
    """
    装饰器:检查从 itchat 返回的字典对象,并将其转化为指定类的实例
    若返回值不为0,会抛出 ResponseError 异常

    :param to_class: 需转化成的类,若为None则不转换
    """

    def decorator(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            ret = func(*args, **kwargs)

            if ret is None:
                return

            smart_map(check_response_body, ret)

            if to_class:
                if args:
                    self = args[0]
                else:
                    self = inspect.currentframe().f_back.f_locals.get('self')

                from wxpy.api.bot import Bot
                if isinstance(self, Bot):
                    bot = weakref.proxy(self)
                else:
                    bot = getattr(self, 'bot', None)
                    if not bot:
                        raise ValueError('bot not found:m\nmethod: {}\nself: {}\nbot: {}'.format(
                            func, self, bot
                        ))

                ret = smart_map(to_class, ret, bot)

                if isinstance(ret, list):
                    from wxpy.api.chats import Group
                    if to_class == Group:
                        from wxpy.api.chats import Groups
                        ret = Groups(ret)
                    else:
                        from wxpy.api.chats import Chats
                        ret = Chats(ret, bot)

            return ret

        return wrapped

    return decorator


def ensure_list(x, except_false=True):
    """
    若传入的对象不为列表,则转化为列表

    :param x: 输入对象
    :param except_false: None, False 等例外,会直接返回原值
    :return: 列表,或 None, False 等
    :rtype: list
    """

    if isinstance(x, (list, tuple)) or (not x and except_false):
        return x
    return [x]


def prepare_keywords(keywords):
    """
    准备关键词
    """

    if not keywords:
        keywords = ''
    if isinstance(keywords, str):
        # noinspection PyTypeChecker
        keywords = re.split(r'\s+', keywords)
    return map(lambda x: x.lower(), keywords)


def match_text(text, keywords):
    """
    判断文本内容中是否包含了所有的关键词 (不区分大小写)

    :param text: 文本内容
    :param keywords: 关键词,可以是空白分割的 str,或是多个精准关键词组成的 list
    :return: 若包含了所有的关键词则为 True,否则为 False
    """

    if not text:
        text = ''
    else:
        text = text.lower()

    keywords = prepare_keywords(keywords)

    for kw in keywords:
        if kw not in text:
            return False
    return True


def match_attributes(obj, **attributes):
    """
    判断对象是否匹配输入的属性条件

    :param obj: 对象
    :param attributes: 属性键值对
    :return: 若匹配则为 True,否则为 False
    """

    has_raw = hasattr(obj, 'raw')

    for attr, value in attributes.items():
        if (getattr(obj, attr, None) or (obj.raw.get(attr) if has_raw else None)) != value:
            return False
    return True


def match_name(chat, keywords):
    """
    判断一个 Chat 对象的名称是否包含了所有的关键词 (不区分大小写)

    :param chat: Chat 对象
    :param keywords: 关键词,可以是空白分割的 str,或是多个精准关键词组成的 list
    :return: 若包含了所有的关键词则为 True,否则为 False
    """
    keywords = prepare_keywords(keywords)

    for kw in keywords:
        for attr in 'remark_name', 'display_name', 'nick_name', 'wxid':
            if kw in '{0}'.format(getattr(chat, attr, '')).lower():
                break
        else:
            return False
    return True


def smart_map(func, i, *args, **kwargs):
    """
    将单个对象或列表中的每个项传入给定的函数,并返回单个结果或列表结果,类似于 map 函数

    :param func: 传入到的函数
    :param i: 列表或单个对象
    :param args: func 函数所需的 args
    :param kwargs: func 函数所需的 kwargs
    :return: 若传入的为列表,则以列表返回每个结果,反之为单个结果
    """
    if isinstance(i, (list, tuple, set)):
        return list(map(lambda x: func(x, *args, **kwargs), i))
    else:
        return func(i, *args, **kwargs)


def wrap_user_name(user_or_users):
    """
    确保将用户转化为带有 UserName 键的用户字典

    :param user_or_users: 单个用户,或列表形式的多个用户
    :return: 单个用户字典,或列表形式的多个用户字典
    """

    from wxpy.api.chats import Chat

    def wrap_one(x):
        if isinstance(x, dict):
            return x
        elif isinstance(x, Chat):
            return x.raw
        elif isinstance(x, str):
            return {'UserName': user_or_users}
        else:
            if PY2:
                # noinspection PyUnresolvedReferences
                if isinstance(x, unicode):
                    return {'UserName': user_or_users}
            raise TypeError('Unsupported type: {}'.format(type(x)))

    return smart_map(wrap_one, user_or_users)


def get_user_name(user_or_users):
    """
    确保将用户转化为 user_name 字串

    :param user_or_users: 单个用户,或列表形式的多个用户
    :return: 返回单个 user_name 字串,或列表形式的多个 user_name 字串
    """

    from wxpy.api.chats import Chat

    def get_one(x):
        if isinstance(x, Chat):
            return x.user_name
        elif isinstance(x, dict):
            return x['UserName']
        elif isinstance(x, str):
            return x
        else:
            if PY2:
                # noinspection PyUnresolvedReferences
                if isinstance(x, unicode):
                    return x
            raise TypeError('Unsupported type: {}'.format(type(x)))

    return smart_map(get_one, user_or_users)


def get_receiver(receiver=None):
    """
    获得作为接收者的聊天对象

    :param receiver:
        * 当为 `None`, `True` 或字符串时,将以该值作为 `cache_path` 参数启动一个新的机器人,并返回该机器人的"文件传输助手"
        * 当为 :class:`机器人 <Bot>` 时,将返回该机器人的"文件传输助手"
        * 当为 :class:`聊天对象 <Chat>` 时,将返回该聊天对象
    :return: 作为接收者的聊天对象
    :rtype: :class:`wxpy.Chat`
    """

    from wxpy.api.chats import Chat
    from wxpy.api.bot import Bot

    if isinstance(receiver, Chat):
        return receiver
    elif isinstance(receiver, Bot):
        return receiver.file_helper
    elif receiver in (None, True) or isinstance(receiver, str):
        return Bot(cache_path=receiver).file_helper
    else:
        raise TypeError('expected Chat, Bot, str, True or None')


def enhance_connection(session, pool_connections=30, pool_maxsize=30, max_retries=30):
    """
    增强 requests.Session 对象的网络连接性能

    :param session: 需增强的 requests.Session 对象
    :param pool_connections: 最大的连接池缓存数量
    :param pool_maxsize: 连接池中的最大连接保存数量
    :param max_retries: 最大的连接重试次数 (仅处理 DNS 查询, socket 连接,以及连接超时)
    """

    for p in 'http', 'https':
        session.mount(
            '{}://'.format(p),
            HTTPAdapter(
                pool_connections=pool_connections,
                pool_maxsize=pool_maxsize,
                max_retries=max_retries,
                pool_block=False
            ))


def enhance_webwx_request(bot, sync_check_timeout=(10, 30), webwx_sync_timeout=(10, 20)):
    """
    针对 Web 微信增强机器人的网络请求

    :param bot: 需优化的机器人实例
    :param sync_check_timeout: 请求 "synccheck" 时的超时秒数
    :param webwx_sync_timeout: 请求 "webwxsync" 时的超时秒数
    """

    login_info = bot.core.loginInfo
    session = bot.core.s

    # get: 用于检查是否有新消息
    sync_check_url = '{}/synccheck'.format(login_info.get('syncUrl', login_info['url']))

    # post: 用于获取消息和更新联系人
    webwx_sync_url = '{li[url]}/webwxsync?sid={li[wxsid]}&skey={li[skey]}' \
                     '&pass_ticket={li[pass_ticket]}'.format(li=login_info)

    # noinspection PyProtectedMember
    def customized_request(method, url, **kwargs):
        """
        根据 请求方法 和 url 灵活调整各种参数
        """

        if method.upper() == 'GET':
            if url == sync_check_url:
                # 设置一个超时,避免无尽等待而停止发送心跳,导致出现 1101 错误
                kwargs['timeout'] = sync_check_timeout

                # deviceid 应每次都变化,否则会导致该连接断开不及时,接收消息变慢
                kwargs['params']['deviceid'] = 'e{}'.format(str(random.random())[2:17])

                bot._sync_check_iterations += 1
                kwargs['params']['_'] = bot._sync_check_iterations

        elif method.upper() == 'POST':
            if url == webwx_sync_url:
                # 同上方设置超时
                kwargs['timeout'] = webwx_sync_timeout

        return requests.Session.request(session, method, url, **kwargs)

    session.request = customized_request


def repr_message(msg):
    """
    用于 Message 和 SentMessage 对象的 __repr__ 和 __unicode__
    """

    from wxpy.api.chats import Group

    text = (str(msg.text or '')).replace('\n', ' ↩ ')
    text += ' ' if text else ''

    if msg.sender == msg.bot.self:
        ret = '↪ {self.receiver.name}'
    elif isinstance(msg.chat, Group) and msg.member != msg.receiver:
        ret = '{self.sender.name} › {self.member.name}'
    else:
        ret = '{self.sender.name}'

    ret += ' : {text}({self.type})'

    return ret.format(self=msg, text=text)


def get_text_without_at_bot(msg):
    """
    获得 Message 对象中的消息内容,并清理 @ 机器人的部分

    :param msg: Message 对象
    :return: 清理 @ 机器人部分后的文本内容
    :rtype: str
    """

    from wxpy.api.chats import Group

    text = msg.text

    if isinstance(msg.chat, Group):
        name = msg.chat.self.name
        text = re.sub(r'\s*@' + re.escape(name) + r'\u2005?\s*', '', text)

    return text


def start_new_thread(target, args=(), kwargs=None, daemon=True, use_caller_name=False):
    """
    启动一个新的进程,需要时自动为进程命名,并返回这个线程
    
    :param target: 调用目标
    :param args: 调用位置参数
    :param kwargs: 调用命名参数
    :param daemon: 作为守护进程
    :param use_caller_name: 为 True 则以调用者为名称,否则以目标为名称

    :return: 新的进程
    :rtype: threading.Thread
    """

    if use_caller_name:
        # 使用调用者的名称
        name = inspect.stack()[1][3]
    else:
        name = target.__name__

    logging.getLogger(
        # 使用外层的 logger
        inspect.currentframe().f_back.f_globals.get('__name__')
    ).debug('new thread: {}'.format(name))
    if PY2:
        _thread = threading.Thread(
            target=target, args=args, kwargs=kwargs,
            name=name)
        _thread.setDaemon(daemon)
    else:
        _thread = threading.Thread(
            target=target, args=args, kwargs=kwargs,
            name=name, daemon=daemon
        )
    _thread.start()

    return _thread


================================================
FILE: wxpy/utils/puid_map.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import atexit
import os
import pickle

import threading
from wxpy.compatible import PY2
if PY2:
    from UserDict import UserDict
else:
    from collections import UserDict

"""

# puid

尝试用聊天对象已知的属性,来查找对应的持久固定并且唯一的 用户 id


## 数据结构

PuidMap 中包含 4 个 dict,分别为

1. user_name -> puid
2. wxid -> puid
3. remark_name -> puid
4. caption (昵称, 性别, 省份, 城市) -> puid


## 查询逻辑

当给定一个 Chat 对象,需要获取对应的 puid 时,将按顺序,使用自己的对应属性,轮询以上 4 个 dict

* 若匹配任何一个,则获取到 puid,并将其他属性更新到其他的 dict
* 如果没有一个匹配,则创建一个新的 puid,并加入到以上的 4 个 dict


"""


class PuidMap(object):
    def __init__(self, path):
        """
        用于获取聊天对象的 puid (持续有效,并且稳定唯一的用户ID),和保存映射关系

        :param path: 映射数据的保存/载入路径
        """
        self.path = path

        self.user_names = TwoWayDict()
        self.wxids = TwoWayDict()
        self.remark_names = TwoWayDict()

        self.captions = TwoWayDict()

        self._thread_lock = threading.Lock()

        if os.path.exists(self.path):
            self.load()

        atexit.register(self.dump)

    @property
    def attr_dicts(self):
        return self.user_names, self.wxids, self.remark_names

    def __len__(self):
        return len(self.user_names)

    def __bool__(self):
        return bool(self.path)

    def __nonzero__(self):
        return bool(self.path)

    def get_puid(self, chat):
        """
        获取指定聊天对象的 puid

        :param chat: 指定的聊天对象
        :return: puid
        :rtype: str
        """

        with self._thread_lock:

            if not (chat.user_name and chat.nick_name):
                return

            chat_attrs = (
                chat.user_name,
                chat.wxid,
                getattr(chat, 'remark_name', None),
            )

            chat_caption = get_caption(chat)

            puid = None

            for i in range(3):
                puid = self.attr_dicts[i].get(chat_attrs[i])
                if puid:
                    break
            else:
                if PY2:
                    captions = self.captions.keys()
                else:
                    captions = self.captions
                for caption in captions:
                    if match_captions(caption, chat_caption):
                        puid = self.captions[caption]
                        break

            if puid:
                new_caption = merge_captions(self.captions.get_key(puid), chat_caption)
            else:
                puid = chat.user_name[-8:]
                new_caption = get_caption(chat)

            for i in range(3):
                chat_attr = chat_attrs[i]
                if chat_attr:
                    self.attr_dicts[i][chat_attr] = puid

            self.captions[new_caption] = puid

            return puid

    def dump(self):
        """
        保存映射数据
        """
        with open(self.path, 'wb') as fp:
            pickle.dump((self.user_names, self.wxids, self.remark_names, self.captions), fp)

    def load(self):
        """
        载入映射数据
        """
        with open(self.path, 'rb') as fp:
            self.user_names, self.wxids, self.remark_names, self.captions = pickle.load(fp)


class TwoWayDict(UserDict):
    """
    可双向查询,且 key, value 均为唯一的 dict
    限制: key, value 均须为不可变对象,且不支持 .update() 方法
    """

    def __init__(self):
        if PY2:
            UserDict.__init__(self)
        else:
            super(TwoWayDict, self).__init__()
        self._reversed = dict()

    def get_key(self, value):
        """
        通过 value 查找 key
        """
        return self._reversed.get(value)

    def del_value(self, value):
        """
        删除 value 及对应的 key
        """
        del self[self._reversed[value]]

    def __setitem__(self, key, value):
        if self.get(key) != value:
            if key in self:
                self.del_value(self[key])
            if value in self._reversed:
                del self[self.get_key(value)]
            self._reversed[value] = key
            if PY2:
                return UserDict.__setitem__(self, key, value)
            else:
                return super(TwoWayDict, self).__setitem__(key, value)

    def __delitem__(self, key):
        del self._reversed[self[key]]
        if PY2:
            return UserDict.__delitem__(self, key)
        else:
            return super(TwoWayDict, self).__delitem__(key)

    def update(*args, **kwargs):
        raise NotImplementedError


def get_caption(chat):
    return (
        chat.nick_name,
        getattr(chat, 'sex', None),
        getattr(chat, 'province', None),
        getattr(chat, 'city', None),
    )


def match_captions(old, new):
    if new[0]:
        for i in range(4):
            if old[i] and new[i] and old[i] != new[i]:
                return False
        return True


def merge_captions(old, new):
    return tuple(new[i] or old[i] for i in range(4))


================================================
FILE: wxpy/utils/tools.py
================================================
# coding: utf-8
from __future__ import unicode_literals

import logging
import time
from functools import wraps

from wxpy.exceptions import ResponseError

logger = logging.getLogger(__name__)


def dont_raise_response_error(func):
    """
    装饰器:用于避免被装饰的函数在运行过程中抛出 ResponseError 错误
    """

    @wraps(func)
    def wrapped(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except ResponseError as e:
            logger.warning('{0.__class__.__name__}: {0}'.format(e))

    return wrapped


def ensure_one(found):
    """
    确保列表中仅有一个项,并返回这个项,否则抛出 `ValueError` 异常

    通常可用在查找聊天对象时,确保查找结果的唯一性,并直接获取唯一项

    :param found: 列表
    :return: 唯一项
    """
    if not isinstance(found, list):
        raise TypeError('expected list, {} found'.format(type(found)))
    elif not found:
        raise ValueError('not found')
    elif len(found) > 1:
        raise ValueError('more than one found')
    else:
        return found[0]


def mutual_friends(*args):
    """
    找到多个微信用户的共同好友

    :param args: 每个参数为一个微信用户的机器人(Bot),或是聊天对象合集(Chats)
    :return: 共同好友列表
    :rtype: :class:`wxpy.Chats`
    """

    from wxpy.api.bot import Bot
    from wxpy.api.chats import Chats, User

    class FuzzyUser(User):
        def __init__(self, user):
            super(FuzzyUser, self).__init__(user.raw, user.bot)

        def __hash__(self):
            return hash((self.nick_name, self.sex, self.province, self.city, self.raw['AttrStatus']))

    mutual = set()

    for arg in args:
        if isinstance(arg, Bot):
            friends = map(FuzzyUser, arg.friends())
        elif isinstance(arg, Chats):
            friends = map(FuzzyUser, arg)
        else:
            raise TypeError

        if mutual:
            mutual &= set(friends)
        else:
            mutual.update(friends)

    return Chats(mutual)


def detect_freq_limit(func, *args, **kwargs):
    """
    检测各类 Web 微信操作的频率限制,获得限制次数和周期
    
    :param func: 需要执行的操作函数
    :param args: 操作函数的位置参数
    :param kwargs: 操作函数的命名参数
    :return: 限制次数, 限制周期(秒数)
    """

    start = time.time()
    count = 0

    while True:
        try:
            func(*args, **kwargs)
        except ResponseError as e:
            logger.info('freq limit reached: {} requests passed, error_info: {}'.format(count, e))
            break
        else:
            count += 1
            logger.debug('{} passed'.format(count))

    while True:
        period = time.time() - start
        try:
            func(*args, **kwargs)
        except ResponseError:
            logger.debug('blocking: {:.0f} secs'.format(period))
            time.sleep(1)
        else:
            logger.info('freq limit detected: {} requests / {:.0f} secs'.format(count, period))
            return count, period
Download .txt
gitextract_ckd9lwhg/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── bot.rst
│   ├── chats.rst
│   ├── conf.py
│   ├── console.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── itchat.rst
│   ├── logging_with_wechat.rst
│   ├── make.bat
│   ├── messages.rst
│   ├── response_error.rst
│   └── utils.rst
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── chats/
│   │   │   ├── __init__.py
│   │   │   ├── test_chat.py
│   │   │   ├── test_chats.py
│   │   │   ├── test_friend.py
│   │   │   ├── test_group.py
│   │   │   ├── test_groups.py
│   │   │   ├── test_member.py
│   │   │   ├── test_mp.py
│   │   │   └── test_user.py
│   │   ├── messages/
│   │   │   ├── __init__.py
│   │   │   └── test_message.py
│   │   └── test_bot.py
│   ├── attachments/
│   │   └── file.txt
│   ├── conftest.py
│   ├── ext/
│   │   └── __init__.py
│   └── utils/
│       └── __init__.py
└── wxpy/
    ├── __compat__.py
    ├── __init__.py
    ├── __main__.py
    ├── api/
    │   ├── __init__.py
    │   ├── bot.py
    │   ├── chats/
    │   │   ├── __init__.py
    │   │   ├── chat.py
    │   │   ├── chats.py
    │   │   ├── friend.py
    │   │   ├── group.py
    │   │   ├── groups.py
    │   │   ├── member.py
    │   │   ├── mp.py
    │   │   └── user.py
    │   ├── consts.py
    │   └── messages/
    │       ├── __init__.py
    │       ├── article.py
    │       ├── message.py
    │       ├── message_config.py
    │       ├── messages.py
    │       ├── registered.py
    │       └── sent_message.py
    ├── compatible/
    │   ├── __init__.py
    │   └── utils.py
    ├── exceptions.py
    ├── ext/
    │   ├── __init__.py
    │   ├── logging_with_wechat.py
    │   ├── sync_message_in_groups.py
    │   ├── talk_bot_utils.py
    │   ├── tuling.py
    │   └── xiaoi.py
    └── utils/
        ├── __init__.py
        ├── base_request.py
        ├── console.py
        ├── misc.py
        ├── puid_map.py
        └── tools.py
Download .txt
SYMBOL INDEX (296 symbols across 38 files)

FILE: tests/api/chats/test_chat.py
  class TestChat (line 1) | class TestChat:
    method test_pin_unpin (line 2) | def test_pin_unpin(self, friend):

FILE: tests/api/chats/test_chats.py
  class TestChats (line 6) | class TestChats:
    method test_search (line 7) | def test_search(self, group, friend):
    method test_stats (line 12) | def test_stats(self, group):
    method test_stats_text (line 19) | def test_stats_text(self, group):

FILE: tests/api/chats/test_friend.py
  class TestFriend (line 1) | class TestFriend:

FILE: tests/api/chats/test_group.py
  class TestGroup (line 6) | class TestGroup:
    method test_group_attributes (line 7) | def test_group_attributes(self, group, friend, member):
    method test_update_group (line 16) | def test_update_group(self, group):
    method test_add_members (line 20) | def test_add_members(self, group, member):
    method test_remove_members (line 27) | def test_remove_members(self, member):

FILE: tests/api/chats/test_groups.py
  class TestGroups (line 1) | class TestGroups:
    method test_search (line 2) | def test_search(self, bot, group, member, friend):

FILE: tests/api/chats/test_mp.py
  class TestMP (line 1) | class TestMP:

FILE: tests/api/chats/test_user.py
  class TestUser (line 6) | class TestUser:
    method test_friend_attributes (line 7) | def test_friend_attributes(self, friend):
    method test_accept (line 21) | def test_accept(self, member):
    method test_remark_name (line 25) | def test_remark_name(self, friend, member):

FILE: tests/api/messages/test_message.py
  function sent_message (line 7) | def sent_message(sent_msg, msg_type, receiver):
  class TestMessage (line 18) | class TestMessage:
    method test_text_message (line 19) | def test_text_message(self, group, friend):
    method test_picture_message (line 36) | def test_picture_message(self, group, image_path):
    method test_video_message (line 41) | def test_video_message(self, group, video_path):
    method test_raw_message (line 46) | def test_raw_message(self, group):
    method test_send (line 52) | def test_send(self, friend, file_path, image_path, video_path):

FILE: tests/api/test_bot.py
  class TestBot (line 8) | class TestBot:
    method test_self (line 9) | def test_self(self, bot):
    method test_repr (line 13) | def test_repr(self, bot):
    method test_alive (line 16) | def test_alive(self, bot):
    method test_dump_login_status (line 19) | def test_dump_login_status(self, bot):
    method test_enable_puid (line 24) | def test_enable_puid(self, bot, base_dir):
    method test_chats (line 30) | def test_chats(self, bot):
    method test_friends (line 35) | def test_friends(self, bot):
    method test_groups (line 42) | def test_groups(self, bot):
    method test_mps (line 49) | def test_mps(self, bot):
    method test_search (line 55) | def test_search(self, bot):
    method test_create_group (line 65) | def test_create_group(self, bot):
    method test_upload_file (line 86) | def test_upload_file(self, bot, file_path, friend):

FILE: tests/conftest.py
  function base_dir (line 33) | def base_dir():
  function bot (line 38) | def bot():
  function friend (line 43) | def friend():
  function group (line 57) | def group():
  function shared_dict (line 62) | def shared_dict():
  function member (line 67) | def member():
  function image_path (line 72) | def image_path():
  function file_path (line 77) | def file_path():
  function video_path (line 82) | def video_path():
  function wait_for_message (line 86) | def wait_for_message(chats=None, msg_types=None, except_self=True, timeo...

FILE: wxpy/__init__.py
  class NullHandler (line 82) | class NullHandler(logging.Handler):
    method emit (line 83) | def emit(self, record):

FILE: wxpy/api/bot.py
  class Bot (line 33) | class Bot(object):
    method __init__ (line 49) | def __init__(
    method __repr__ (line 121) | def __repr__(self):
    method __unicode__ (line 124) | def __unicode__(self):
    method logout (line 128) | def logout(self):
    method alive (line 138) | def alive(self):
    method alive (line 146) | def alive(self, value):
    method dump_login_status (line 149) | def dump_login_status(self, cache_path=None):
    method enable_puid (line 155) | def enable_puid(self, path='wxpy_puid.pkl'):
    method except_self (line 180) | def except_self(self, chats_or_dicts):
    method chats (line 192) | def chats(self, update=False):
    method _retrieve_itchat_storage (line 202) | def _retrieve_itchat_storage(self, attr):
    method friends (line 207) | def friends(self, update=False):
    method groups (line 223) | def groups(self, update=False, contact_only=False):
    method mps (line 246) | def mps(self, update=False):
    method user_details (line 262) | def user_details(self, user_or_users, chunk_size=50):
    method search (line 292) | def search(self, keywords=None, **attributes):
    method add_friend (line 312) | def add_friend(self, user, verify_content=''):
    method add_mp (line 331) | def add_mp(self, user):
    method accept_friend (line 348) | def accept_friend(self, user, verify_content=''):
    method create_group (line 375) | def create_group(self, users, topic=None):
    method upload_file (line 407) | def upload_file(self, path):
    method _process_message (line 435) | def _process_message(self, msg):
    method register (line 471) | def register(
    method _listen (line 495) | def _listen(self):
    method start (line 520) | def start(self):
    method stop (line 532) | def stop(self):
    method join (line 543) | def join(self):
    method _cleanup (line 555) | def _cleanup(self):

FILE: wxpy/api/chats/chat.py
  function wrapped_send (line 18) | def wrapped_send(msg_type):
  class Chat (line 82) | class Chat(object):
    method __init__ (line 87) | def __init__(self, raw, bot):
    method puid (line 93) | def puid(self):
    method nick_name (line 116) | def nick_name(self):
    method name (line 128) | def name(self):
    method send (line 139) | def send(self, content=None, media_id=None):
    method send_msg (line 166) | def send_msg(self, msg=None):
    method send_image (line 184) | def send_image(self, path, media_id=None):
    method send_file (line 196) | def send_file(self, path, media_id=None):
    method send_video (line 208) | def send_video(self, path=None, media_id=None):
    method send_raw_msg (line 220) | def send_raw_msg(self, raw_type, raw_content, uri=None, msg_ext=None):
    method mark_as_read (line 270) | def mark_as_read(self):
    method pin (line 294) | def pin(self):
    method unpin (line 302) | def unpin(self):
    method get_avatar (line 310) | def get_avatar(self, save_path=None):
    method uin (line 337) | def uin(self):
    method alias (line 347) | def alias(self):
    method wxid (line 357) | def wxid(self):
    method user_name (line 368) | def user_name(self):
    method __repr__ (line 379) | def __repr__(self):
    method __unicode__ (line 382) | def __unicode__(self):
    method __eq__ (line 385) | def __eq__(self, other):
    method __cmp__ (line 388) | def __cmp__(self, other):
    method __hash__ (line 393) | def __hash__(self):

FILE: wxpy/api/chats/chats.py
  class Chats (line 14) | class Chats(list):
    method __init__ (line 19) | def __init__(self, chat_list=None, source=None):
    method __add__ (line 24) | def __add__(self, other):
    method search (line 27) | def search(self, keywords=None, **attributes):
    method stats (line 52) | def stats(self, attribs=('sex', 'province', 'city')):
    method stats_text (line 70) | def stats_text(self, total=True, sex=True, top_provinces=10, top_citie...
    method add_all (line 138) | def add_all(self, interval=3, verify_content=''):

FILE: wxpy/api/chats/friend.py
  class Friend (line 11) | class Friend(User):

FILE: wxpy/api/chats/group.py
  class Group (line 14) | class Group(Chat):
    method __init__ (line 19) | def __init__(self, raw, bot):
    method members (line 23) | def members(self):
    method __contains__ (line 40) | def __contains__(self, user):
    method __iter__ (line 46) | def __iter__(self):
    method __len__ (line 50) | def __len__(self):
    method search (line 53) | def search(self, keywords=None, **attributes):
    method owner (line 70) | def owner(self):
    method is_owner (line 83) | def is_owner(self):
    method self (line 90) | def self(self):
    method update_group (line 99) | def update_group(self, members_details=False):
    method add_members (line 115) | def add_members(self, users, use_invitation=False):
    method remove_members (line 132) | def remove_members(self, members):
    method rename_group (line 146) | def rename_group(self, name):

FILE: wxpy/api/chats/groups.py
  class Groups (line 8) | class Groups(list):
    method __init__ (line 21) | def __init__(self, group_list=None):
    method search (line 43) | def search(self, keywords=None, users=None, **attributes):

FILE: wxpy/api/chats/member.py
  class Member (line 10) | class Member(User):
    method __init__ (line 15) | def __init__(self, raw, group):
    method group (line 20) | def group(self):
    method display_name (line 27) | def display_name(self):
    method remove (line 33) | def remove(self):
    method name (line 40) | def name(self):

FILE: wxpy/api/chats/mp.py
  class MP (line 6) | class MP(User):

FILE: wxpy/api/chats/user.py
  class User (line 12) | class User(Chat):
    method __init__ (line 17) | def __init__(self, raw, bot):
    method remark_name (line 21) | def remark_name(self):
    method set_remark_name (line 28) | def set_remark_name(self, remark_name):
    method sex (line 40) | def sex(self):
    method province (line 54) | def province(self):
    method city (line 61) | def city(self):
    method signature (line 68) | def signature(self):
    method is_friend (line 75) | def is_friend(self):
    method add (line 89) | def add(self, verify_content=''):
    method accept (line 97) | def accept(self, verify_content=''):

FILE: wxpy/api/messages/article.py
  class Article (line 7) | class Article(object):
    method __init__ (line 8) | def __init__(self):
    method __repr__ (line 23) | def __repr__(self):
    method __unicode__ (line 26) | def __unicode__(self):
    method __hash__ (line 29) | def __hash__(self):
    method __eq__ (line 32) | def __eq__(self, other):
    method __cmp__ (line 35) | def __cmp__(self, other):

FILE: wxpy/api/messages/message.py
  class Message (line 30) | class Message(object):
    method __init__ (line 41) | def __init__(self, raw, bot):
    method __hash__ (line 51) | def __hash__(self):
    method __repr__ (line 55) | def __repr__(self):
    method __unicode__ (line 58) | def __unicode__(self):
    method type (line 64) | def type(self):
    method id (line 96) | def id(self):
    method text (line 104) | def text(self):
    method get_file (line 125) | def get_file(self, save_path=None):
    method file_name (line 141) | def file_name(self):
    method file_size (line 148) | def file_size(self):
    method media_id (line 155) | def media_id(self):
    method is_at (line 164) | def is_at(self):
    method img_height (line 173) | def img_height(self):
    method img_width (line 180) | def img_width(self):
    method play_length (line 187) | def play_length(self):
    method voice_length (line 194) | def voice_length(self):
    method url (line 201) | def url(self):
    method articles (line 212) | def articles(self):
    method card (line 248) | def card(self):
    method create_time (line 259) | def create_time(self):
    method receive_time (line 270) | def receive_time(self):
    method latency (line 277) | def latency(self):
    method location (line 286) | def location(self):
    method chat (line 306) | def chat(self):
    method sender (line 322) | def sender(self):
    method receiver (line 332) | def receiver(self):
    method member (line 342) | def member(self):
    method _get_chat_by_user_name (line 363) | def _get_chat_by_user_name(self, user_name):
    method forward (line 390) | def forward(self, chat, prefix=None, suffix=None, raise_for_unsupporte...

FILE: wxpy/api/messages/message_config.py
  class MessageConfig (line 13) | class MessageConfig(object):
    method __init__ (line 18) | def __init__(
    method enabled (line 35) | def enabled(self):
    method enabled (line 42) | def enabled(self, boolean):
    method __repr__ (line 50) | def __repr__(self):
    method __unicode__ (line 59) | def __unicode__(self):

FILE: wxpy/api/messages/messages.py
  class Messages (line 8) | class Messages(list):
    method __init__ (line 13) | def __init__(self, msg_list=None, max_history=200):
    method append (line 19) | def append(self, msg):
    method search (line 28) | def search(self, keywords=None, **attributes):

FILE: wxpy/api/messages/registered.py
  class Registered (line 9) | class Registered(list):
    method __init__ (line 10) | def __init__(self, bot):
    method get_config (line 19) | def get_config(self, msg):
    method get_config_by_func (line 44) | def get_config_by_func(self, func):
    method _change_status (line 56) | def _change_status(self, func, enabled):
    method enable (line 63) | def enable(self, func=None):
    method disable (line 71) | def disable(self, func=None):
    method _check_status (line 79) | def _check_status(self, enabled):
    method enabled (line 87) | def enabled(self):
    method disabled (line 96) | def disabled(self):

FILE: wxpy/api/messages/sent_message.py
  class SentMessage (line 12) | class SentMessage(object):
    method __init__ (line 19) | def __init__(self, attributes):
    method __hash__ (line 56) | def __hash__(self):
    method __repr__ (line 60) | def __repr__(self):
    method __unicode__ (line 63) | def __unicode__(self):
    method latency (line 67) | def latency(self):
    method chat (line 75) | def chat(self):
    method member (line 82) | def member(self):
    method bot (line 92) | def bot(self):
    method sender (line 99) | def sender(self):
    method recall (line 105) | def recall(self):

FILE: wxpy/compatible/utils.py
  class TemporaryDirectory (line 14) | class TemporaryDirectory(object):
    method __init__ (line 26) | def __init__(self, suffix="", prefix="tmp", dir=None):
    method __repr__ (line 31) | def __repr__(self):
    method __enter__ (line 34) | def __enter__(self):
    method cleanup (line 37) | def cleanup(self, _warn=False):
    method __exit__ (line 54) | def __exit__(self, exc, value, tb):
    method __del__ (line 57) | def __del__(self):
    method _rmtree (line 73) | def _rmtree(self, path):
  function force_encoded_string_output (line 95) | def force_encoded_string_output(func):

FILE: wxpy/exceptions.py
  class ResponseError (line 5) | class ResponseError(Exception):
    method __init__ (line 10) | def __init__(self, err_code, err_msg):

FILE: wxpy/ext/logging_with_wechat.py
  class WeChatLoggingHandler (line 11) | class WeChatLoggingHandler(logging.Handler):
    method __init__ (line 12) | def __init__(self, receiver=None):
    method emit (line 25) | def emit(self, record):
  function get_wechat_logger (line 38) | def get_wechat_logger(receiver=None, name=None, level=logging.WARNING):

FILE: wxpy/ext/sync_message_in_groups.py
  function assign_emoji (line 26) | def assign_emoji(chat):
  function forward_prefix (line 31) | def forward_prefix(user):
  function sync_message_in_groups (line 37) | def sync_message_in_groups(

FILE: wxpy/ext/talk_bot_utils.py
  function get_context_user_id (line 7) | def get_context_user_id(msg, max_len=32, re_sub=r'[^a-zA-Z\d]'):
  function next_topic (line 35) | def next_topic():

FILE: wxpy/ext/tuling.py
  class Tuling (line 16) | class Tuling(object):
    method __init__ (line 27) | def __init__(self, api_key=None):
    method is_last_member (line 42) | def is_last_member(self, msg):
    method do_reply (line 48) | def do_reply(self, msg, at_member=True):
    method reply_text (line 61) | def reply_text(self, msg, at_member=True):

FILE: wxpy/ext/xiaoi.py
  class XiaoI (line 20) | class XiaoI(object):
    method __init__ (line 26) | def __init__(self, key, secret):
    method _make_signature (line 56) | def _make_signature(self):
    method _make_http_header_xauth (line 79) | def _make_http_header_xauth(self):
    method do_reply (line 93) | def do_reply(self, msg):
    method reply_text (line 105) | def reply_text(self, msg):

FILE: wxpy/utils/base_request.py
  class BaseRequest (line 13) | class BaseRequest(object):
    method __init__ (line 14) | def __init__(self, bot, uri, params=None):
    method request (line 41) | def request(self, method, to_class=None):

FILE: wxpy/utils/console.py
  function _ipython (line 9) | def _ipython(local, banner):
  function _bpython (line 22) | def _bpython(local, banner):
  function _python (line 29) | def _python(local, banner):
  function embed (line 46) | def embed(local=None, banner='', shell=None):
  function get_arg_parser (line 83) | def get_arg_parser():
  function shell_entry (line 118) | def shell_entry():

FILE: wxpy/utils/misc.py
  function decode_text_from_webwx (line 22) | def decode_text_from_webwx(text):
  function check_response_body (line 36) | def check_response_body(response_body):
  function handle_response (line 56) | def handle_response(to_class=None):
  function ensure_list (line 108) | def ensure_list(x, except_false=True):
  function prepare_keywords (line 123) | def prepare_keywords(keywords):
  function match_text (line 136) | def match_text(text, keywords):
  function match_attributes (line 158) | def match_attributes(obj, **attributes):
  function match_name (line 175) | def match_name(chat, keywords):
  function smart_map (line 194) | def smart_map(func, i, *args, **kwargs):
  function wrap_user_name (line 210) | def wrap_user_name(user_or_users):
  function get_user_name (line 237) | def get_user_name(user_or_users):
  function get_receiver (line 264) | def get_receiver(receiver=None):
  function enhance_connection (line 289) | def enhance_connection(session, pool_connections=30, pool_maxsize=30, ma...
  function enhance_webwx_request (line 310) | def enhance_webwx_request(bot, sync_check_timeout=(10, 30), webwx_sync_t...
  function repr_message (line 356) | def repr_message(msg):
  function get_text_without_at_bot (line 378) | def get_text_without_at_bot(msg):
  function start_new_thread (line 398) | def start_new_thread(target, args=(), kwargs=None, daemon=True, use_call...

FILE: wxpy/utils/puid_map.py
  class PuidMap (line 43) | class PuidMap(object):
    method __init__ (line 44) | def __init__(self, path):
    method attr_dicts (line 66) | def attr_dicts(self):
    method __len__ (line 69) | def __len__(self):
    method __bool__ (line 72) | def __bool__(self):
    method __nonzero__ (line 75) | def __nonzero__(self):
    method get_puid (line 78) | def get_puid(self, chat):
    method dump (line 131) | def dump(self):
    method load (line 138) | def load(self):
  class TwoWayDict (line 146) | class TwoWayDict(UserDict):
    method __init__ (line 152) | def __init__(self):
    method get_key (line 159) | def get_key(self, value):
    method del_value (line 165) | def del_value(self, value):
    method __setitem__ (line 171) | def __setitem__(self, key, value):
    method __delitem__ (line 183) | def __delitem__(self, key):
    method update (line 190) | def update(*args, **kwargs):
  function get_caption (line 194) | def get_caption(chat):
  function match_captions (line 203) | def match_captions(old, new):
  function merge_captions (line 211) | def merge_captions(old, new):

FILE: wxpy/utils/tools.py
  function dont_raise_response_error (line 13) | def dont_raise_response_error(func):
  function ensure_one (line 28) | def ensure_one(found):
  function mutual_friends (line 47) | def mutual_friends(*args):
  function detect_freq_limit (line 84) | def detect_freq_limit(func, *args, **kwargs):
Condensed preview — 75 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (167K chars).
[
  {
    "path": ".gitignore",
    "chars": 3023,
    "preview": ".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 st"
  },
  {
    "path": "LICENSE",
    "chars": 1096,
    "preview": "**The MIT License**\n\nCopyright 2017 [Youfou](https://github.com/youfou)\n\nPermission is hereby granted, free of charge, t"
  },
  {
    "path": "MANIFEST.in",
    "chars": 27,
    "preview": "include README.rst LICENSE\n"
  },
  {
    "path": "README.rst",
    "chars": 2751,
    "preview": "wxpy: 用 Python 玩微信\n==============================\n\n.. image:: https://badge.fury.io/py/wxpy.svg\n    :target: https://bad"
  },
  {
    "path": "docs/Makefile",
    "chars": 605,
    "preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the shell_entry line.\nSPHINXOPTS    =\n"
  },
  {
    "path": "docs/bot.rst",
    "chars": 2414,
    "preview": "机器人对象\n==============================\n\n..  module:: wxpy\n\n机器人 :class:`Bot` 对象可被理解为一个 Web 微信客户端。\n\n\n..  note::\n\n    | 关于发送消"
  },
  {
    "path": "docs/chats.rst",
    "chars": 3272,
    "preview": "聊天对象\n==============================\n\n..  module:: wxpy\n\n通过机器人对象 :class:`Bot <Bot>` 的\n:meth:`chats() <Bot.chats>`,\n:meth:"
  },
  {
    "path": "docs/conf.py",
    "chars": 5639,
    "preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# wxpy documentation build configuration file, created by\n# sphinx-quic"
  },
  {
    "path": "docs/console.rst",
    "chars": 1544,
    "preview": "愉快的探索和调试\n==============================\n\n..  module:: wxpy\n\n想要做点小试验,调试代码,或是探索 wxpy 的功能特性?反复修改和运行太麻烦。\n\n试试下面两种玩法,告别涂涂改改的摸索"
  },
  {
    "path": "docs/faq.rst",
    "chars": 1275,
    "preview": "必看: 常见问题 FAQ\n==============================\n\n..  module:: wxpy\n\n..  hint::\n\n    这里罗列了一些常见的问题,在提出新的问题前,请先看完本文。\n\n\n启动后马上退出了"
  },
  {
    "path": "docs/index.rst",
    "chars": 2083,
    "preview": ".. wxpy documentation master file, created by\n   sphinx-quickstart on Sat Feb 25 23:57:26 2017.\n   You can adapt this fi"
  },
  {
    "path": "docs/itchat.rst",
    "chars": 830,
    "preview": "itchat 与原始数据\n==============================\n\n..  module:: wxpy\n\n\n正是得益于 |itchat| 的坚实基础,wxpy 才能够在短时间内快速实现这些新的接口和功能。\n\n感谢 it"
  },
  {
    "path": "docs/logging_with_wechat.rst",
    "chars": 1273,
    "preview": "用微信监控你的程序\n==============================\n\n..  module:: wxpy\n\n通过利用微信强大的通知能力,我们可以把程序中的警告/日志发到自己的微信上。\n\nwxpy 提供以下两种方式来实现这个需求"
  },
  {
    "path": "docs/make.bat",
    "chars": 812,
    "preview": "@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=sp"
  },
  {
    "path": "docs/messages.rst",
    "chars": 5017,
    "preview": "消息处理\n==============================\n\n..  module:: wxpy\n\n每当机器人接收到消息时,会自动执行以下两个步骤\n\n1. 将消息保存到 :class:`Bot.messages` 中\n2. 查找"
  },
  {
    "path": "docs/response_error.rst",
    "chars": 1746,
    "preview": "异常处理\n==============================\n\n\n异常的抛出和捕捉\n--------------------\n\n\n..  module:: wxpy\n\n每当使用 wxpy 向微信发出请求 (例如发送消息、加好友、建"
  },
  {
    "path": "docs/utils.rst",
    "chars": 1743,
    "preview": "实用组件\n==============================\n\n..  module:: wxpy\n\n额外内置了一些实用的小组件,可按需使用。\n\n\n聊天机器人\n------------------------------\n\n目前提"
  },
  {
    "path": "requirements.txt",
    "chars": 30,
    "preview": "itchat==1.2.32\nrequests\nfuture"
  },
  {
    "path": "setup.cfg",
    "chars": 46,
    "preview": "[aliases]\nrelease=sdist build egg_info upload\n"
  },
  {
    "path": "setup.py",
    "chars": 1437,
    "preview": "# coding: utf-8\n# from __future__ import unicode_literals\n\nimport re\nimport codecs\n\nfrom setuptools import find_packages"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/chats/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/chats/test_chat.py",
    "chars": 98,
    "preview": "class TestChat:\n    def test_pin_unpin(self, friend):\n        friend.pin()\n        friend.unpin()\n"
  },
  {
    "path": "tests/api/chats/test_chats.py",
    "chars": 579,
    "preview": "from collections import Counter\n\nfrom wxpy import *\n\n\nclass TestChats:\n    def test_search(self, group, friend):\n       "
  },
  {
    "path": "tests/api/chats/test_friend.py",
    "chars": 26,
    "preview": "class TestFriend:\n    pass"
  },
  {
    "path": "tests/api/chats/test_group.py",
    "chars": 855,
    "preview": "import pytest\n\nfrom wxpy import *\n\n\nclass TestGroup:\n    def test_group_attributes(self, group, friend, member):\n       "
  },
  {
    "path": "tests/api/chats/test_groups.py",
    "chars": 185,
    "preview": "class TestGroups:\n    def test_search(self, bot, group, member, friend):\n        found = bot.groups().search(group.name,"
  },
  {
    "path": "tests/api/chats/test_member.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/chats/test_mp.py",
    "chars": 23,
    "preview": "class TestMP:\n    pass\n"
  },
  {
    "path": "tests/api/chats/test_user.py",
    "chars": 976,
    "preview": "import re\n\nfrom wxpy import *\n\n\nclass TestUser:\n    def test_friend_attributes(self, friend):\n        assert isinstance("
  },
  {
    "path": "tests/api/messages/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/messages/test_message.py",
    "chars": 2772,
    "preview": "from datetime import datetime\n\nfrom tests.conftest import wait_for_message\nfrom wxpy import *\n\n\ndef sent_message(sent_ms"
  },
  {
    "path": "tests/api/test_bot.py",
    "chars": 2779,
    "preview": "import logging\nimport os\nimport time\n\nfrom wxpy import *\n\n\nclass TestBot:\n    def test_self(self, bot):\n        assert b"
  },
  {
    "path": "tests/attachments/file.txt",
    "chars": 17,
    "preview": "Hello from wxpy!\n"
  },
  {
    "path": "tests/conftest.py",
    "chars": 2051,
    "preview": "# coding: utf-8\n\n\"\"\"\n部分用例需要与 \"wxpy 机器人\" 进行互动\n\"\"\"\n\nimport os\nimport time\nfrom functools import partial\nfrom queue import "
  },
  {
    "path": "tests/ext/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/utils/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wxpy/__compat__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wxpy/__init__.py",
    "chars": 1983,
    "preview": "#!/usr/bin/env python3\n# coding: utf-8\n\n\"\"\"\n\n\n登陆微信::\n\n    # 导入模块\n    from wxpy import *\n    # 初始化机器人,扫码登陆\n    bot = Bot("
  },
  {
    "path": "wxpy/__main__.py",
    "chars": 94,
    "preview": "# coding: utf-8\n\nfrom .utils import shell_entry\n\nif __name__ == '__main__':\n    shell_entry()\n"
  },
  {
    "path": "wxpy/api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wxpy/api/bot.py",
    "chars": 15975,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport atexit\nimport functools\nimport logging\nimport os.path\nim"
  },
  {
    "path": "wxpy/api/chats/__init__.py",
    "chars": 196,
    "preview": "from .chat import Chat\nfrom .chats import Chats\nfrom .friend import Friend\nfrom .group import Group\nfrom .groups import "
  },
  {
    "path": "wxpy/api/chats/chat.py",
    "chars": 10413,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport datetime\nimport logging\nimport re\nimport time\nfrom funct"
  },
  {
    "path": "wxpy/api/chats/chats.py",
    "chars": 4444,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport time\nfrom collections import Counter\n\nfro"
  },
  {
    "path": "wxpy/api/chats/friend.py",
    "chars": 191,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom .user import User\n\nlogger = logging.getLog"
  },
  {
    "path": "wxpy/api/chats/group.py",
    "chars": 4392,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import ensure_list, get_user_na"
  },
  {
    "path": "wxpy/api/chats/groups.py",
    "chars": 2182,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom wxpy.utils import ensure_list, match_attributes, match_nam"
  },
  {
    "path": "wxpy/api/chats/member.py",
    "chars": 1068,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom .user import User\n\n\n# Todo: 若尝试获取群成员信息时为空,自动更新成员信息 (并要照顾到遍"
  },
  {
    "path": "wxpy/api/chats/mp.py",
    "chars": 93,
    "preview": "# 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",
    "chars": 2178,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import handle_response\nfrom .ch"
  },
  {
    "path": "wxpy/api/consts.py",
    "chars": 342,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n# 文本\nTEXT = 'Text'\n# 位置\nMAP = 'Map'\n# 名片\nCARD = 'Card'\n# 提示\nNOTE"
  },
  {
    "path": "wxpy/api/messages/__init__.py",
    "chars": 204,
    "preview": "from .article import Article\nfrom .message import Message\nfrom .message_config import MessageConfig\nfrom .messages impor"
  },
  {
    "path": "wxpy/api/messages/article.py",
    "chars": 825,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom wxpy.compatible.utils import force_encoded_string_output\n\n"
  },
  {
    "path": "wxpy/api/messages/message.py",
    "chars": 14509,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport os\nimport tempfile\nimport weakref\nfrom da"
  },
  {
    "path": "wxpy/api/messages/message_config.py",
    "chars": 1565,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport weakref\n\nfrom wxpy.compatible.utils impor"
  },
  {
    "path": "wxpy/api/messages/messages.py",
    "chars": 1217,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport threading\n\nfrom wxpy.utils import match_attributes, match"
  },
  {
    "path": "wxpy/api/messages/registered.py",
    "chars": 2288,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport weakref\n\nfrom wxpy.api.consts import SYSTEM\n\n\nclass Regi"
  },
  {
    "path": "wxpy/api/messages/sent_message.py",
    "chars": 2511,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.compatible.utils import force_encoded"
  },
  {
    "path": "wxpy/compatible/__init__.py",
    "chars": 193,
    "preview": "# coding: utf-8\nimport sys as _sys\n\nPY_VERSION = _sys.version\nPY2 = PY_VERSION < '3'\n\nif PY2:\n    from future.standard_l"
  },
  {
    "path": "wxpy/compatible/utils.py",
    "chars": 3213,
    "preview": "# coding: utf-8\nfrom __future__ import print_function\nimport warnings as _warnings\nimport os as _os\nimport sys as _sys\n\n"
  },
  {
    "path": "wxpy/exceptions.py",
    "chars": 361,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\n\nclass ResponseError(Exception):\n    \"\"\"\n    当 BaseResponse 的返回"
  },
  {
    "path": "wxpy/ext/__init__.py",
    "chars": 184,
    "preview": "from .logging_with_wechat import WeChatLoggingHandler, get_wechat_logger\nfrom .sync_message_in_groups import sync_messag"
  },
  {
    "path": "wxpy/ext/logging_with_wechat.py",
    "chars": 1488,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\n\nfrom wxpy.utils import get_receiver\n\nlogger = l"
  },
  {
    "path": "wxpy/ext/sync_message_in_groups.py",
    "chars": 2562,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nfrom binascii import crc32\n\nfrom wxpy.utils import start_new_th"
  },
  {
    "path": "wxpy/ext/talk_bot_utils.py",
    "chars": 918,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport random\nimport re\n\n\ndef get_context_user_id(msg, max_len=3"
  },
  {
    "path": "wxpy/ext/tuling.py",
    "chars": 4112,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\nimport logging\nimport pprint\n\nimport requests\n\nfrom wxpy.ext.tal"
  },
  {
    "path": "wxpy/ext/xiaoi.py",
    "chars": 3346,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n# created by: Han Feng (https://github.com/hanx11)\n\nimport colle"
  },
  {
    "path": "wxpy/utils/__init__.py",
    "chars": 481,
    "preview": "from .base_request import BaseRequest\nfrom .console import embed, shell_entry\nfrom .misc import decode_text_from_webwx, "
  },
  {
    "path": "wxpy/utils/base_request.py",
    "chars": 1778,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport functools\nimport json\n\nimport itchat.config\nimport itcha"
  },
  {
    "path": "wxpy/utils/console.py",
    "chars": 4368,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport inspect\n\nfrom wxpy.compatible import PY2\n\n\ndef _ipython("
  },
  {
    "path": "wxpy/utils/misc.py",
    "chars": 11048,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport inspect\nimport logging\nimport random\nimport re\nimport th"
  },
  {
    "path": "wxpy/utils/puid_map.py",
    "chars": 4858,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport atexit\nimport os\nimport pickle\n\nimport threading\nfrom wx"
  },
  {
    "path": "wxpy/utils/tools.py",
    "chars": 2758,
    "preview": "# coding: utf-8\nfrom __future__ import unicode_literals\n\nimport logging\nimport time\nfrom functools import wraps\n\nfrom wx"
  }
]

About this extraction

This page contains the full source code of the youfou/wxpy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 75 files (151.7 KB), approximately 45.3k tokens, and a symbol index with 296 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!