The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .codeclimate.yml
├── .coveragerc
├── .editorconfig
├── .github
    ├── ISSUE_TEMPLATE
    │   ├── Bug_report.md
    │   └── Feature_request.md
    ├── PULL_REQUEST_TEMPLATE.md
    ├── dependabot.yml
    ├── issue-close-app.yml
    └── workflows
    │   └── test.yml
├── .gitignore
├── .gitmodules
├── .pipignore
├── .scrutinizer.yml
├── .style.yapf
├── .vscode
    └── settings.json
├── LICENSE
├── MANIFEST.in
├── README.rst
├── artwork
    └── logo.svg
├── bors.toml
├── dev-requirements.txt
├── docs-requirements.txt
├── docs
    ├── .gitignore
    ├── Makefile
    ├── _static
    │   ├── logo.png
    │   └── qq.png
    ├── _themes
    │   ├── LICENSE
    │   ├── README.rst
    │   ├── flask
    │   │   ├── layout.html
    │   │   ├── relations.html
    │   │   ├── sidebarintro.html
    │   │   ├── static
    │   │   │   └── flasky.css_t
    │   │   └── theme.conf
    │   └── flask_theme_support.py
    ├── api.rst
    ├── changelog.rst
    ├── client.rst
    ├── conf.py
    ├── config.rst
    ├── contrib.rst
    ├── contribution-guide.rst
    ├── deploy.rst
    ├── encryption.rst
    ├── error-page.rst
    ├── events.rst
    ├── handlers.rst
    ├── index.rst
    ├── make.bat
    ├── messages.rst
    ├── replies.rst
    ├── session.rst
    ├── start.rst
    └── utils.rst
├── example
    └── hello_world.py
├── pytest.ini
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
    ├── client_config.py
    ├── django_test_env
    │   ├── django_test
    │   │   ├── __init__.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   └── manage.py
    ├── messages
    │   └── test_entries.py
    ├── test_client.py
    ├── test_config.py
    ├── test_contrib.py
    ├── test_crypto.py
    ├── test_handler.py
    ├── test_logger.py
    ├── test_parser.py
    ├── test_replies.py
    ├── test_robot.py
    ├── test_session.py
    └── test_utils.py
├── tox-requirements.txt
├── tox.ini
└── werobot
    ├── __init__.py
    ├── client.py
    ├── config.py
    ├── contrib
        ├── __init__.py
        ├── bottle.py
        ├── django.py
        ├── error.html
        ├── flask.py
        └── tornado.py
    ├── crypto
        ├── __init__.py
        ├── exceptions.py
        └── pkcs7.py
    ├── exceptions.py
    ├── logger.py
    ├── messages
        ├── __init__.py
        ├── base.py
        ├── entries.py
        ├── events.py
        └── messages.py
    ├── parser.py
    ├── pay.py
    ├── replies.py
    ├── robot.py
    ├── session
        ├── __init__.py
        ├── filestorage.py
        ├── mongodbstorage.py
        ├── mysqlstorage.py
        ├── postgresqlstorage.py
        ├── redisstorage.py
        ├── saekvstorage.py
        └── sqlitestorage.py
    ├── testing.py
    └── utils.py


/.codeclimate.yml:
--------------------------------------------------------------------------------
 1 | engines:
 2 |   duplication:
 3 |     enabled: true
 4 |     config:
 5 |       languages:
 6 |       - python
 7 |   fixme:
 8 |     enabled: true
 9 |   markdownlint:
10 |     enabled: true
11 |   pep8:
12 |     enabled: true
13 | ratings:
14 |   paths:
15 |   - "**.py"
16 | exclude_paths:
17 | - docs/
18 | - travis/
19 | - tests/


--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
 1 | [run]
 2 | source = werobot
 3 | 
 4 | [report]
 5 | exclude_lines =
 6 |     pragma: no cover
 7 | 
 8 |     def __repr__
 9 | 
10 |     raise AssertionError
11 |     raise NotImplementedError
12 | 
13 |     if 0:
14 |     if __name__ == .__main__.:
15 | 


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | indent_size = 4
 5 | indent_style = space
 6 | end_of_line = lf
 7 | insert_final_newline = true
 8 | 
 9 | [*.{yml,ini}]
10 | indent_size = 2
11 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug Report / Bug 反馈
 3 | about: 反馈 WeRoBot 的 bug
 4 | 
 5 | ---
 6 | 
 7 | <!--
 8 | 请注意:不规范的问题会被部署的 issue bot 自动关闭。
 9 | Issue tracker 只用于反馈 bug 和 feature request。
10 | 与 WeRoBot 用法相关的问题请加入主页上的 QQ 群讨论或在 SegmentFault 社区提问。
11 | 在提问前请仔细阅读我们的文档,并推荐阅读《提问的智慧》:
12 | https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md
13 | -->
14 | * **对 Bug 的描述**
15 |   * 当前行为:
16 |   * 正确的行为:
17 | 
18 | * **环境**
19 |   * 平台:
20 |   * WeRoBot 版本号:
21 |   * Python 版本:
22 | 
23 | * **复现代码或 repo 链接**
24 | 
25 | ```python
26 | from werobot import WeRoBot
27 | 
28 | # 请在这里给出 bug 的复现代码。如有必要,可以创建一个复现 repo 并将链接粘贴到这里。
29 | ```
30 | 
31 | * **复现步骤**
32 | 
33 | * **其他信息**
34 | <!-- 如对 bug 修复的建议、相关 issue 或 PR 的引用等信息 -->
35 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: 为 WeRoBot 的功能提出建议
 4 | 
 5 | ---
 6 | 
 7 | <!--
 8 | Issue tracker 只用于反馈 bug 和 feature request 。
 9 | 与 WeRoBot 用法相关的问题请加入主页上的 QQ 群讨论或在 SegmentFault 社区提问。
10 | 在提问前请仔细阅读我们的文档,并推荐阅读《提问的智慧》:
11 | https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md
12 | -->
13 | 
14 | * **遇到的问题**
15 | <!-- feature 总是要解决问题,来讲讲遇到的问题吧 -->
16 | 
17 | * **解决方案**
18 | <!-- 描述为了解决这个问题需要 WeRoBot 添加的 feature -->
19 | 
20 | * **其他内容**
21 | <!-- 如相关 issue 或 PR 的引用等信息 -->
22 | 


--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | <!--
2 | 在发布 Pull Request 之前,请花一点时间读一下我们的贡献指南:
3 | https://werobot.readthedocs.io/zh_CN/master/contribution-guide.html
4 | -->
5 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
 1 | version: 2
 2 | updates:
 3 | - package-ecosystem: pip
 4 |   directory: "/"
 5 |   schedule:
 6 |     interval: weekly
 7 |     time: "07:00"
 8 |   open-pull-requests-limit: 10
 9 |   ignore:
10 |   - dependency-name: multipart
11 |     versions:
12 |     - "> 0.1"
13 |   - dependency-name: redis
14 |     versions:
15 |     - "< 2.10.7, >= 2.10.6.a"
16 |   - dependency-name: yapf
17 |     versions:
18 |     - ">= 0.26.a, < 0.27"
19 |   - dependency-name: responses
20 |     versions:
21 |     - 0.13.1
22 |   - dependency-name: sphinx
23 |     versions:
24 |     - 3.5.0
25 |     - 3.5.1
26 |     - 3.5.2
27 |     - 3.5.3
28 |   - dependency-name: tox
29 |     versions:
30 |     - 3.21.3
31 |     - 3.21.4
32 |     - 3.22.0
33 |   - dependency-name: cryptography
34 |     versions:
35 |     - 3.4.1
36 |   - dependency-name: coverage
37 |     versions:
38 |     - "5.4"
39 | 


--------------------------------------------------------------------------------
/.github/issue-close-app.yml:
--------------------------------------------------------------------------------
 1 | comment: "> Issue tracker 只用于反馈 bug 和 feature request。在提问前请仔细阅读我们的文档,并推荐阅读《提问的智慧》:https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md"
 2 | issueConfigs:
 3 | - content:
 4 |   - 对 Bug 的描述
 5 |   - 环境
 6 |   - 复现代码或 repo 链接
 7 |   - 复现步骤
 8 |   - 其他信息
 9 | - content:
10 |   - 遇到的问题
11 |   - 解决方案
12 |   - 其他内容


--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
 1 | name: tests
 2 | 
 3 | on: [push, pull_request]
 4 | 
 5 | jobs:
 6 |   test:
 7 |     runs-on: ubuntu-latest
 8 |     strategy:
 9 |       matrix:
10 |         os: [ubuntu-latest, macos-latest, windows-latest]
11 |         python-version: [3.6, 3.7, 3.8, 3.9]
12 |     steps:
13 |       - uses: actions/checkout@v2
14 |       - name: Set up Python ${{ matrix.python-version }}
15 |         uses: actions/setup-python@v1
16 |         with:
17 |           python-version: ${{ matrix.python-version }}
18 |       - uses: actions/cache@v1
19 |         with:
20 |           path: ~/.cache/pip
21 |           key: ${{ matrix.os }}-pip-${{ matrix.python-version }}
22 |           restore-keys: |
23 |             ${{ matrix.os }}-pip-
24 |       - uses: actions/cache@v1
25 |         with:
26 |           path: .tox
27 |           key: ${{ matrix.os }}-tox-${{ matrix.python-version }}-${{ hashFiles('**/tox.ini') }}
28 |           restore-keys: |
29 |             ${{ matrix.os }}-tox-${{ matrix.python-version }}
30 |       - name: Install dependencies
31 |         run: |
32 |           python -m pip install --upgrade pip wheel
33 |           pip install -r dev-requirements.txt
34 |       - name: Test with tox
35 |         run: |
36 |           tox -s
37 |         env:
38 |           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
39 |   lint:
40 |     runs-on: ubuntu-latest
41 |     steps:
42 |       - uses: actions/checkout@v2
43 |       - name: Set up Python
44 |         uses: actions/setup-python@v1
45 |       - uses: actions/cache@v1
46 |         with:
47 |           path: ~/.cache/pip
48 |           key: ${{ runner.os }}-pip-lint-${{ hashFiles('**/*requirements.txt') }}
49 |           restore-keys: |
50 |             ${{ runner.os }}-pip-
51 |       - name: Install dependencies
52 |         run: |
53 |           python -m pip install --upgrade pip wheel
54 |           cat dev-requirements.txt | grep yapf== | xargs pip install
55 |       - name: Run yapf
56 |         run: |
57 |           yapf -p -r -d docs/ werobot/ tests/ *.py
58 |   docs:
59 |     runs-on: ubuntu-latest
60 |     steps:
61 |       - uses: actions/checkout@v2
62 |       - name: Set up Python
63 |         uses: actions/setup-python@v1
64 |       - uses: actions/cache@v1
65 |         with:
66 |           path: ~/.cache/pip
67 |           key: ${{ runner.os }}-pip-docs-${{ hashFiles('**/*requirements.txt') }}
68 |           restore-keys: |
69 |             ${{ runner.os }}-pip-
70 |       - name: Install dependencies
71 |         run: |
72 |           python -m pip install --upgrade pip wheel
73 |           pip install -r docs-requirements.txt
74 |       - name: Build docs
75 |         run: |
76 |           sphinx-build -W docs docs/_build/html
77 |       - uses: actions/upload-artifact@v1
78 |         with:
79 |           name: docs
80 |           path: docs/_build/html
81 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | *.py[cod]
 2 | .python-version
 3 | 
 4 | # C extensions
 5 | *.so
 6 | 
 7 | # Packages
 8 | *.egg
 9 | *.egg-info
10 | dist
11 | build
12 | eggs
13 | .eggs/
14 | parts
15 | bin
16 | var
17 | sdist
18 | develop-eggs
19 | .installed.cfg
20 | lib
21 | lib64
22 | 
23 | # Installer logs
24 | pip-log.txt
25 | 
26 | # Unit test / coverage reports
27 | .coverage
28 | .tox
29 | nosetests.xml
30 | .cache/
31 | cover/
32 | htmlcov/
33 | 
34 | # Translations
35 | *.mo
36 | 
37 | # Emacs
38 | .#*
39 | 
40 | # Mr Developer
41 | .mr.developer.cfg
42 | .project
43 | .pydevproject
44 | 
45 | .DS_Store
46 | .idea
47 | 
48 | #Sphinx
49 | docs/_
50 | 
51 | #VirtualEnv
52 | .Python
53 | include/
54 | Scripts/
55 | pyvenv.cfg
56 | 
57 | werobot_session.db
58 | 
59 | .coveralls.yml
60 | 
61 | werobot_session*
62 | pip-selfcheck.json
63 | venv
64 | 
65 | *.sqlite3
66 | .pytest_cache
67 | 


--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/offu/WeRoBot/7b3d62d7ae126fc8bc9e033e6358dec2852d1d5a/.gitmodules


--------------------------------------------------------------------------------
/.pipignore:
--------------------------------------------------------------------------------
1 | WeRoBot
2 | distribute


--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 |     python:
3 |         code_rating: true
4 |         duplicate_code: true
5 | filter:
6 |     excluded_paths:
7 |         - tests/*
8 |         - travis/*
9 |         - example/*


--------------------------------------------------------------------------------
/.style.yapf:
--------------------------------------------------------------------------------
 1 | [style]
 2 | based_on_style = pep8
 3 | align_closing_bracket_with_visual_indent = true
 4 | allow_multiline_dictionary_keys = false
 5 | allow_split_before_dict_value = false
 6 | dedent_closing_brackets = true
 7 | each_dict_entry_on_separate_line = true
 8 | coalesce_brackets = false
 9 | spaces_before_comment = 2
10 | split_before_logical_operator = true
11 | 


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 |     "python.linting.flake8Enabled": false,
3 |     "python.formatting.provider": "yapf",
4 |     "python.linting.pylintEnabled": false,
5 |     "python.testing.pytestEnabled": true,
6 |     "python.pythonPath": "venv/bin/python3",
7 |     "restructuredtext.confPath": "${workspaceFolder}/docs"
8 | }
9 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 whtsky
2 | 
3 | 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:
4 | 
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | 
7 | 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.


--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include requirements.txt
2 | include README.rst
3 | include LICENSE
4 | include werobot/contrib/error.html
5 | recursive-include tests *
6 | recursive-exclude tests *.pyc
7 | recursive-exclude tests *.pyo
8 | 


--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
 1 | ====================================
 2 | WeRoBot
 3 | ====================================
 4 | 
 5 | .. image:: https://github.com/offu/werobot/workflows/tests/badge.svg
 6 |     :target: https://github.com/offu/werobot/actions
 7 | .. image:: https://codecov.io/gh/offu/WeRoBot/branch/master/graph/badge.svg
 8 |     :target: https://codecov.io/gh/offu/WeRoBot
 9 | .. image:: https://img.shields.io/badge/QQ%20Group-283206829-brightgreen.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0yNzAgODA2cS04LTE5LTgtNTIgMC0yMCAxMS00OXQyNC00NXEtMS0yMiA3LjUtNTN0MjIuNS00M3EwLTEzOSA5Mi41LTI4OC41dDIxNy41LTIwOS41cTEzOS02NiAzMjQtNjYgMTMzIDAgMjY2IDU1IDQ5IDIxIDkwIDQ4dDcxIDU2IDU1IDY4IDQyIDc0IDMyLjUgODQuNSAyNS41IDg5LjUgMjIgOThsMSA1cTU1IDgzIDU1IDE1MCAwIDE0LTkgNDB0LTkgMzhxMCAxIDEuNSAzLjV0My41IDUgMiAzLjVxNzcgMTE0IDEyMC41IDIxNC41dDQzLjUgMjA4LjVxMCA0My0xOS41IDEwMHQtNTUuNSA1N3EtOSAwLTE5LjUtNy41dC0xOS0xNy41LTE5LTI2LTE2LTI2LjUtMTMuNS0yNi05LTE3LjVxLTEtMS0zLTFsLTUgNHEtNTkgMTU0LTEzMiAyMjMgMjAgMjAgNjEuNSAzOC41dDY5IDQxLjUgMzUuNSA2NXEtMiA0LTQgMTZ0LTcgMThxLTY0IDk3LTMwMiA5Ny01MyAwLTExMC41LTl0LTk4LTIwLTEwNC41LTMwcS0xNS01LTIzLTctMTQtNC00Ni00LjV0LTQwLTEuNXEtNDEgNDUtMTI3LjUgNjV0LTE2OC41IDIwcS0zNSAwLTY5LTEuNXQtOTMtOS0xMDEtMjAuNS03NC41LTQwLTMyLjUtNjRxMC00MCAxMC01OS41dDQxLTQ4LjVxMTEtMiA0MC41LTEzdDQ5LjUtMTJxNCAwIDE0LTIgMi0yIDItNGwtMi0zcS00OC0xMS0xMDgtMTA1LjV0LTczLTE1Ni41bC01LTNxLTQgMC0xMiAyMC0xOCA0MS01NC41IDc0LjV0LTc3LjUgMzcuNWgtMXEtNCAwLTYtNC41dC01LTUuNXEtMjMtNTQtMjMtMTAwIDAtMjc1IDI1Mi00NjZ6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B
10 |     :target: https://jq.qq.com/?_wv=1027&k=449sXsV
11 | 
12 | WeRoBot 是一个微信公众号开发框架,采用MIT协议发布。
13 | 
14 | 文档在这里: https://werobot.readthedocs.org/zh_CN/latest/
15 | 
16 | 安装
17 | ========
18 | 
19 | 推荐使用 pip 进行安装 ::
20 | 
21 |     pip install werobot
22 | 
23 | Hello World
24 | =============
25 | 
26 | 一个非常简单的 Hello World 微信公众号,会对收到的所有文本消息回复 Hello World ::
27 | 
28 |     import werobot
29 | 
30 |     robot = werobot.WeRoBot(token='tokenhere')
31 | 
32 |     @robot.text
33 |     def hello_world():
34 |         return 'Hello World!'
35 | 
36 |     robot.run()
37 |     
38 | Credits 
39 | =======
40 | Contributors
41 | -----------------
42 | Thank you to all the people who have already contributed. 
43 | |occontributorimage|
44 | 
45 | .. |occontributorimage| image:: https://opencollective.com/werobot/contributors.svg?width=890&button=false
46 |     :target: https://opencollective.com/werobot
47 |     :alt: Repo Contributors
48 | 


--------------------------------------------------------------------------------
/bors.toml:
--------------------------------------------------------------------------------
1 | status = [
2 |   "continuous-integration/travis-ci/push",
3 |   "continuous-integration/appveyor/branch",
4 |   "ci/circleci",
5 | ]
6 | 
7 | # 4 hours
8 | timeout-sec = 14400
9 | 


--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -r tox-requirements.txt
2 | -r docs-requirements.txt
3 | yapf==0.30.0
4 | tox==3.24.5
5 | Django
6 | 


--------------------------------------------------------------------------------
/docs-requirements.txt:
--------------------------------------------------------------------------------
1 | -r tox-requirements.txt
2 | Sphinx==4.4.0
3 | Django
4 | 


--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build/
2 | 


--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
  1 | # Makefile for Sphinx documentation
  2 | #
  3 | 
  4 | # You can set these variables from the command line.
  5 | SPHINXOPTS    =
  6 | SPHINXBUILD   = sphinx-build
  7 | PAPER         =
  8 | BUILDDIR      = _build
  9 | 
 10 | # Internal variables.
 11 | PAPEROPT_a4     = -D latex_paper_size=a4
 12 | PAPEROPT_letter = -D latex_paper_size=letter
 13 | ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 14 | # the i18n builder cannot share the environment and doctrees with the others
 15 | I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 16 | 
 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
 18 | 
 19 | help:
 20 | 	@echo "Please use \`make <target>' where <target> is one of"
 21 | 	@echo "  html       to make standalone HTML files"
 22 | 	@echo "  dirhtml    to make HTML files named index.html in directories"
 23 | 	@echo "  singlehtml to make a single large HTML file"
 24 | 	@echo "  pickle     to make pickle files"
 25 | 	@echo "  json       to make JSON files"
 26 | 	@echo "  htmlhelp   to make HTML files and a HTML help project"
 27 | 	@echo "  qthelp     to make HTML files and a qthelp project"
 28 | 	@echo "  devhelp    to make HTML files and a Devhelp project"
 29 | 	@echo "  epub       to make an epub"
 30 | 	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 31 | 	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
 32 | 	@echo "  text       to make text files"
 33 | 	@echo "  man        to make manual pages"
 34 | 	@echo "  texinfo    to make Texinfo files"
 35 | 	@echo "  info       to make Texinfo files and run them through makeinfo"
 36 | 	@echo "  gettext    to make PO message catalogs"
 37 | 	@echo "  changes    to make an overview of all changed/added/deprecated items"
 38 | 	@echo "  linkcheck  to check all external links for integrity"
 39 | 	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
 40 | 
 41 | clean:
 42 | 	-rm -rf $(BUILDDIR)/*
 43 | 
 44 | html:
 45 | 	$(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 46 | 	@echo
 47 | 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
 48 | 
 49 | dirhtml:
 50 | 	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
 51 | 	@echo
 52 | 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
 53 | 
 54 | singlehtml:
 55 | 	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
 56 | 	@echo
 57 | 	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
 58 | 
 59 | pickle:
 60 | 	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
 61 | 	@echo
 62 | 	@echo "Build finished; now you can process the pickle files."
 63 | 
 64 | json:
 65 | 	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 66 | 	@echo
 67 | 	@echo "Build finished; now you can process the JSON files."
 68 | 
 69 | htmlhelp:
 70 | 	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
 71 | 	@echo
 72 | 	@echo "Build finished; now you can run HTML Help Workshop with the" \
 73 | 	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 74 | 
 75 | qthelp:
 76 | 	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
 77 | 	@echo
 78 | 	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
 79 | 	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
 80 | 	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WeRoBot.qhcp"
 81 | 	@echo "To view the help file:"
 82 | 	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WeRoBot.qhc"
 83 | 
 84 | devhelp:
 85 | 	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
 86 | 	@echo
 87 | 	@echo "Build finished."
 88 | 	@echo "To view the help file:"
 89 | 	@echo "# mkdir -p $HOME/.local/share/devhelp/WeRoBot"
 90 | 	@echo "# ln -s $(BUILDDIR)/devhelp $HOME/.local/share/devhelp/WeRoBot"
 91 | 	@echo "# devhelp"
 92 | 
 93 | epub:
 94 | 	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
 95 | 	@echo
 96 | 	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
 97 | 
 98 | latex:
 99 | 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
100 | 	@echo
101 | 	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
102 | 	@echo "Run \`make' in that directory to run these through (pdf)latex" \
103 | 	      "(use \`make latexpdf' here to do that automatically)."
104 | 
105 | latexpdf:
106 | 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
107 | 	@echo "Running LaTeX files through pdflatex..."
108 | 	$(MAKE) -C $(BUILDDIR)/latex all-pdf
109 | 	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
110 | 
111 | text:
112 | 	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
113 | 	@echo
114 | 	@echo "Build finished. The text files are in $(BUILDDIR)/text."
115 | 
116 | man:
117 | 	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
118 | 	@echo
119 | 	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
120 | 
121 | texinfo:
122 | 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
123 | 	@echo
124 | 	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
125 | 	@echo "Run \`make' in that directory to run these through makeinfo" \
126 | 	      "(use \`make info' here to do that automatically)."
127 | 
128 | info:
129 | 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
130 | 	@echo "Running Texinfo files through makeinfo..."
131 | 	make -C $(BUILDDIR)/texinfo info
132 | 	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
133 | 
134 | gettext:
135 | 	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
136 | 	@echo
137 | 	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
138 | 
139 | changes:
140 | 	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
141 | 	@echo
142 | 	@echo "The overview file is in $(BUILDDIR)/changes."
143 | 
144 | linkcheck:
145 | 	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
146 | 	@echo
147 | 	@echo "Link check complete; look for any errors in the above output " \
148 | 	      "or in $(BUILDDIR)/linkcheck/output.txt."
149 | 
150 | doctest:
151 | 	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
152 | 	@echo "Testing of doctests in the sources finished, look at the " \
153 | 	      "results in $(BUILDDIR)/doctest/output.txt."
154 | 


--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/offu/WeRoBot/7b3d62d7ae126fc8bc9e033e6358dec2852d1d5a/docs/_static/logo.png


--------------------------------------------------------------------------------
/docs/_static/qq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/offu/WeRoBot/7b3d62d7ae126fc8bc9e033e6358dec2852d1d5a/docs/_static/qq.png


--------------------------------------------------------------------------------
/docs/_themes/LICENSE:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2010 by Armin Ronacher.
 2 | 
 3 | Some rights reserved.
 4 | 
 5 | Redistribution and use in source and binary forms of the theme, with or
 6 | without modification, are permitted provided that the following conditions
 7 | are met:
 8 | 
 9 | * Redistributions of source code must retain the above copyright
10 |   notice, this list of conditions and the following disclaimer.
11 | 
12 | * Redistributions in binary form must reproduce the above
13 |   copyright notice, this list of conditions and the following
14 |   disclaimer in the documentation and/or other materials provided
15 |   with the distribution.
16 | 
17 | * The names of the contributors may not be used to endorse or
18 |   promote products derived from this software without specific
19 |   prior written permission.
20 | 
21 | We kindly ask you to only use these themes in an unmodified manner just
22 | for Flask and Flask-related products, not for unrelated projects.  If you
23 | like the visual style and want to use it for your own projects, please
24 | consider making some larger changes to the themes (such as changing
25 | font faces, sizes, colors or margins).
26 | 
27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
37 | POSSIBILITY OF SUCH DAMAGE.
38 | 


--------------------------------------------------------------------------------
/docs/_themes/README.rst:
--------------------------------------------------------------------------------
 1 | Flask Sphinx Themes
 2 | ===================
 3 | 
 4 | This repository contains Sphinx themes for Flask and Flask related
 5 | projects.  To use this theme in your Sphinx documentation:
 6 | 
 7 | 1. Put this folder as ``_themes`` in the docs folder.  Alternatively
 8 |    you can use git submodules to check out the contents there.
 9 | 
10 | 2. Add this to ``conf.py``:
11 | 
12 |    .. code-block:: python
13 | 
14 |        sys.path.append(os.path.join(os.path.dirname(__file__), '_themes'))
15 |        html_theme_path = ['_themes']
16 |        html_theme = 'flask'
17 | 
18 | Themes
19 | ------
20 | 
21 | The following themes exist for ``html_theme``.
22 | 
23 | ======================= ===============================================
24 | flask                   The standard Flask documentation theme for
25 |                         large projects
26 | 
27 | flask_small             Small single page theme.  Intended to be used
28 |                         by very small addon libraries for Flask.
29 | ======================= ===============================================
30 | 
31 | Options
32 | -------
33 | 
34 | The following options can be set with ``html_theme_options``.
35 | 
36 | ======================= ===============================================
37 | index_logo              Filename of a picture in ``_static`` to be used
38 |                         as replacement for the ``h1`` in the
39 |                         ``index.rst`` file.
40 |                         *Default unset.*
41 | 
42 | index_logo_height       Height of the index logo.
43 |                         *Default 120px*.
44 | 
45 | touch_icon              Filename of a picture in ``_static`` to be use
46 |                         as the app icon on Apple devices.
47 |                         *Default unset.*
48 | 
49 | github_fork             Repository name on GitHub for the "Fork Me"
50 |                         badge.
51 |                         *Default unset.*
52 | 
53 | github_ribbon_color     Color for the "Fork Me" badge.
54 |                         *Default darkblue_121621.*
55 | ======================= ===============================================
56 | 
57 | Sidebar Templates
58 | -----------------
59 | 
60 | The following sidebar templates can be included in ``html_sidebars``.
61 | 
62 | ======================= ===============================================
63 | relations.html          Show parent, previous, and next links.
64 | ======================= ===============================================
65 | 
66 | Pygments Style
67 | --------------
68 | 
69 | The theme automatically sets ``pygments_style`` to the provided style.
70 | Make sure you remove any override from ``conf.py`` or set it to
71 | ``flask_theme_support.FlaskyStyle``.
72 | 


--------------------------------------------------------------------------------
/docs/_themes/flask/layout.html:
--------------------------------------------------------------------------------
 1 | {% extends 'basic/layout.html' %}
 2 | 
 3 | {% block extrahead %}
 4 |     {{ super() }}
 5 |     {% if theme_touch_icon %}
 6 |         <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}">
 7 |     {% endif %}
 8 |     <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
 9 | {% endblock %}
10 | 
11 | {% block relbar2 %}
12 |     {% if theme_github_fork %}
13 |         <a href="http://github.com/{{ theme_github_fork }}">
14 |             <img style="position: fixed; top: 0; right: 0; border: 0;"
15 |                  src="http://s3.amazonaws.com/github/ribbons/forkme_right_{{ theme_github_ribbon_color }}.png"
16 |                  alt="Fork me on GitHub">
17 |         </a>
18 |     {% endif %}
19 | {% endblock %}
20 | 
21 | {% block header %}
22 |     {{ super() }}
23 |     {% if pagename == 'index' %}<div class=indexwrapper>{% endif %}
24 | {% endblock %}
25 | 
26 | {% block footer %}
27 |     {{ super() }}
28 |     {% if pagename == 'index' %}</div>{% endif %}
29 | {% endblock %}
30 | 


--------------------------------------------------------------------------------
/docs/_themes/flask/relations.html:
--------------------------------------------------------------------------------
 1 | <h3>导航</h3>
 2 | <ul>
 3 |   {%- for parent in parents %}
 4 |   <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
 5 |   {%- endfor %}
 6 |     {%- if prev %}
 7 |       <li>{{ _('Previous topic') }}: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
 8 |         }}">{{ prev.title }}</a></li>
 9 |     {%- endif %}
10 |     {%- if next %}
11 |       <li>{{ _('Next topic') }}: <a href="{{ next.link|e }}" title="{{ _('next chapter')
12 |         }}">{{ next.title }}</a></li>
13 |     {%- endif %}
14 |   {%- for parent in parents %}
15 |   </ul></li>
16 |   {%- endfor %}
17 |   </ul></li>
18 | </ul>
19 | 


--------------------------------------------------------------------------------
/docs/_themes/flask/sidebarintro.html:
--------------------------------------------------------------------------------
 1 | {% if theme_github_url %}
 2 |     <h3><a href="index.html">{{ project }}</a></h3>
 3 |     <a href="http://github.com/{{ theme_github_url }}" class="sidebar-github-link">
 4 |         <img src="https://img.shields.io/github/stars/{{ theme_github_url }}.svg?style=social&label=Star&maxAge=2592000" />
 5 |         <img src="https://img.shields.io/github/forks/{{ theme_github_url }}.svg?style=social&label=Fork&maxAge=2592000" />
 6 |     </a>
 7 | 
 8 |     <h3>遇到问题了?</h3>
 9 |     <p>
10 |         如果你遇到了问题,请去 <a href="https://github.com/whtsky/WeRoBot/issues/new">发 Issue</a>。
11 |     </p>
12 |     QQ群: 283206829 <br>
13 |     <img src="{{ pathto('_static/qq.png', 1) }}" />
14 | {% endif %}


--------------------------------------------------------------------------------
/docs/_themes/flask/theme.conf:
--------------------------------------------------------------------------------
 1 | [theme]
 2 | inherit = basic
 3 | stylesheet = flasky.css
 4 | pygments_style = flask_theme_support.FlaskyStyle
 5 | 
 6 | [options]
 7 | index_logo =
 8 | index_logo_height = 120px
 9 | github_url =
10 | touch_icon =
11 | github_fork =
12 | github_ribbon_color = darkblue_121621
13 | 


--------------------------------------------------------------------------------
/docs/_themes/flask_theme_support.py:
--------------------------------------------------------------------------------
 1 | # flasky extensions.  flasky pygments style based on tango style
 2 | from pygments.style import Style
 3 | from pygments.token import Keyword, Name, Comment, String, Error, \
 4 |      Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
 5 | 
 6 | 
 7 | class FlaskyStyle(Style):
 8 |     background_color = "#f8f8f8"
 9 |     default_style = ""
10 | 
11 |     styles = {
12 |         # No corresponding class for the following:
13 |         #Text:                     "", # class:  ''
14 |         Whitespace: "underline #f8f8f8",  # class: 'w'
15 |         Error: "#a40000 border:#ef2929",  # class: 'err'
16 |         Other: "#000000",  # class 'x'
17 |         Comment: "italic #8f5902",  # class: 'c'
18 |         Comment.Preproc: "noitalic",  # class: 'cp'
19 |         Keyword: "bold #004461",  # class: 'k'
20 |         Keyword.Constant: "bold #004461",  # class: 'kc'
21 |         Keyword.Declaration: "bold #004461",  # class: 'kd'
22 |         Keyword.Namespace: "bold #004461",  # class: 'kn'
23 |         Keyword.Pseudo: "bold #004461",  # class: 'kp'
24 |         Keyword.Reserved: "bold #004461",  # class: 'kr'
25 |         Keyword.Type: "bold #004461",  # class: 'kt'
26 |         Operator: "#582800",  # class: 'o'
27 |         Operator.Word: "bold #004461",  # class: 'ow' - like keywords
28 |         Punctuation: "bold #000000",  # class: 'p'
29 | 
30 |         # because special names such as Name.Class, Name.Function, etc.
31 |         # are not recognized as such later in the parsing, we choose them
32 |         # to look the same as ordinary variables.
33 |         Name: "#000000",  # class: 'n'
34 |         Name.Attribute: "#c4a000",  # class: 'na' - to be revised
35 |         Name.Builtin: "#004461",  # class: 'nb'
36 |         Name.Builtin.Pseudo: "#3465a4",  # class: 'bp'
37 |         Name.Class: "#000000",  # class: 'nc' - to be revised
38 |         Name.Constant: "#000000",  # class: 'no' - to be revised
39 |         Name.Decorator: "#888",  # class: 'nd' - to be revised
40 |         Name.Entity: "#ce5c00",  # class: 'ni'
41 |         Name.Exception: "bold #cc0000",  # class: 'ne'
42 |         Name.Function: "#000000",  # class: 'nf'
43 |         Name.Property: "#000000",  # class: 'py'
44 |         Name.Label: "#f57900",  # class: 'nl'
45 |         Name.Namespace: "#000000",  # class: 'nn' - to be revised
46 |         Name.Other: "#000000",  # class: 'nx'
47 |         Name.Tag: "bold #004461",  # class: 'nt' - like a keyword
48 |         Name.Variable: "#000000",  # class: 'nv' - to be revised
49 |         Name.Variable.Class: "#000000",  # class: 'vc' - to be revised
50 |         Name.Variable.Global: "#000000",  # class: 'vg' - to be revised
51 |         Name.Variable.Instance: "#000000",  # class: 'vi' - to be revised
52 |         Number: "#990000",  # class: 'm'
53 |         Literal: "#000000",  # class: 'l'
54 |         Literal.Date: "#000000",  # class: 'ld'
55 |         String: "#4e9a06",  # class: 's'
56 |         String.Backtick: "#4e9a06",  # class: 'sb'
57 |         String.Char: "#4e9a06",  # class: 'sc'
58 |         String.Doc: "italic #8f5902",  # class: 'sd' - like a comment
59 |         String.Double: "#4e9a06",  # class: 's2'
60 |         String.Escape: "#4e9a06",  # class: 'se'
61 |         String.Heredoc: "#4e9a06",  # class: 'sh'
62 |         String.Interpol: "#4e9a06",  # class: 'si'
63 |         String.Other: "#4e9a06",  # class: 'sx'
64 |         String.Regex: "#4e9a06",  # class: 'sr'
65 |         String.Single: "#4e9a06",  # class: 's1'
66 |         String.Symbol: "#4e9a06",  # class: 'ss'
67 |         Generic: "#000000",  # class: 'g'
68 |         Generic.Deleted: "#a40000",  # class: 'gd'
69 |         Generic.Emph: "italic #000000",  # class: 'ge'
70 |         Generic.Error: "#ef2929",  # class: 'gr'
71 |         Generic.Heading: "bold #000080",  # class: 'gh'
72 |         Generic.Inserted: "#00A000",  # class: 'gi'
73 |         Generic.Output: "#888",  # class: 'go'
74 |         Generic.Prompt: "#745334",  # class: 'gp'
75 |         Generic.Strong: "bold #000000",  # class: 'gs'
76 |         Generic.Subheading: "bold #800080",  # class: 'gu'
77 |         Generic.Traceback: "bold #a40000",  # class: 'gt'
78 |     }
79 | 


--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
 1 | API
 2 | ==========
 3 | 
 4 | .. module:: werobot
 5 | 
 6 | 应用对象
 7 | ------------
 8 | 
 9 | .. module:: werobot.robot
10 | .. autoclass:: BaseRoBot
11 |     :members:
12 | .. autoclass:: WeRoBot
13 |     :members:
14 | 
15 | 配置对象
16 | ------------
17 | 
18 | .. module:: werobot.config
19 | .. autoclass:: Config
20 |     :members:
21 | 
22 | Session 对象
23 | ------------
24 | .. module:: werobot.session.sqlitestorage
25 | .. autoclass:: SQLiteStorage
26 | 
27 | .. module:: werobot.session.filestorage
28 | .. autoclass:: FileStorage
29 | 
30 | .. module:: werobot.session.mongodbstorage
31 | .. autoclass:: MongoDBStorage
32 | 
33 | .. module:: werobot.session.redisstorage
34 | .. autoclass:: RedisStorage
35 | 
36 | .. module:: werobot.session.saekvstorage
37 | .. autoclass:: SaeKVDBStorage
38 | 
39 | .. module:: werobot.session.mysqlstorage
40 | .. autoclass:: MySQLStorage
41 | 
42 | .. module:: werobot.session.postgresqlstorage
43 | .. autoclass:: PostgreSQLStorage
44 | 
45 | log
46 | ------------
47 | .. module:: werobot.logger
48 | .. autofunction:: enable_pretty_logging
49 | 


--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
  1 | Changelog
  2 | =============
  3 | 
  4 | Version 1.13.1
  5 | ----------------
  6 | + 更新了部分依赖。
  7 | 
  8 | Version 1.13.0
  9 | ----------------
 10 | + 停止了对 Django 1.11、Django 2.0 的测试
 11 | + 增加了对 Django 2.2、 Django 3.0、 Django 3.1 的测试
 12 | + 停止了对 Python 3.4、 Python 3.5 的测试
 13 | + 增加了对 Python 3.9 的测试
 14 | + :func:`werobot.client.Client.send_template_message` 添加跳转小程序 ( `#604 <https://github.com/offu/WeRoBot/pull/604>`_ )
 15 | 
 16 | Version 1.12.0
 17 | ----------------
 18 | + 增加了对微信模板回调消息的处理 :func:`robot.templatesendjobfinish_event <werobot.robot.BaseRoBot.templatesendjobfinish_event>`  ( `#544 <https://github.com/offu/WeRoBot/pull/544>`_ )
 19 | 
 20 | Version 1.11.0
 21 | ----------------
 22 | + 停止了对 Python2 与 PyPy 的支持。 ( `#539 <https://github.com/offu/WeRoBot/pull/539>`_ )
 23 | + 停止了对 SAE 相关代码的测试。 ( `#539 <https://github.com/offu/WeRoBot/pull/539>`_ )
 24 | 
 25 | Version 1.10.1
 26 | ----------------
 27 | + 修复 :ref:`群发接口` 的 docstring 样式。
 28 | 
 29 | Version 1.10.0
 30 | ----------------
 31 | + 在 Client 中增加对 :ref:`上传图文消息素材` 的支持  ( `#493 <https://github.com/offu/WeRoBot/pull/493>`_ )
 32 | + 在 Client 中增加对 :ref:`群发接口` 的支持  ( `#493 <https://github.com/offu/WeRoBot/pull/493>`_ )
 33 | 
 34 | Version 1.9.0
 35 | ----------------
 36 | + 在 Client 中增加对 :ref:`用户标签管理` 的支持 ( `#426 <https://github.com/offu/WeRoBot/pull/426>`_ )
 37 | 
 38 | Version 1.8.0
 39 | ----------------
 40 | + 增加 :class:`werobot.session.postgresqlstorage.PostgreSQLStorage` (`#383 <https://github.com/offu/WeRoBot/issues/383>`_) (`#412 <https://github.com/offu/WeRoBot/pull/412>`_)
 41 | + 修复 imp 在 Python3.4 后被废弃的问题 (`#411 <https://github.com/offu/WeRoBot/pull/411>`_)
 42 | 
 43 | Version 1.7.0
 44 | ----------------
 45 | + 为  :func:`werobot.client.Client.send_text_message`,:func:`werobot.client.Client.send_image_message`,:func:`werobot.client.Client.send_voice_message`,:func:`werobot.client.Client.send_video_message`,:func:`werobot.client.Client.send_music_message`,:func:`werobot.client.Client.send_article_message`,:func:`werobot.client.Client.send_news_message` 加入 ``kf_account`` 参数 ( `#384 <https://github.com/offu/WeRoBot/issues/384>`_ )
 46 | + 将  :class:`werobot.replies.TransferCustomerServiceReply` 的 ``account`` 改为非必需 ( `#363 <https://github.com/offu/WeRoBot/issues/363>`_ )
 47 | 
 48 | Version 1.6.0
 49 | ----------------
 50 | + 增加对发送小程序卡片的支持: :func:`werobot.client.Client.send_miniprogrampage_message` ( `#309 <https://github.com/offu/WeRoBot/pull/309>`_ by `@helloqiu <https://github.com/helloqiu>`_)
 51 | 
 52 | Version 1.5.0
 53 | ----------------
 54 | + 为正则匹配的 handler 加入匹配后的 ``Match Object`` 作为参数(`#305 <https://github.com/offu/WeRoBot/pull/305>`_)(`Author: cxgreat2014 <https://github.com/cxgreat2014>`_)
 55 | 
 56 | Version 1.4.1
 57 | ----------------
 58 | + 修复 :func:`werobot.client.Client.post` 中文文件名的 bug (`#292 <https://github.com/offu/WeRoBot/issues/292>`_)
 59 | 
 60 | Version 1.4.0
 61 | ----------------
 62 | + 增加 :class:`werobot.messages.events.CardPassCheckEvent`
 63 | + 增加 :class:`werobot.messages.events.CardNotPassCheckEvent`
 64 | + 增加 :class:`werobot.messages.events.UserGetCardEvent`
 65 | + 增加 :class:`werobot.messages.events.UserGiftingCardEvent`
 66 | + 增加 :class:`werobot.messages.events.UserDelCardEvent`
 67 | + 增加 :class:`werobot.messages.events.UserConsumeCardEvent`
 68 | + 增加 :class:`werobot.messages.events.UserPayFromPayCellEvent`
 69 | + 增加 :class:`werobot.messages.events.UserViewCardEvent`
 70 | + 增加 :class:`werobot.messages.events.UserEnterSessionFromCardEvent`
 71 | + 增加 :class:`werobot.messages.events.UpdateMemberCardEvent`
 72 | + 增加 :class:`werobot.messages.events.CardSkuRemindEvent`
 73 | + 增加 :class:`werobot.messages.events.CardPayOrderEvent`
 74 | + 增加 :class:`werobot.messages.events.SubmitMembercardUserInfoEvent`
 75 | 
 76 | Version 1.3.0
 77 | ----------------
 78 | + 增加 :class:`werobot.messages.events.UserScanProductEvent`
 79 | + 增加 :class:`werobot.messages.events.UserScanProductEnterSessionEvent`
 80 | + 增加 :class:`werobot.messages.events.UserScanProductAsyncEvent`
 81 | + 增加 :class:`werobot.messages.events.UserScanProductVerifyActionEvent`
 82 | + 增加 :class:`werobot.messages.events.PicSysphotoEvent`
 83 | + 增加 :class:`werobot.messages.events.PicPhotoOrAlbumEvent`
 84 | + 增加 :class:`werobot.messages.events.PicWeixinEvent`
 85 | + 增加 :class:`werobot.messages.events.LocationSelectEvent`
 86 | 
 87 | Version 1.2.0
 88 | ----------------
 89 | + 增加 :class:`werobot.session.mysqlstorage.MySQLStorage`
 90 | + 增加 :class:`werobot.messages.events.ScanCodePushEvent`
 91 | + 增加 :class:`werobot.messages.events.ScanCodeWaitMsgEvent`
 92 | + 增加 :func:`werobot.robot.BaseRoBot.add_filter`
 93 | + :func:`werobot.utils.generate_token` 在 Python 3.6+ 下优先使用 ``secrets.choice`` 来随机生成 token
 94 | + 修复 :func:`werobot.client.Client.get_media_list` 的调用参数错误 (`#208 <https://github.com/whtsky/WeRoBot/issues/208>`_)
 95 | + 修复了某些情况下 Client 中文编码不正确的问题 (`#250 <https://github.com/whtsky/WeRoBot/issues/250>`_)
 96 | + Handler 中的 Exception 现在会以 Error level 记录到 logger 中
 97 | + 在文档中增加了独立的 :doc:`api` 部分
 98 | + 添加了 ``video`` 和 ``shortvideo`` 的修饰器
 99 | + 增加了 :class:`werobot.session.saekvstorage.SaeKVDBStorage` 的测试
100 | + 增加了对 Django 2.0 的测试
101 | + 抛弃对 Django < 1.8 、 Django 1.9 、 Django 1.10 的支持
102 | 
103 | Version 1.1.1
104 | ----------------
105 | 
106 | + 修复 :func:`werobot.client.Client.create_menu` 文档中的错误
107 | + 在 :func:`werobot.client.Client.send_music_message` 的文档中提示了可能的缩略图不显示的问题
108 | 
109 | Version 1.1.0
110 | ----------------
111 | 
112 | + 为 :class:`werobot.robot.BaseRoBot` 增加 ``client`` property
113 | + 允许在初始化 :class:`werobot.robot.BaseRoBot` 时传入 :doc:`config` 。注意如果传入了 config , BaseRoBot 会忽略除 ``config`` 与 ``logger`` 外的其他所有的参数。
114 | + deprecate :class:`werobot.robot.BaseRoBot` 的 ``enable_session`` 参数
115 | + Session Storage 现在是惰性加载的了; 如果希望关闭 Session , 请将 :doc:`config` 中的 ``SESSION_STORAGE`` 设为 ``False`` (`#189 <https://github.com/whtsky/WeRoBot/issues/189>`_)
116 | + 修复了打包时 `error.html` 被忽略导致的默认错误页面错误的问题 (`#194 <https://github.com/whtsky/WeRoBot/issues/194>`_)
117 | + 允许使用 ``reply.time`` 的方式快速读取 Reply 属性
118 | + 完善 :doc:`client` 中自定义菜单、消息管理、素材管理、用户管理、账户管理、素材管理部分的 `API`
119 | + 修复了直接 GET 访问 Robot 主页返回 500 的问题
120 | 
121 | Version 1.0.0
122 | ----------------
123 | 
124 | + 增加对消息加解密的支持
125 | + 重写 werobot.messages, 完善对 Event 的支持
126 | + 将微信消息的 `id` 属性重命名为 `message_id`
127 | + 增加 :class:`werobot.reply.SuccessReply`
128 | + 增加 :class:`werobot.reply.ImageReply`
129 | + 增加 :class:`werobot.reply.VoiceReply`
130 | + 增加 :class:`werobot.reply.VideoReply`
131 | + 删除 :func:`werobot.reply.create_reply`
132 | + 为 :class:`werobot.reply.WeChatReply` 增加 ``process_args`` 方法
133 | + 为 :class:`werobot.robot.BaseRoBot` 增加 ``parse_message`` 方法
134 | + 为 :class:`werobot.robot.BaseRoBot` 增加 ``get_encrypted_reply`` 方法
135 | + 删去了 Reply 中过时的 flag
136 | + 修复 :class:`werobot.session.filestorage.FileStorage` 在 PyPy 下的兼容性问题
137 | + 增加 :class:`werobot.session.sqlitestorage.SQLiteStorage`
138 | + 将默认的 SessionBackend 切换为 :class:`werobot.session.sqlitestorage.SQLiteStorage`
139 | + 将图文消息单个消息的渲染函数放到 :class:`werobot.replies.Article` 内
140 | + 取消对 Python2.6, Python3.3 的支持
141 | + 增加与 Django 1.6+, Flask, Bottle, Tornado 集成的支持
142 | + 替换 `inspect.getargspec()` 
143 | 
144 | Version 0.6.1
145 | ----------------
146 | 
147 | + Fix wrong URL in ``upload_media``
148 | + Add VideoMessage
149 | 
150 | Version 0.6.0
151 | ----------------
152 | 
153 | + Add ``@werobot.filter``
154 | + Add :class:`werobot.session.saekvstorage`
155 | + Add support for Weixin Pay ( :class:`werobot.pay.WeixinPayClient` )
156 | + Add ``werobot.reply.TransferCustomerServiceReply``
157 | + Fix FileStorage's bug
158 | 
159 | Version 0.5.3
160 | ----------------
161 | 
162 | + Fix: can't handle request for root path
163 | 
164 | Version 0.5.2
165 | ----------------
166 | 
167 | + Fix Python 3 support
168 | 
169 | Version 0.5.1
170 | ----------------
171 | 
172 | + Fix typo
173 | 
174 | Version 0.5.0
175 | ----------------
176 | 
177 | + Add ``werobot.client``
178 | + Add ``werobot.config``
179 | + Add ``werobot.logger``
180 | + Add ``@werobot.key_click`` (Thanks @tg123)
181 | + Support Location Event
182 | + Use smart args
183 | + Friendly 403 page
184 | + Improved server support
185 | + Enable session by default.
186 | + Drop ``werobot.testing.make_text_message``
187 | + Drop ``werobot.testing.make_image_message``
188 | + Drop ``werobot.testing.make_location_message``
189 | + Drop ``werobot.testing.make_voice_message``
190 | + Drop ``werobot.testing.WeTest.send``
191 | + Rewrite ``werobot.message``
192 | + Rewrite testing case
193 | 
194 | Version 0.4.1
195 | ----------------
196 | + Add VoiceMessage
197 | + Add ``message.raw``: Raw XML of message
198 | + Rename ``UnknownMessage.content`` to ``UnknownMessage.raw``
199 | + Fix a bug when signature is invalid.
200 | + Ignore session when receive UnknownMessage
201 | 
202 | Version 0.4.0
203 | ----------------
204 | + Add session support
205 | + Add logging support
206 | + Rename ``werobot.test`` to ``werobot.testing``
207 | + Handlers added by ``@robot.handler`` will have the lowest priority.
208 | 
209 | Version 0.3.5
210 | ----------------
211 | + Bug fix: Make ``BaseRoBot`` importable
212 | 
213 | Version 0.3.4
214 | ----------------
215 | + Rename ``WeRoBot.app`` to ``WeRoBot.wsgi``
216 | + Add ``BaseRoBot`` class. It's useful for creating extensions.
217 | + Reorganized documents.
218 | 
219 | Version 0.3.3
220 | ----------------
221 | + Add ``host`` param in werobot.run
222 | + Update EventMessage
223 | + Add LinkMessage
224 | 
225 | Version 0.3.2
226 | ----------------
227 | + Convert all arguments to unicode in Python 2 ( See issue `#1 <https://github.com/whtsky/WeRoBot/pull/1>`_ )
228 | 
229 | Version 0.3.1
230 | ----------------
231 | + Add ``server`` param in werobot.run
232 | 
233 | Version 0.3.0
234 | ----------------
235 | + Add new messages and replies support for WeChat 4.5
236 | 


--------------------------------------------------------------------------------
/docs/client.rst:
--------------------------------------------------------------------------------
  1 | ``WeRoBot.Client`` —— 微信 API 操作类
  2 | =====================================
  3 | 有部分接口暂未实现,可自行调用微信接口。
  4 | 
  5 | .. module:: werobot.client
  6 | 
  7 | 开始开发
  8 | ------------
  9 | 
 10 | 获取 access token
 11 | ``````````````````````````````
 12 | 详细请参考 http://mp.weixin.qq.com/wiki/14/9f9c82c1af308e3b14ba9b973f99a8ba.html
 13 | 
 14 | .. automethod:: Client.grant_token
 15 | .. automethod:: Client.get_access_token
 16 | 
 17 | .. note:: Client 的操作都会自动进行 `access token` 的获取和过期刷新操作,如果有特殊需求(如多进程部署)可重写 ``get_access_token``。
 18 | 
 19 | 获取微信服务器IP地址
 20 | ``````````````````````````````
 21 | 详细请参考 http://mp.weixin.qq.com/wiki/4/41ef0843d6e108cf6b5649480207561c.html
 22 | 
 23 | .. automethod:: Client.get_ip_list
 24 | 
 25 | 自定义菜单
 26 | ------------
 27 | 
 28 | 自定义菜单创建接口
 29 | ``````````````````````````````
 30 | 详细请参考 http://mp.weixin.qq.com/wiki/10/0234e39a2025342c17a7d23595c6b40a.html
 31 | 
 32 | .. automethod:: Client.create_menu
 33 | 
 34 | 自定义菜单查询接口
 35 | ``````````````````````````````
 36 | 详细请参考 http://mp.weixin.qq.com/wiki/5/f287d1a5b78a35a8884326312ac3e4ed.html
 37 | 
 38 | .. automethod:: Client.get_menu
 39 | 
 40 | 自定义菜单删除接口
 41 | ``````````````````````````````
 42 | 详细请参考 http://mp.weixin.qq.com/wiki/3/de21624f2d0d3dafde085dafaa226743.html
 43 | 
 44 | .. automethod:: Client.delete_menu
 45 | 
 46 | 个性化菜单接口
 47 | ``````````````````````````````
 48 | 详细请参考 http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
 49 | 
 50 | .. automethod:: Client.create_custom_menu
 51 | .. automethod:: Client.delete_custom_menu
 52 | .. automethod:: Client.match_custom_menu
 53 | 
 54 | 获取自定义菜单配置接口
 55 | ``````````````````````````````
 56 | 详细请参考 http://mp.weixin.qq.com/wiki/14/293d0cb8de95e916d1216a33fcb81fd6.html
 57 | 
 58 | .. automethod:: Client.get_custom_menu_config
 59 | 
 60 | 消息管理
 61 | ------------
 62 | 
 63 | 客服接口
 64 | ``````````````````````````````
 65 | 详细请参考 http://mp.weixin.qq.com/wiki/11/c88c270ae8935291626538f9c64bd123.html
 66 | 发送卡券接口暂时未支持。可自行实现。
 67 | 
 68 | .. automethod:: Client.add_custom_service_account
 69 | .. automethod:: Client.update_custom_service_account
 70 | .. automethod:: Client.delete_custom_service_account
 71 | .. automethod:: Client.upload_custom_service_account_avatar
 72 | .. automethod:: Client.get_custom_service_account_list
 73 | .. automethod:: Client.get_online_custom_service_account_list
 74 | .. automethod:: Client.send_text_message
 75 | .. automethod:: Client.send_image_message
 76 | .. automethod:: Client.send_voice_message
 77 | .. automethod:: Client.send_video_message
 78 | .. automethod:: Client.send_music_message
 79 | .. automethod:: Client.send_article_message
 80 | .. automethod:: Client.send_news_message
 81 | .. automethod:: Client.send_miniprogrampage_message
 82 | 
 83 | 群发接口
 84 | ``````````````````````````````
 85 | 
 86 | .. automethod:: Client.send_mass_msg
 87 | .. automethod:: Client.delete_mass_msg
 88 | .. automethod:: Client.send_mass_preview_to_user
 89 | .. automethod:: Client.get_mass_msg_status
 90 | .. automethod:: Client.get_mass_msg_speed
 91 | 
 92 | 用户管理
 93 | ------------
 94 | 
 95 | 用户分组管理
 96 | ``````````````````````````````
 97 | 详细请参考 http://mp.weixin.qq.com/wiki/8/d6d33cf60bce2a2e4fb10a21be9591b8.html
 98 | 
 99 | .. automethod:: Client.create_group
100 | .. automethod:: Client.get_groups
101 | .. automethod:: Client.get_group_by_id
102 | .. automethod:: Client.update_group
103 | .. automethod:: Client.move_user
104 | .. automethod:: Client.move_users
105 | .. automethod:: Client.delete_group
106 | 
107 | 设置备注名
108 | ``````````````````````````````
109 | 详细请参考 http://mp.weixin.qq.com/wiki/16/528098c4a6a87b05120a7665c8db0460.html
110 | 
111 | .. automethod:: Client.remark_user
112 | 
113 | 获取用户基本信息
114 | ``````````````````````````````
115 | 详细请参考 http://mp.weixin.qq.com/wiki/1/8a5ce6257f1d3b2afb20f83e72b72ce9.html
116 | 
117 | .. automethod:: Client.get_user_info
118 | .. automethod:: Client.get_users_info
119 | 
120 | 账户管理
121 | ------------
122 | 长链接转短链接接口和微信认证事件推送暂未添加,可自行实现。
123 | 
124 | 生成带参数的二维码
125 | ``````````````````````````````
126 | 详细请参考 http://mp.weixin.qq.com/wiki/18/167e7d94df85d8389df6c94a7a8f78ba.html
127 | 
128 | .. automethod:: Client.create_qrcode
129 | .. automethod:: Client.show_qrcode
130 | 
131 | 获取用户列表
132 | ``````````````````````````````
133 | 详细请参考 http://mp.weixin.qq.com/wiki/12/54773ff6da7b8bdc95b7d2667d84b1d4.html
134 | 
135 | .. automethod:: Client.get_followers
136 | 
137 | 素材管理
138 | ------------
139 | 
140 | 新增临时素材
141 | ``````````````````````````````
142 | 详细请参考 http://mp.weixin.qq.com/wiki/15/2d353966323806a202cd2deaafe8e557.html
143 | 
144 | .. automethod:: Client.upload_media
145 | 
146 | 获取临时素材
147 | ``````````````````````````````
148 | 详细请参考 http://mp.weixin.qq.com/wiki/9/677a85e3f3849af35de54bb5516c2521.html
149 | 
150 | .. automethod:: Client.download_media
151 | 
152 | 新增永久素材
153 | ``````````````````````````````
154 | 详细请参考 http://mp.weixin.qq.com/wiki/10/10ea5a44870f53d79449290dfd43d006.html
155 | 
156 | .. automethod:: Client.add_news
157 | .. automethod:: Client.upload_news_picture
158 | .. automethod:: Client.upload_permanent_media
159 | .. automethod:: Client.upload_permanent_video
160 | 
161 | 获取永久素材
162 | ``````````````````````````````
163 | 详细请参考 http://mp.weixin.qq.com/wiki/12/3c12fac7c14cb4d0e0d4fe2fbc87b638.html
164 | 
165 | .. automethod:: Client.download_permanent_media
166 | 
167 | 删除永久素材
168 | ``````````````````````````````
169 | 详细请参考 http://mp.weixin.qq.com/wiki/7/2212203f4e17253b9aef77dc788f5337.html
170 | 
171 | .. automethod:: Client.delete_permanent_media
172 | 
173 | 上传图文消息素材
174 | ``````````````````````````````
175 | 
176 | .. automethod:: Client.upload_news
177 | 
178 | 修改永久图文素材
179 | ``````````````````````````````
180 | 详细请参考 http://mp.weixin.qq.com/wiki/10/c7bad9a463db20ff8ccefeedeef51f9e.html
181 | 
182 | .. automethod:: Client.update_news
183 | 
184 | 获取素材总数
185 | ``````````````````````````````
186 | 详细请参考 http://mp.weixin.qq.com/wiki/5/a641fd7b5db7a6a946ebebe2ac166885.html
187 | 
188 | .. automethod:: Client.get_media_count
189 | 
190 | 获取素材列表
191 | ``````````````````````````````
192 | 详细请参考 http://mp.weixin.qq.com/wiki/15/8386c11b7bc4cdd1499c572bfe2e95b3.html
193 | 
194 | .. automethod:: Client.get_media_list
195 | 
196 | 用户标签管理
197 | ------------
198 | 详细请参考 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837
199 | 
200 | 创建标签
201 | ``````````````````````````````
202 | .. automethod:: Client.create_tag
203 | 
204 | 获取公众号已创建的标签
205 | ``````````````````````````````
206 | .. automethod:: Client.get_tags
207 | 
208 | 编辑标签
209 | ``````````````````````````````
210 | .. automethod:: Client.update_tag
211 | 
212 | 删除标签
213 | ``````````````````````````````
214 | .. automethod:: Client.delete_tag
215 | 
216 | 获取标签下粉丝列表
217 | ``````````````````````````````
218 | .. automethod:: Client.get_users_by_tag
219 | 
220 | 批量为用户打标签
221 | ``````````````````````````````
222 | .. automethod:: Client.tag_users
223 | 
224 | 批量为用户取消标签
225 | ``````````````````````````````
226 | .. automethod:: Client.untag_users
227 | 
228 | 获取用户身上的标签列表
229 | ``````````````````````````````
230 | .. automethod:: Client.get_tags_by_user
231 | 
232 | 模板消息
233 | ------------
234 | .. automethod:: Client.send_template_message
235 | 
236 | 
237 | 返回码都是什么意思?
238 | --------------------------
239 | 
240 | 参考 https://mp.weixin.qq.com/wiki/10/6380dc743053a91c544ffd2b7c959166.html
241 | 
242 | 48001 -- API Unauthorized
243 | ---------------------------
244 | 
245 | 如果你遇到了这个错误,请检查你的微信公众号是否有调用该接口的权限。
246 | 参考: https://mp.weixin.qq.com/wiki/13/8d4957b72037e3308a0ca1b21f25ae8d.html
247 | 


--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | #
  3 | # WeRoBot documentation build configuration file, created by
  4 | # sphinx-quickstart on Mon Feb  4 16:51:10 2013.
  5 | #
  6 | # This file is execfile()d with the current directory set to its containing dir.
  7 | #
  8 | # Note that not all possible configuration values are present in this
  9 | # autogenerated file.
 10 | #
 11 | # All configuration values have a default; values that are commented out
 12 | # serve to show the default.
 13 | 
 14 | import sys
 15 | import os
 16 | 
 17 | # If extensions (or modules to document with autodoc) are in another directory,
 18 | # add these directories to sys.path here. If the directory is relative to the
 19 | # documentation root, use os.path.abspath to make it absolute, like shown here.
 20 | sys.path.insert(0, os.path.abspath(".."))
 21 | sys.path.append(os.path.abspath("_themes"))
 22 | 
 23 | import werobot
 24 | 
 25 | # -- General configuration -----------------------------------------------------
 26 | 
 27 | # If your documentation needs a minimal Sphinx version, state it here.
 28 | # needs_sphinx = '1.0'
 29 | 
 30 | # Add any Sphinx extension module names here, as strings. They can be extensions
 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 32 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"]
 33 | 
 34 | # Add any paths that contain templates here, relative to this directory.
 35 | templates_path = ["_templates"]
 36 | 
 37 | # The suffix of source filenames.
 38 | source_suffix = ".rst"
 39 | 
 40 | # The encoding of source files.
 41 | # source_encoding = 'utf-8-sig'
 42 | 
 43 | # The master toctree document.
 44 | master_doc = "index"
 45 | 
 46 | # General information about the project.
 47 | project = u"WeRoBot"
 48 | copyright = u"2019, offu"
 49 | 
 50 | # The version info for the project you're documenting, acts as replacement for
 51 | # |version| and |release|, also used in various other places throughout the
 52 | # built documents.
 53 | #
 54 | version = release = werobot.__version__
 55 | 
 56 | # The language for content autogenerated by Sphinx. Refer to documentation
 57 | # for a list of supported languages.
 58 | language = "zh_CN"
 59 | 
 60 | # There are two options for replacing |today|: either, you set today to some
 61 | # non-false value, then it is used:
 62 | # today = ''
 63 | # Else, today_fmt is used as the format for a strftime call.
 64 | # today_fmt = '%B %d, %Y'
 65 | 
 66 | # List of patterns, relative to source directory, that match files and
 67 | # directories to ignore when looking for source files.
 68 | exclude_patterns = ["_build", "_themes"]
 69 | 
 70 | # The reST default role (used for this markup: `text`) to use for all documents.
 71 | # default_role = None
 72 | 
 73 | # If true, '()' will be appended to :func: etc. cross-reference text.
 74 | # add_function_parentheses = True
 75 | 
 76 | # If true, the current module name will be prepended to all description
 77 | # unit titles (such as .. function::).
 78 | # add_module_names = True
 79 | 
 80 | # If true, sectionauthor and moduleauthor directives will be shown in the
 81 | # output. They are ignored by default.
 82 | # show_authors = False
 83 | 
 84 | # The name of the Pygments (syntax highlighting) style to use.
 85 | pygments_style = "flask_theme_support.FlaskyStyle"
 86 | 
 87 | # A list of ignored prefixes for module index sorting.
 88 | # modindex_common_prefix = []
 89 | 
 90 | # -- Options for HTML output ---------------------------------------------------
 91 | 
 92 | # The theme to use for HTML and HTML Help pages.  See the documentation for
 93 | # a list of builtin themes.
 94 | sys.path.append(os.path.join(os.path.dirname(__file__), "_themes"))
 95 | html_theme_path = ["_themes"]
 96 | html_theme = "flask"
 97 | 
 98 | # Theme options are theme-specific and customize the look and feel of a theme
 99 | # further.  For a list of options available for each theme, see the
100 | # documentation.
101 | html_theme_options = {"touch_icon": "logo.png", "github_url": "offu/WeRoBot"}
102 | 
103 | # Add any paths that contain custom themes here, relative to this directory.
104 | # html_theme_path = []
105 | 
106 | # The name for this set of Sphinx documents.  If None, it defaults to
107 | # "<project> v<release> documentation".
108 | # html_title = None
109 | 
110 | # A shorter title for the navigation bar.  Default is the same as html_title.
111 | # html_short_title = None
112 | 
113 | # The name of an image file (relative to this directory) to place at the top
114 | # of the sidebar.
115 | # html_logo = None
116 | 
117 | # The name of an image file (within the static path) to use as favicon of the
118 | # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
119 | # pixels large.
120 | # html_favicon = None
121 | 
122 | # Add any paths that contain custom static files (such as style sheets) here,
123 | # relative to this directory. They are copied after the builtin static files,
124 | # so a file named "default.css" will overwrite the builtin "default.css".
125 | html_static_path = ["_static"]
126 | 
127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
128 | # using the given strftime format.
129 | # html_last_updated_fmt = '%b %d, %Y'
130 | 
131 | # If true, SmartyPants will be used to convert quotes and dashes to
132 | # typographically correct entities.
133 | # html_use_smartypants = True
134 | 
135 | # Custom sidebar templates, maps document names to template names.
136 | html_sidebars = {
137 |     "index": ["sidebarintro.html", "searchbox.html"],
138 |     "**": [
139 |         "sidebarintro.html", "localtoc.html", "relations.html",
140 |         "searchbox.html"
141 |     ],
142 | }
143 | 
144 | # Additional templates that should be rendered to pages, maps page names to
145 | # template names.
146 | # html_additional_pages = {}
147 | 
148 | # If false, no module index is generated.
149 | # html_domain_indices = True
150 | 
151 | # If false, no index is generated.
152 | # html_use_index = True
153 | 
154 | # If true, the index is split into individual pages for each letter.
155 | # html_split_index = False
156 | 
157 | # If true, links to the reST sources are added to the pages.
158 | # html_show_sourcelink = True
159 | 
160 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
161 | # html_show_sphinx = True
162 | 
163 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
164 | # html_show_copyright = True
165 | 
166 | # If true, an OpenSearch description file will be output, and all pages will
167 | # contain a <link> tag referring to it.  The value of this option must be the
168 | # base URL from which the finished HTML is served.
169 | # html_use_opensearch = ''
170 | 
171 | # This is the file name suffix for HTML files (e.g. ".xhtml").
172 | # html_file_suffix = None
173 | 
174 | # Output file base name for HTML help builder.
175 | htmlhelp_basename = "WeRoBotdoc"
176 | 
177 | # -- Options for LaTeX output --------------------------------------------------
178 | 
179 | latex_elements = {
180 |     # The paper size ('letterpaper' or 'a4paper').
181 |     #'papersize': 'letterpaper',
182 |     # The font size ('10pt', '11pt' or '12pt').
183 |     #'pointsize': '10pt',
184 |     # Additional stuff for the LaTeX preamble.
185 |     #'preamble': '',
186 | }
187 | 
188 | # Grouping the document tree into LaTeX files. List of tuples
189 | # (source start file, target name, title, author, documentclass [howto/manual]).
190 | latex_documents = [
191 |     ("index", "WeRoBot.tex", u"WeRoBot Documentation", u"offu", "manual")
192 | ]
193 | 
194 | # The name of an image file (relative to this directory) to place at the top of
195 | # the title page.
196 | # latex_logo = None
197 | 
198 | # For "manual" documents, if this is true, then toplevel headings are parts,
199 | # not chapters.
200 | # latex_use_parts = False
201 | 
202 | # If true, show page references after internal links.
203 | # latex_show_pagerefs = False
204 | 
205 | # If true, show URL addresses after external links.
206 | # latex_show_urls = False
207 | 
208 | # Documents to append as an appendix to all manuals.
209 | # latex_appendices = []
210 | 
211 | # If false, no module index is generated.
212 | # latex_domain_indices = True
213 | 
214 | # -- Options for manual page output --------------------------------------------
215 | 
216 | # One entry per manual page. List of tuples
217 | # (source start file, name, description, authors, manual section).
218 | man_pages = [("index", "werobot", u"WeRoBot Documentation", [u"offu"], 1)]
219 | 
220 | # If true, show URL addresses after external links.
221 | # man_show_urls = False
222 | 
223 | # -- Options for Texinfo output ------------------------------------------------
224 | 
225 | # Grouping the document tree into Texinfo files. List of tuples
226 | # (source start file, target name, title, author,
227 | #  dir menu entry, description, category)
228 | texinfo_documents = [
229 |     (
230 |         "index",
231 |         "WeRoBot",
232 |         u"WeRoBot Documentation",
233 |         u"offu",
234 |         "WeRoBot",
235 |         "One line description of project.",
236 |         "Miscellaneous",
237 |     )
238 | ]
239 | 
240 | # Documents to append as an appendix to all manuals.
241 | # texinfo_appendices = []
242 | 
243 | # If false, no module index is generated.
244 | # texinfo_domain_indices = True
245 | 
246 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
247 | # texinfo_show_urls = 'footnote'
248 | 


--------------------------------------------------------------------------------
/docs/config.rst:
--------------------------------------------------------------------------------
 1 | Config
 2 | =====================
 3 | 
 4 | WeRoBot 使用 ``WeRoBot.Config`` 类来存储配置信息。  ``WeRoBot`` 类实例的 ``config`` 属性是一个 :class:`werobot.config.Config` 实例。
 5 | 
 6 | :class:`~werobot.config.Config` 继承自 `dict` 。因此, 你可以像使用普通 dict 一样使用它 ::
 7 | 
 8 |     from werobot import WeRoBot
 9 |     robot = WeRoBot(token='2333')
10 | 
11 |     robot.config.update(
12 |         HOST='0.0.0.0',
13 |         PORT=80
14 |     )
15 | 
16 | 当然, 你也可以先创建一个 Config ,然后在初始化 ``WeRobot`` 的时候传入自己的 Config ::
17 | 
18 |     from werobot.config import Config
19 |     config = Config(
20 |         TOKEN="token from config!"
21 |     )
22 |     robot = WeRoBot(config=config, token="token from init")
23 |     assert robot.token == "token from config!"
24 | 
25 | .. note:: 如果你在初始化 ``WeRoBot`` 时传入了 ``config`` 参数, ``WeRoBot`` 会忽略除 ``logger`` 外其他所有的初始化参数。 如果你需要对 ``WeRoBot`` 进行一些配置操作, 请修改 Config 。
26 | 
27 | 与普通 `dict` 不同的是, 你可以先把配置文件保存在一个对象或是文件中, 然后在 :class:`~werobot.config.Config` 中导入配置 ::
28 | 
29 |     from werobot import WeRoBot
30 |     robot = WeRoBot(token='2333')
31 | 
32 |     class MyConfig(object):
33 |         HOST = '0.0.0.0'
34 |         PORT = 80
35 | 
36 |     robot.config.from_object(MyConfig)
37 |     robot.config.from_pyfile("config.py")
38 | 
39 | 
40 | 
41 | 默认配置
42 | ----------
43 | 
44 | .. code:: python
45 | 
46 |     dict(
47 |         TOKEN=None,
48 |         SERVER="auto",
49 |         HOST="127.0.0.1",
50 |         PORT="8888",
51 |         SESSION_STORAGE=None,
52 |         APP_ID=None,
53 |         APP_SECRET=None,
54 |         ENCODING_AES_KEY=None
55 |     )
56 | 


--------------------------------------------------------------------------------
/docs/contrib.rst:
--------------------------------------------------------------------------------
  1 | 与其他 Web 框架集成
  2 | ===================
  3 | 
  4 | WeRoBot 可以作为独立服务运行,也可以集成在其他 Web 框架中一同运行。
  5 | 
  6 | Django
  7 | --------
  8 | WeRoBot 支持 Django 2.2+。
  9 | 
 10 | 首先,在一个文件中写好你的微信机器人 ::
 11 | 
 12 |     # Filename: robot.py
 13 | 
 14 |     from werobot import WeRoBot
 15 | 
 16 |     myrobot = WeRoBot(token='token')
 17 | 
 18 | 
 19 |     @myrobot.handler
 20 |     def hello(message):
 21 |         return 'Hello World!'
 22 | 
 23 | 然后,在你 Django 项目中的 ``urls.py`` 中调用 :func:`werobot.contrib.django.make_view` ,将 WeRoBot 集成进 Django ::
 24 | 
 25 |     from django.conf.urls import patterns, include, url
 26 |     from werobot.contrib.django import make_view
 27 |     from robot import myrobot
 28 | 
 29 |     urlpatterns = patterns('',
 30 |         url(r'^robot/', make_view(myrobot)),
 31 |     )
 32 | 
 33 | .. module:: werobot.contrib.django
 34 | .. autofunction:: make_view
 35 | 
 36 | Flask
 37 | ----------
 38 | 首先, 同样在文件中写好你的微信机器人 ::
 39 | 
 40 |     # Filename: robot.py
 41 | 
 42 |     from werobot import WeRoBot
 43 | 
 44 |     myrobot = WeRoBot(token='token')
 45 | 
 46 | 
 47 |     @myrobot.handler
 48 |     def hello(message):
 49 |         return 'Hello World!'
 50 | 
 51 | 然后, 在 Flask 项目中为 Flask 实例集成 WeRoBot ::
 52 | 
 53 |     from flask import Flask
 54 |     from robot import myrobot
 55 |     from werobot.contrib.flask import make_view
 56 | 
 57 |     app = Flask(__name__)
 58 |     app.add_url_rule(rule='/robot/', # WeRoBot 挂载地址
 59 |                      endpoint='werobot', # Flask 的 endpoint
 60 |                      view_func=make_view(myrobot),
 61 |                      methods=['GET', 'POST'])
 62 | 
 63 | .. module:: werobot.contrib.flask
 64 | .. autofunction:: make_view
 65 | 
 66 | 
 67 | Bottle
 68 | --------
 69 | 在你的 Bottle App 中集成 WeRoBot ::
 70 | 
 71 |     from werobot import WeRoBot
 72 | 
 73 |     myrobot = WeRoBot(token='token')
 74 | 
 75 |     @myrobot.handler
 76 |     def hello(message):
 77 |         return 'Hello World!'
 78 | 
 79 |     from bottle import Bottle
 80 |     from werobot.contrib.bottle import make_view
 81 | 
 82 |     app = Bottle()
 83 |     app.route('/robot',  # WeRoBot 挂载地址
 84 |              ['GET', 'POST'],
 85 |              make_view(myrobot))
 86 | 
 87 | .. module:: werobot.contrib.bottle
 88 | .. autofunction:: make_view
 89 | 
 90 | Tornado
 91 | ----------
 92 | 最简单的 Hello World ::
 93 | 
 94 |     import tornado.ioloop
 95 |     import tornado.web
 96 |     from werobot import WeRoBot
 97 |     from werobot.contrib.tornado import make_handler
 98 | 
 99 |     myrobot = WeRoBot(token='token')
100 | 
101 | 
102 |     @myrobot.handler
103 |     def hello(message):
104 |         return 'Hello World!'
105 | 
106 |     application = tornado.web.Application([
107 |         (r"/robot/", make_handler(myrobot)),
108 |     ])
109 | 
110 |     if __name__ == "__main__":
111 |         application.listen(8888)
112 |         tornado.ioloop.IOLoop.instance().start()
113 | 
114 | .. module:: werobot.contrib.tornado
115 | .. autofunction:: make_handler
116 | 


--------------------------------------------------------------------------------
/docs/contribution-guide.rst:
--------------------------------------------------------------------------------
 1 | 贡献指南
 2 | ===========================
 3 | 
 4 | 有许多种为 WeRoBot 做贡献的方式, 包括但并不仅限于
 5 | 
 6 | + `上报 bug <https://github.com/whtsky/WeRoBot/issues/new?labels=bug>`_
 7 | + `提交 Feature Request <https://github.com/whtsky/WeRoBot/issues/new?labels=Feature Request>`_
 8 | + :ref:`贡献代码`
 9 | + 加入 WeRoBot QQ 群(283206829) 帮助解答问题
10 | + 把 WeRoBot 安利给你周围的人 :)
11 | 
12 | 贡献代码
13 | ----------
14 | 
15 | 如果你希望为 WeRoBot 贡献代码, 请现在 GitHub 上 `Fork <https://github.com/whtsky/WeRoBot>`_ WeRoBot 仓库, 然后在 ``master`` 分支上开一个新的分支。
16 | 
17 | 如果你的贡献的代码是修复 Bug , 请确认这个 Bug 已经有了对应的 Issue (如果没有, 请先创建一个); 然后在 Pull Request 的描述里面引用这个 Bug 的 Issue ID , 就像这样(假设 Issue ID 为 153) ::
18 | 
19 |     Fix #153
20 | 
21 | 环境搭建
22 | ~~~~~~~~~~~
23 | 建议使用 ``virtualenv`` 创建虚拟环境进行开发, 然后安装开发环境需要的 packages。
24 | 关于 Python 版本, 推荐使用 Python 3.6 进行开发。
25 | 
26 | 如果使用的是 3.x 版本 ::
27 | 
28 |     # Python 3.5
29 |     python -m venv venv
30 | 
31 | 如果是其他版本 ::
32 | 
33 |     # virtualenv is highly recommended.
34 |     virtualenv venv
35 |     # Activate virtualenv.
36 |     source venv/bin/activate
37 |     # Install dev packages.
38 |     pip install -r dev-requirements.txt
39 | 
40 | 代码风格
41 | ~~~~~~~~~~~
42 | 我们使用 `yapf <https://github.com/google/yapf>`_ 进行代码格式化。
43 | 在提交代码之前,请格式化一下你的代码 ::
44 | 
45 |     # Install yapf
46 |     pip install yapf
47 |     # format code
48 |     yapf -i -p -r werobot/ tests/ *.py
49 | 
50 | 你也可以 `安装 yapf Pre-Commit Hook <https://github.com/google/yapf/tree/master/plugins#git-pre-commit-hook>`_ 来自动进行代码格式化工作。
51 | 
52 | 测试
53 | ~~~~~~~~~~~
54 | 在代码提交之前, 请先运行本地的测试。每次提交之后会有在线的 CI 运行更多版本兼容性的测试, 请密切关注测试结果。 ::
55 | 
56 |     # Run tests locally.
57 |     python setup.py test
58 | 
59 | 当然也可以使用 tox 在本地运行多版本的兼容性测试。 ::
60 | 
61 |     # Run multi-version tests locally.
62 |     tox
63 | 
64 | 如果你的 Pull Request 添加了新的模块或者功能,请为这些代码添加必要的测试。 所有的测试文件都在 tests 文件夹下。
65 | 
66 | 当一切开发完成之后, 可以发 Pull Request 到 ``master`` 分支, 我们会为你的代码做 Review。同时 CI 也会自动运行测试。
67 | 
68 | .. note:: 我们只会 Merge 通过了测试的代码。
69 | 


--------------------------------------------------------------------------------
/docs/deploy.rst:
--------------------------------------------------------------------------------
  1 | 部署
  2 | =====================
  3 | 
  4 | .. note:: 本节所讨论的是将 WeRoBot 作为独立服务运行情况下的部署操作。 如果你希望将 WeRoBot 集成到其他 Web 框架内,请阅读 :doc:`contrib`
  5 | 
  6 | 在独立服务器上部署
  7 | ----------------------
  8 | 
  9 | 使用 ``werobot.run`` 来启动 WSGI 服务器
 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 11 | 
 12 | 你可以在  ``werobot.config`` 中配置好 WeRoBot 需要监听的地址和端口号, 然后使用 ``werobot.run`` 来启动服务器 ::
 13 | 
 14 |     import werobot
 15 | 
 16 |     robot = werobot.WeRoBot(token='tokenhere')
 17 | 
 18 |     @robot.handler
 19 |     def echo(message):
 20 |         return 'Hello World!'
 21 | 
 22 |     robot.config['HOST'] = '0.0.0.0'
 23 |     robot.config['PORT'] = 80
 24 | 
 25 |     robot.run()
 26 | 
 27 | .. note:: 你需要 root 或管理员权限才能监听 1024 以下的端口。
 28 | 
 29 | 你可以通过传递 `server` 参数来手动指定使用的服务器 ::
 30 | 
 31 |     import werobot
 32 | 
 33 |     robot = werobot.WeRoBot(token='tokenhere')
 34 | 
 35 |     @robot.handler
 36 |     def echo(message):
 37 |         return 'Hello World!'
 38 | 
 39 |     robot.config['HOST'] = '0.0.0.0'
 40 |     robot.config['PORT'] = 80
 41 | 
 42 |     robot.run(server='gevent')
 43 | 
 44 | server 支持以下几种:
 45 | 
 46 | + cgi
 47 | + flup
 48 | + wsgiref
 49 | + waitress
 50 | + cherrypy
 51 | + paste
 52 | + fapws3
 53 | + tornado
 54 | + gae
 55 | + twisted
 56 | + diesel
 57 | + meinheld
 58 | + gunicorn
 59 | + eventlet
 60 | + gevent
 61 | + rocket
 62 | + bjoern
 63 | + auto
 64 | 
 65 | 当 server 为 auto 时, WeRoBot 会自动依次尝试以下几种服务器:
 66 | 
 67 | + Waitress
 68 | + Paste
 69 | + Twisted
 70 | + CherryPy
 71 | + WSGIRef
 72 | 
 73 | 所以,只要你安装了相应的服务器软件,就可以使用 ``werobot.run`` 直接跑在生产环境下。
 74 | 
 75 | .. note:: server 的默认值为 ``auto`` 。
 76 | .. attention::  `WSGIRef <http://docs.python.org/library/wsgiref.html#module-wsgiref.simple_server>`_ 的性能非常差, 仅能用于开发环境。 如果你要在生产环境下部署 WeRoBot , 请确保你在使用其他 server 。
 77 | 
 78 | 通过 WSGI HTTP Server 运行 WeRoBot
 79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 80 | 
 81 | ``werobot.wsgi`` 暴露了一个 WSGI Application ,你可以使用任何你喜欢的 WSGI HTTP Server 来部署 WeRoBot。
 82 | 比如, 如果你想用 Gunicorn 来部署 ::
 83 | 
 84 |     # FileName: robot.py
 85 |     from werobot import WeRoBot
 86 |     robot = WeRoBot()
 87 | 
 88 | 那么你只需要在 Shell 下运行 ::
 89 | 
 90 |     gunicorn robot:robot.wsgi
 91 | 
 92 | 就可以了。
 93 | 
 94 | 使用 Supervisor 管理守护进程
 95 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 96 | 
 97 | 请注意, ``werobot.run`` 是跑在 **非守护进程模式下** 的——也就是说,一旦你关闭终端,进程就会自动退出。
 98 | 
 99 | 我们建议您使用 `Supervisor <http://supervisord.org/>`_ 来管理 WeRoBot 的进程。
100 | 
101 | 配置文件样例: ::
102 | 
103 |     [program:wechat_robot]
104 |     command = python /home/<username>/robot.py
105 |     user = <username>
106 |     redirect_stderr = true
107 |     stdout_logfile = /home/<username>/logs/robot.log
108 |     stdout_errfile = /home/<username>/logs/robot_error.log
109 | 
110 | 使用 Nginx 进行反向代理
111 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
112 | 
113 | 微信服务器只支持80端口的机器人——显然,你的服务器上不会只跑着一个微信机器人。对于这种情况,我们建议您使用 Nginx 来进行反向代理。
114 | 
115 | 配置文件样例: ::
116 | 
117 |     server {
118 |         server_name example.com;
119 |         listen 80;
120 | 
121 |         location / {
122 |             proxy_pass_header Server;
123 |             proxy_redirect off;
124 |             proxy_pass http://127.0.0.1:12233;
125 |         }
126 |     }
127 | 
128 | .. note:: 在这个例子中, WeRoBot 的端口号为 12233。你应该在微信管理后台中将服务器地址设为 ``http://example.com`` 。
129 | 
130 | 在SAE上部署
131 | -----------------
132 | 
133 | .. attention:: 从 :ref:`Version 1.11.0` 开始,WeRoBot 停止测试 SAE 相关部分的代码。
134 | 
135 | 新浪云上的 Python 应用的入口为 index.wsgi:application ,也就是 index.wsgi 这个文件中名为 application 的 callable object。
136 | 
137 | 
138 | 所以,假设你在 `robot.py` 中使用了 WeRoBot ::
139 | 
140 |     # filename: robot.py
141 |     import werobot
142 | 
143 |     robot = werobot.WeRoBot(token='tokenhere')
144 | 
145 | 
146 |     @robot.handler
147 |     def echo(message):
148 |         return 'Hello World!'
149 | 
150 | 你需要再创建一个 `index.wsgi` 文件, 里面写 ::
151 | 
152 |     import sae
153 |     from robot import robot
154 | 
155 | 
156 |     application = sae.create_wsgi_app(robot.wsgi)
157 | 
158 | 然后按照 SAE 的要求编写好 `config.yaml` 文件就可以了。
159 | 可以参考 `示例仓库 <https://github.com/whtsky/WeRoBot-SAE-demo>`_
160 | 
161 | 如果你希望使用 SAE 提供的 KVDB 存储 Session 数据, 可以选择 :class:`werobot.session.saekvstorage` 作为你的 Session Storage.
162 | 
163 | 


--------------------------------------------------------------------------------
/docs/encryption.rst:
--------------------------------------------------------------------------------
 1 | 消息加解密
 2 | ==========
 3 | 
 4 | WeRoBot 支持对消息的加解密,即微信公众号的安全模式。
 5 | 在开启消息加解密功能之前,请先阅读微信官方的 `消息加解密说明 <https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1434696670>`_
 6 | 为 WeRoBot 开启消息加密功能,首先需要安装 ``cryptography`` ::
 7 | 
 8 |     pip install cryptography
 9 | 
10 | 之后, 你只需要将开发者 ID(`AppID`) 和微信公众平台后台设置的 `EncodingAESKey` 加到 WeRoBot 的 :ref:`Config` 里面就可以了 ::
11 | 
12 |     from werobot import WeRoBot
13 |     robot = WeRoBot()
14 |     robot.config["APP_ID"] = "Your AppID"
15 |     robot.config['ENCODING_AES_KEY'] = 'Your Encoding AES Key'
16 | 
17 | WeRoBot 之后会自动进行消息的加解密工作。
18 | 


--------------------------------------------------------------------------------
/docs/error-page.rst:
--------------------------------------------------------------------------------
 1 | 错误页面
 2 | ==========
 3 | ``WeRoBot`` 自带了一个错误页面,它将会在 Signature 验证不通过的时候返回错误页面。
 4 | 
 5 | 定制错误页面
 6 | ------------
 7 | 如果你想为 ``WeRoBot`` 指定 Signature 验证不通过时显示的错误页面,可以这么做: ::
 8 | 
 9 |     @robot.error_page
10 |     def make_error_page(url):
11 |         return "<h1>喵喵喵 %s 不是给麻瓜访问的快走开</h1>" % url
12 | 


--------------------------------------------------------------------------------
/docs/handlers.rst:
--------------------------------------------------------------------------------
  1 | Handler
  2 | =========
  3 | 
  4 | 
  5 | WeRoBot会将合法的请求发送给 handlers 依次执行。
  6 | 
  7 | 如果某一个 Handler 返回了非空值, WeRoBot 就会根据这个值创建回复,后面的 handlers 将不会被执行。
  8 | 
  9 | 你可以通过修饰符或 :meth:`~werobot.robot.BaseRoBot.add_handler` 添加 handler ::
 10 | 
 11 |     import werobot
 12 | 
 13 |     robot = werobot.WeRoBot(token='tokenhere')
 14 | 
 15 |     # 通过修饰符添加handler
 16 |     @robot.handler
 17 |     def echo(message):
 18 |         return 'Hello World!'
 19 | 
 20 |     # 通过`add_handler`添加handler
 21 |     def echo(message):
 22 |         return 'Hello World!'
 23 |     robot.add_handler(echo)
 24 | 
 25 | 类型过滤
 26 | ------------
 27 | 
 28 | 在大多数情况下, 一个 Handler 并不能处理所有类型的消息。幸运的是, WeRoBot 可以帮你过滤收到的消息。
 29 | 
 30 | 只想处理被新用户关注的消息?::
 31 | 
 32 |     import werobot
 33 | 
 34 |     robot = werobot.WeRoBot(token='tokenhere')
 35 | 
 36 |     @robot.subscribe
 37 |     def subscribe(message):
 38 |         return 'Hello My Friend!'
 39 | 
 40 |     robot.run()
 41 | 
 42 | 或者,你的 handler 只能处理文本? ::
 43 | 
 44 |     import werobot
 45 | 
 46 |     robot = werobot.WeRoBot(token='tokenhere')
 47 | 
 48 |     @robot.text
 49 |     def echo(message):
 50 |         return message.content
 51 | 
 52 |     robot.run()
 53 | 
 54 | 在 WeRobot 中我们把请求分成了 Message 和 Event 两种类型,针对两种类型的请求分别有不同的 Handler。
 55 | 
 56 | ========================================================================================================  =========================================
 57 | 修饰符                                                                                                       类型
 58 | ========================================================================================================  =========================================
 59 | :func:`robot.text <werobot.robot.BaseRoBot.text>`                                                           文本 (Message)
 60 | :func:`robot.image <werobot.robot.BaseRoBot.image>`                                                         图像 (Message)
 61 | :func:`robot.location <werobot.robot.BaseRoBot.location>`                                                   位置 (Message)
 62 | :func:`robot.link <werobot.robot.BaseRoBot.link>`                                                           链接 (Message)
 63 | :func:`robot.voice <werobot.robot.BaseRoBot.voice>`                                                         语音 (Message)
 64 | :func:`robot.unknown <werobot.robot.BaseRoBot.unknown>`                                                     未知类型 (Message)
 65 | :func:`robot.subscribe <werobot.robot.BaseRoBot.subscribe>`                                                 被关注 (Event)
 66 | :func:`robot.unsubscribe <werobot.robot.BaseRoBot.unsubscribe>`                                             被取消关注 (Event)
 67 | :func:`robot.click <werobot.robot.BaseRoBot.click>`                                                         自定义菜单事件 (Event)
 68 | :func:`robot.view <werobot.robot.BaseRoBot.view>`                                                           链接 (Event)
 69 | :func:`robot.scancode_push <werobot.robot.BaseRoBot.scancode_push>`                                         扫描推送 (Event)
 70 | :func:`robot.scancode_waitmsg <werobot.robot.BaseRoBot.scancode_waitmsg>`                                   扫描弹消息 (Event)
 71 | :func:`robot.pic_sysphoto <werobot.robot.BaseRoBot.pic_sysphoto>`                                           弹出系统拍照发图(Event)
 72 | :func:`robot.pic_photo_or_album <werobot.robot.BaseRoBot.pic_photo_or_album>`                               弹出拍照或者相册发图(Event)
 73 | :func:`robot.pic_weixin <werobot.robot.BaseRoBot.pic_weixin>`                                               弹出微信相册发图器(Event)
 74 | :func:`robot.location_select <werobot.robot.BaseRoBot.location_select>`                                     弹出地理位置选择器(Event)
 75 | :func:`robot.scan <werobot.robot.BaseRoBot.scan>`                                                           已关注扫描二维码(Event)
 76 | :func:`robot.user_scan_product <werobot.robot.BaseRoBot.user_scan_product>`                                 打开商品主页事件推送(Event)
 77 | :func:`robot.user_scan_product_enter_session <werobot.robot.BaseRoBot.user_scan_product_enter_session>`     进入公众号事件推送(Event)
 78 | :func:`robot.user_scan_product_async <werobot.robot.BaseRoBot.user_scan_product_async>`                     地理位置信息异步推送(Event)
 79 | :func:`robot.user_scan_product_verify_action <werobot.robot.BaseRoBot.user_scan_product_verify_action>`     商品审核结果推送(Event)
 80 | :func:`robot.card_pass_check <werobot.robot.BaseRoBot.card_pass_check>`                                     卡券通过审核 (Event)
 81 | :func:`robot.card_not_pass_check <werobot.robot.BaseRoBot.card_not_pass_check>`                             卡券未通过审核 (Event)
 82 | :func:`robot.user_get_card <werobot.robot.BaseRoBot.user_get_card>`                                         用户领取卡券 (Event)
 83 | :func:`robot.user_gifting_card <werobot.robot.BaseRoBot.user_gifting_card>`                                 用户转赠卡券 (Event)
 84 | :func:`robot.user_del_card <werobot.robot.BaseRoBot.user_del_card>`                                         用户删除卡券 (Event)
 85 | :func:`robot.user_consume_card <werobot.robot.BaseRoBot.user_consume_card>`                                 卡券被核销 (Event)
 86 | :func:`robot.user_pay_from_pay_cell <werobot.robot.BaseRoBot.user_pay_from_pay_cell>`                       微信买单完成 (Event)
 87 | :func:`robot.user_view_card <werobot.robot.BaseRoBot.user_view_card>`                                       用户进入会员卡 (Event)
 88 | :func:`robot.user_enter_session_from_card <werobot.robot.BaseRoBot.user_enter_session_from_card>`           用户卡券里点击查看公众号进入会话 (Event)
 89 | :func:`robot.update_member_card <werobot.robot.BaseRoBot.update_member_card>`                               会员卡积分余额发生变动 (Event)
 90 | :func:`robot.card_sku_remind <werobot.robot.BaseRoBot.card_sku_remind>`                                     库存警告 (Event)
 91 | :func:`robot.card_pay_order <werobot.robot.BaseRoBot.card_pay_order>`                                       券点发生变动 (Event)
 92 | :func:`robot.templatesendjobfinish_event <werobot.robot.BaseRoBot.templatesendjobfinish_event>`             模板信息推送事件 (Event)
 93 | :func:`robot.submit_membercard_user_info <werobot.robot.BaseRoBot.submit_membercard_user_info>`             激活卡券 (Event)
 94 | :func:`robot.location_event <werobot.robot.BaseRoBot.location_event>`                                       上报位置 (Event)
 95 | :func:`robot.unknown_event <werobot.robot.BaseRoBot.unknown_event>`                                         未知类型 (Event)
 96 | ========================================================================================================  =========================================
 97 | 
 98 | 额,这个 handler 想处理文本信息和地理位置信息? ::
 99 | 
100 |     import werobot
101 | 
102 |     robot = werobot.WeRoBot(token='tokenhere')
103 | 
104 |     @robot.text
105 |     @robot.location
106 |     def handler(message):
107 |         # Do what you love to do
108 |         pass
109 | 
110 |     robot.run()
111 | 
112 | 当然,你也可以用 :meth:`~werobot.robot.BaseRoBot.add_handler` 函数添加handler,就像这样::
113 | 
114 |     import werobot
115 | 
116 |     robot = werobot.WeRoBot(token='tokenhere')
117 | 
118 |     def handler(message):
119 |         # Do what you love to do
120 |         pass
121 | 
122 |     robot.add_handler(handler, type='text')
123 |     robot.add_handler(handler, type='location')
124 | 
125 |     robot.run()
126 | 
127 | .. note:: 通过 ``robot.handler`` 添加的 handler 将收到所有信息;只有在其他 handler 没有给出返回值的情况下, 通过 ``robot.handler`` 添加的 handler 才会被调用。
128 | 
129 | robot.key_click —— 回应自定义菜单
130 | ---------------------------------
131 | 
132 | :meth:`~werobot.robot.BaseRoBot.key_click` 是对 :meth:`~werobot.robot.BaseRoBot.click` 修饰符的改进。
133 | 
134 | 如果你在自定义菜单中定义了一个 Key 为 ``abort`` 的菜单,响应这个菜单的 handler 可以写成这样 ::
135 | 
136 |     @robot.key_click("abort")
137 |     def abort():
138 |         return "I'm a robot"
139 | 
140 | 当然,如果你不喜欢用 :meth:`~werobot.robot.BaseRoBot.key_click` ,也可以写成这样 ::
141 | 
142 |     @robot.click
143 |     def abort(message):
144 |         if message.key == "abort":
145 |             return "I'm a robot"
146 | 
147 | 两者是等价的。
148 | 
149 | robot.filter ——  回应有指定文本的消息
150 | -------------------------------------
151 | 
152 | :meth:`~werobot.robot.BaseRoBot.filter` 是对 :meth:`~werobot.robot.BaseRoBot.text` 修饰符的改进。
153 | 
154 | 现在你可以写这样的代码 ::
155 | 
156 |     @robot.filter("a")
157 |     def a():
158 |         return "正文为 a "
159 | 
160 |     import re
161 | 
162 | 
163 |     @robot.filter(re.compile(".*?bb.*?"))
164 |     def b():
165 |         return "正文中含有 bb "
166 | 
167 |     @robot.filter(re.compile(".*?c.*?"), "d")
168 |     def c():
169 |         return "正文中含有 c 或正文为 d"
170 | 
171 |     @robot.filter(re.compile("(.*)?e(.*)?"), "f")
172 |     def d(message, session, match):
173 |         if match:
174 |             return "正文为 " + match.group(1) + "e" + match.group(2)
175 |         return "正文为 f"
176 | 
177 | 这段代码等价于 ::
178 | 
179 |     @robot.text
180 |     def a(message):
181 |         if message.content == "a":
182 |             return "正文为 a "
183 |     import re
184 | 
185 | 
186 |     @robot.text
187 |     def b(message):
188 |         if re.compile(".*?bb.*?").match(message.content):
189 |             return "正文中含有 b "
190 | 
191 |     @robot.text
192 |     def c(message):
193 |         if re.compile(".*?c.*?").match(message.content) or message.content == "d":
194 |             return "正文中含有 c 或正文为 d"
195 | 
196 |     @robot.text
197 |     def d(message):
198 |         match = re.compile("(.*)?e(.*)?").match(message.content)
199 |         if match:
200 |             return "正文为 " + match.group(1) + "e" + match.group(2)
201 |         if  message.content == "f":
202 |             return "正文为 f"
203 | 
204 | 如果你想通过修饰符以外的方法添加 filter,可以使用 :func:`~werobot.robot.BaseRoBot.add_filter` 方法 ::
205 | 
206 |     def say_hello():
207 |         return "hello!"
208 | 
209 |     robot.add_filter(func=say_hello, rules=["hello", "hi", re.compile(".*?hello.*?")])
210 | 
211 | 更多内容详见 :class:`werobot.robot.BaseRoBot`
212 | 


--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
 1 | WeRoBot
 2 | =======
 3 | 
 4 | 
 5 | WeRoBot 是一个微信公众号开发框架。
 6 | 
 7 | .. toctree::
 8 |     :maxdepth: 2
 9 | 
10 |     start
11 |     encryption
12 |     deploy
13 |     handlers
14 |     session
15 |     client
16 |     messages
17 |     events
18 |     replies
19 |     config
20 |     contrib
21 |     error-page
22 |     utils
23 |     contribution-guide
24 |     api
25 |     changelog
26 | 


--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
  1 | @ECHO OFF
  2 | 
  3 | REM Command file for Sphinx documentation
  4 | 
  5 | if "%SPHINXBUILD%" == "" (
  6 | 	set SPHINXBUILD=sphinx-build
  7 | )
  8 | set BUILDDIR=_build
  9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
 10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
 11 | if NOT "%PAPER%" == "" (
 12 | 	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
 13 | 	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
 14 | )
 15 | 
 16 | if "%1" == "" goto help
 17 | 
 18 | if "%1" == "help" (
 19 | 	:help
 20 | 	echo.Please use `make ^<target^>` where ^<target^> is one of
 21 | 	echo.  html       to make standalone HTML files
 22 | 	echo.  dirhtml    to make HTML files named index.html in directories
 23 | 	echo.  singlehtml to make a single large HTML file
 24 | 	echo.  pickle     to make pickle files
 25 | 	echo.  json       to make JSON files
 26 | 	echo.  htmlhelp   to make HTML files and a HTML help project
 27 | 	echo.  qthelp     to make HTML files and a qthelp project
 28 | 	echo.  devhelp    to make HTML files and a Devhelp project
 29 | 	echo.  epub       to make an epub
 30 | 	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
 31 | 	echo.  text       to make text files
 32 | 	echo.  man        to make manual pages
 33 | 	echo.  texinfo    to make Texinfo files
 34 | 	echo.  gettext    to make PO message catalogs
 35 | 	echo.  changes    to make an overview over all changed/added/deprecated items
 36 | 	echo.  linkcheck  to check all external links for integrity
 37 | 	echo.  doctest    to run all doctests embedded in the documentation if enabled
 38 | 	goto end
 39 | )
 40 | 
 41 | if "%1" == "clean" (
 42 | 	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
 43 | 	del /q /s %BUILDDIR%\*
 44 | 	goto end
 45 | )
 46 | 
 47 | if "%1" == "html" (
 48 | 	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
 49 | 	if errorlevel 1 exit /b 1
 50 | 	echo.
 51 | 	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
 52 | 	goto end
 53 | )
 54 | 
 55 | if "%1" == "dirhtml" (
 56 | 	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
 57 | 	if errorlevel 1 exit /b 1
 58 | 	echo.
 59 | 	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
 60 | 	goto end
 61 | )
 62 | 
 63 | if "%1" == "singlehtml" (
 64 | 	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
 65 | 	if errorlevel 1 exit /b 1
 66 | 	echo.
 67 | 	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
 68 | 	goto end
 69 | )
 70 | 
 71 | if "%1" == "pickle" (
 72 | 	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
 73 | 	if errorlevel 1 exit /b 1
 74 | 	echo.
 75 | 	echo.Build finished; now you can process the pickle files.
 76 | 	goto end
 77 | )
 78 | 
 79 | if "%1" == "json" (
 80 | 	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
 81 | 	if errorlevel 1 exit /b 1
 82 | 	echo.
 83 | 	echo.Build finished; now you can process the JSON files.
 84 | 	goto end
 85 | )
 86 | 
 87 | if "%1" == "htmlhelp" (
 88 | 	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
 89 | 	if errorlevel 1 exit /b 1
 90 | 	echo.
 91 | 	echo.Build finished; now you can run HTML Help Workshop with the ^
 92 | .hhp project file in %BUILDDIR%/htmlhelp.
 93 | 	goto end
 94 | )
 95 | 
 96 | if "%1" == "qthelp" (
 97 | 	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
 98 | 	if errorlevel 1 exit /b 1
 99 | 	echo.
100 | 	echo.Build finished; now you can run "qcollectiongenerator" with the ^
101 | .qhcp project file in %BUILDDIR%/qthelp, like this:
102 | 	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WeRoBot.qhcp
103 | 	echo.To view the help file:
104 | 	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WeRoBot.ghc
105 | 	goto end
106 | )
107 | 
108 | if "%1" == "devhelp" (
109 | 	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
110 | 	if errorlevel 1 exit /b 1
111 | 	echo.
112 | 	echo.Build finished.
113 | 	goto end
114 | )
115 | 
116 | if "%1" == "epub" (
117 | 	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
118 | 	if errorlevel 1 exit /b 1
119 | 	echo.
120 | 	echo.Build finished. The epub file is in %BUILDDIR%/epub.
121 | 	goto end
122 | )
123 | 
124 | if "%1" == "latex" (
125 | 	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
126 | 	if errorlevel 1 exit /b 1
127 | 	echo.
128 | 	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
129 | 	goto end
130 | )
131 | 
132 | if "%1" == "text" (
133 | 	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
134 | 	if errorlevel 1 exit /b 1
135 | 	echo.
136 | 	echo.Build finished. The text files are in %BUILDDIR%/text.
137 | 	goto end
138 | )
139 | 
140 | if "%1" == "man" (
141 | 	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
142 | 	if errorlevel 1 exit /b 1
143 | 	echo.
144 | 	echo.Build finished. The manual pages are in %BUILDDIR%/man.
145 | 	goto end
146 | )
147 | 
148 | if "%1" == "texinfo" (
149 | 	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
150 | 	if errorlevel 1 exit /b 1
151 | 	echo.
152 | 	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
153 | 	goto end
154 | )
155 | 
156 | if "%1" == "gettext" (
157 | 	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
158 | 	if errorlevel 1 exit /b 1
159 | 	echo.
160 | 	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
161 | 	goto end
162 | )
163 | 
164 | if "%1" == "changes" (
165 | 	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
166 | 	if errorlevel 1 exit /b 1
167 | 	echo.
168 | 	echo.The overview file is in %BUILDDIR%/changes.
169 | 	goto end
170 | )
171 | 
172 | if "%1" == "linkcheck" (
173 | 	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
174 | 	if errorlevel 1 exit /b 1
175 | 	echo.
176 | 	echo.Link check complete; look for any errors in the above output ^
177 | or in %BUILDDIR%/linkcheck/output.txt.
178 | 	goto end
179 | )
180 | 
181 | if "%1" == "doctest" (
182 | 	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
183 | 	if errorlevel 1 exit /b 1
184 | 	echo.
185 | 	echo.Testing of doctests in the sources finished, look at the ^
186 | results in %BUILDDIR%/doctest/output.txt.
187 | 	goto end
188 | )
189 | 
190 | :end
191 | 


--------------------------------------------------------------------------------
/docs/messages.rst:
--------------------------------------------------------------------------------
  1 | Message
  2 | ==========
  3 | 
  4 | Message 公共属性
  5 | ----------------
  6 | 
  7 | 除了 UnknownMessage, 每一种 Message 都包括以下属性:
  8 | 
  9 | =========== ===================================
 10 | name         value
 11 | =========== ===================================
 12 | message_id   消息id,64位整型
 13 | target       开发者账号( OpenID )
 14 | source       发送方账号( OpenID )
 15 | time         信息发送的时间,一个UNIX时间戳。
 16 | raw          信息的原始 XML 格式
 17 | =========== ===================================
 18 | 
 19 | TextMessage
 20 | ------------
 21 | 
 22 | TextMessage 的属性:
 23 | 
 24 | 
 25 | ======== ===================================
 26 | name      value
 27 | ======== ===================================
 28 | type      'text'
 29 | content   信息的内容
 30 | ======== ===================================
 31 | 
 32 | ImageMessage
 33 | -------------
 34 | 
 35 | ImageMessage 的属性:
 36 | 
 37 | ======= ==================================
 38 | name     value
 39 | ======= ==================================
 40 | type     'image'
 41 | img      图片网址。你可以从这个网址下到图片
 42 | ======= ==================================
 43 | 
 44 | LinkMessage
 45 | ------------
 46 | ============    ==================================
 47 | name             value
 48 | ============    ==================================
 49 | type             'link'
 50 | title            消息标题
 51 | description      消息描述
 52 | url              消息链接
 53 | ============    ==================================
 54 | 
 55 | 
 56 | LocationMessage
 57 | ----------------
 58 | 
 59 | LocationMessage 的属性:
 60 | 
 61 | ========= ===================================
 62 | name       value
 63 | ========= ===================================
 64 | type       'location'
 65 | location   一个元组。(纬度, 经度)
 66 | scale      地图缩放大小
 67 | label      地理位置信息
 68 | ========= ===================================
 69 | 
 70 | 
 71 | VoiceMessage
 72 | --------------------
 73 | 
 74 | VoiceMessage 的属性:
 75 | 
 76 | ============ =====================================
 77 | name          value
 78 | ============ =====================================
 79 | type          'voice'
 80 | media_id      消息媒体 ID
 81 | format        声音格式
 82 | recognition   语音识别结果
 83 | ============ =====================================
 84 | 
 85 | VideoMessage
 86 | --------------------
 87 | 
 88 | VideoMessage 的属性:
 89 | 
 90 | ================ =====================================
 91 | name              value
 92 | ================ =====================================
 93 | type              'video'
 94 | media_id          消息媒体 ID
 95 | thumb_media_id    视频缩略图媒体 ID
 96 | ================ =====================================
 97 | 
 98 | UnknownMessage
 99 | ---------------
100 | 
101 | UnknownMessage 的属性:
102 | 
103 | ========= =====================================
104 | name       value
105 | ========= =====================================
106 | type       'unknown'
107 | raw        请求的正文部分。标准的XML格式。
108 | ========= =====================================
109 | 
110 | .. note:: 如果你不为 WeRoBot 贡献代码,你完全可以无视掉 UnknownMessage 。在正常的使用中,WeRoBot应该不会收到 `UnknownMessage` ——除非 WeRoBot 停止开发。
111 | 


--------------------------------------------------------------------------------
/docs/replies.rst:
--------------------------------------------------------------------------------
  1 | 回复
  2 | ==============
  3 | 
  4 | 
  5 | 你可以在构建Reply时传入一个合法的 `Message` 对象来自动生成 `source` 和 `target` ::
  6 | 
  7 |     reply = TextReply(message=message, content='Hello!')
  8 | 
  9 | 
 10 | TextReply
 11 | -----------
 12 | 
 13 | `TextReply` 是简单的文本消息,构造函数的参数如下:
 14 | 
 15 | ========= ===================================
 16 | name       value
 17 | ========= ===================================
 18 | content    信息正文。
 19 | target     信息的目标用户。通常是机器人用户。
 20 | source     信息的来源用户。通常是发送信息的用户。
 21 | time       信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
 22 | ========= ===================================
 23 | 
 24 | .. note:: 如果你的handler返回了一个字符串, WeRoBot会自动将其转化为一个文本消息。
 25 | 
 26 | ImageReply
 27 | -----------
 28 | 
 29 | `ImageReply` 为回复图片消息,构造函数的参数如下:
 30 | 
 31 | ========= ===================================
 32 | name       value
 33 | ========= ===================================
 34 | media_id   通过素材管理接口上传多媒体文件,得到的id。
 35 | target     信息的目标用户。通常是机器人用户。
 36 | source     信息的来源用户。通常是发送信息的用户。
 37 | time       信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
 38 | ========= ===================================
 39 | 
 40 | VoiceReply
 41 | ----------
 42 | 
 43 | `VoiceReply` 为回复语音消息,构造函数的参数如下:
 44 | 
 45 | ========= ===================================
 46 | name       value
 47 | ========= ===================================
 48 | media_id   通过素材管理接口上传多媒体文件,得到的id。
 49 | target     信息的目标用户。通常是机器人用户。
 50 | source     信息的来源用户。通常是发送信息的用户。
 51 | time       信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
 52 | ========= ===================================
 53 | 
 54 | VideoReply
 55 | ----------
 56 | 
 57 | `VideoReply` 为回复视频消息,构造函数的参数如下:
 58 | 
 59 | ============ ===================================
 60 | name          value
 61 | ============ ===================================
 62 | media_id      通过素材管理接口上传多媒体文件,得到的id。
 63 | title         视频消息的标题。可为空。
 64 | description   视频消息的描述。可为空。
 65 | target        信息的目标用户。通常是机器人用户。
 66 | source        信息的来源用户。通常是发送信息的用户。
 67 | time          信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
 68 | ============ ===================================
 69 | 
 70 | 
 71 | ArticlesReply
 72 | ---------------
 73 | 
 74 | `ArticlesReply` 是图文消息,构造函数的参数如下:
 75 | 
 76 | ========= ===================================
 77 | name       value
 78 | ========= ===================================
 79 | content    信息正文。**可为空**。
 80 | target     信息的目标用户。通常是机器人用户。
 81 | source     信息的来源用户。通常是发送信息的用户。
 82 | time       信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
 83 | ========= ===================================
 84 | 
 85 | 你需要给 `ArticlesReply` 添加 `Article` 来增加图文。
 86 | `Article` 类位于 `werobot.reply.Article` 。
 87 | 
 88 | `Article` 的构造函数的参数如下:
 89 | 
 90 | ============ ===================================
 91 | name          value
 92 | ============ ===================================
 93 | title         标题
 94 | description   描述
 95 | img           图片链接
 96 | url           点击图片后跳转链接
 97 | ============ ===================================
 98 | 
 99 | 注意,微信公众平台对图片链接有特殊的要求,详情可以在
100 | `消息接口使用指南 <http://mp.weixin.qq.com/cgi-bin/readtemplate?t=wxm-callbackapi-doc&lang=zh_CN>`_ 里看到。
101 | 
102 | 在构造完一个 `Article` 后, 你需要通过 `ArticlesReply` 的 `add_article` 参数把它添加进去。就像这样: ::
103 | 
104 |     from werobot.replies import ArticlesReply, Article
105 |     reply = ArticlesReply(message=message)
106 |     article = Article(
107 |         title="WeRoBot",
108 |         description="WeRoBot是一个微信机器人框架",
109 |         img="https://github.com/apple-touch-icon-144.png",
110 |         url="https://github.com/whtsky/WeRoBot"
111 |     )
112 |     reply.add_article(article)
113 | 
114 | .. note:: 根据微信公众平台的 `最新公告 <https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&lang=zh_CN>`_,每个ArticlesReply中 **最多添加1个Article** 。
115 | 
116 | 你也可以让你的 handler 返回一个列表, 里面每一个元素都是一个长度为四的列表,
117 |  WeRoBot 会将其自动转为 ArticlesReply 。就像这样: ::
118 | 
119 |     import werobot
120 | 
121 |     robot = werobot.WeRoBot(token='tokenhere')
122 | 
123 |     @robot.text
124 |     def articles(message):
125 |         return [
126 |             [
127 |                 "title",
128 |                 "description",
129 |                 "img",
130 |                 "url"
131 |             ],
132 |             [
133 |                 "whtsky",
134 |                 "I wrote WeRoBot",
135 |                 "https://secure.gravatar.com/avatar/0024710771815ef9b74881ab21ba4173?s=420",
136 |                 "http://whouz.com/"
137 |             ]
138 |         ]
139 | 
140 |     robot.run()
141 | 
142 | 
143 | MusicReply
144 | -----------
145 | 
146 | `MusicReply` 是音乐消息,构造函数的参数如下:
147 | 
148 | =============    ======================================================================
149 | name              value
150 | =============    ======================================================================
151 | target            信息的目标用户。通常是机器人用户。
152 | source            信息的来源用户。通常是发送信息的用户。
153 | time              信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
154 | title             标题
155 | description       描述
156 | url               音乐链接
157 | hq_url            高质量音乐链接,WIFI环境优先使用该链接播放音乐。可为空 [3]_
158 | =============    ======================================================================
159 | 
160 | 你也可以让你的 handler 返回一个长度为三或四的列表, [3]_
161 |  WeRoBot 会将其自动转为 MusicReply 。就像这样: ::
162 | 
163 |     import werobot
164 | 
165 |     robot = werobot.WeRoBot(token='tokenhere')
166 | 
167 |     @robot.text
168 |     def music(message):
169 |         return [
170 |             "title",
171 |             "description",
172 |             "music_url",
173 |             "hq_music_url"
174 |             ]
175 | 
176 |     @robot.text
177 |     def music2(message):
178 |         return [
179 |             "微信你不懂爱",
180 |             "龚琳娜最新力作",
181 |             "http://weixin.com/budongai.mp3",
182 |             ]
183 | 
184 |     robot.run()
185 | 
186 | 
187 | .. [3] 如果你省略了高质量音乐链接的地址, WeRoBot 会自动将音乐链接的地址用于高质量音乐链接。
188 | 
189 | TransferCustomerServiceReply
190 | -----------------------------
191 | 
192 | 将消息转发到多客服,构造函数的参数如下:
193 | 
194 | =============    ======================================================================
195 | name              value
196 | =============    ======================================================================
197 | target            信息的目标用户。通常是机器人用户。
198 | source            信息的来源用户。通常是发送信息的用户。
199 | time              信息发送的时间,一个UNIX时间戳。默认情况下会使用当前时间。
200 | account           指定会话接入的客服账号,可以没有此参数,没有时会自动分配给可用客服。
201 | =============    ======================================================================
202 | 
203 | SuccessReply
204 | ---------------
205 | 给微信服务器回复 "success"。
206 | 假如服务器无法保证在五秒内处理并回复,需要回复 `SuccessReply` ,这样微信服务器才不会对此作任何处理,并且不会发起重试。
207 | 


--------------------------------------------------------------------------------
/docs/session.rst:
--------------------------------------------------------------------------------
 1 | Session
 2 | ==========
 3 | 
 4 | 你可以通过 Session 实现用户状态的记录。
 5 | 
 6 | 一个简单的使用 Session 的 Demo ::
 7 | 
 8 |     from werobot import WeRoBot
 9 |     robot = WeRoBot(token=werobot.utils.generate_token())
10 | 
11 |     @robot.text
12 |     def first(message, session):
13 |         if 'last' in session:
14 |             return
15 |         session['last'] = message.content
16 |         return message.content
17 | 
18 |     robot.run()
19 | 
20 | 开启/关闭 Session
21 | -----------------
22 | 
23 | Session 在 WeRoBot 中默认开启, 并使用 :class:`werobot.session.sqlitestorage.SQLiteStorage` 作为存储后端。 如果想要更换存储后端, 可以修改 :doc:`config` 中的 ``SESSION_STORAGE`` ::
24 | 
25 |     from werobot import WeRoBot
26 |     from werobot.session.filestorage import FileStorage
27 |     robot = WeRoBot(token="token")
28 |     robot.config['SESSION_STORAGE'] = FileStorage()
29 | 
30 | 
31 | 如果想要关闭 Session 功能, 只需把 ``SESSION_STORAGE`` 设为 False 即可 ::
32 | 
33 |     from werobot import WeRoBot
34 |     robot = WeRoBot(token="token")
35 |     robot.config['SESSION_STORAGE'] = False
36 | 
37 | 修改 Handler 以使用 Session
38 | --------------------------------
39 | 
40 | 没有打开 Session 的时候,一个标准的 WeRoBot Handler 应该是这样的 ::
41 | 
42 |     @robot.text
43 |     def hello(message):
44 |         return "Hello!"
45 | 
46 | 而在打开 Session 之后, 如果你的 handler 不需要使用 Session ,可以保持不变; 如果需要使用 Session ,则这个 Handler 需要修改为接受第二个参数: ``session`` ::
47 | 
48 |     @robot.subscribe_event
49 |     def intro(message):
50 |         return "Hello!"
51 | 
52 |     @robot.text
53 |     def hello(message, session):
54 |         count = session.get("count", 0) + 1
55 |         session["count"] = count
56 |         return "Hello! You have sent %s messages to me" % count
57 | 
58 | 传入的 ``session`` 参数是一个标准的 Python 字典。
59 | 
60 | 更多可用的 Session Storage 详见 :ref:`Session 对象`。
61 | 


--------------------------------------------------------------------------------
/docs/start.rst:
--------------------------------------------------------------------------------
 1 | 入门
 2 | =============
 3 | 
 4 | 
 5 | Hello World
 6 | -------------
 7 | 最简单的Hello World, 会给收到的每一条信息回复 `Hello World` ::
 8 | 
 9 |     import werobot
10 | 
11 |     robot = werobot.WeRoBot(token='tokenhere')
12 | 
13 |     @robot.handler
14 |     def hello(message):
15 |         return 'Hello World!'
16 | 
17 |     # 让服务器监听在 0.0.0.0:80
18 |     robot.config['HOST'] = '0.0.0.0'
19 |     robot.config['PORT'] = 80
20 |     robot.run()
21 | 
22 | 消息处理
23 | --------------
24 | WeRoBot 会解析微信服务器发来的消息, 并将消息转换成成 :ref:`Message` 或者是 :ref:`Event` 。
25 | :ref:`Message` 表示用户发来的消息,如文本消息、图片消息; :ref:`Event` 则表示用户触发的事件, 如关注事件、扫描二维码事件。
26 | 在消息解析、转换完成后, WeRoBot 会将消息转交给 :ref:`Handler` 进行处理,并将 :ref:`Handler` 的返回值返回给微信服务器。
27 | 
28 | 在刚才的 Hello World 中, 我们编写的 ::
29 | 
30 |     @robot.handler
31 |     def hello(message):
32 |         return 'Hello World!'
33 | 
34 | 就是一个简单的 :ref:`Handler` , `@robot.handler` 意味着 `robot` 会将所有接收到的消息( 包括 :ref:`Message` 和 :ref:`Event` ) 都转交给这个 :ref:`Handler` 来处理。
35 | 当然, 你也可以编写一些只能处理特定消息的 :ref:`Handler` ::
36 | 
37 |     # @robot.text 修饰的 Handler 只处理文本消息
38 |     @robot.text
39 |     def echo(message):
40 |         return message.content
41 | 
42 |     # @robot.image 修饰的 Handler 只处理图片消息
43 |     @robot.image
44 |     def img(message):
45 |         return message.img
46 | 
47 | 使用 Session 记录用户状态
48 | -------------------------
49 | 
50 | WeRoBot 提供了 :ref:`Session` 功能, 可以让你方便的记录用户状态。
51 | 比如, 这个 Handler 可以判断发消息的用户之前有没有发送过消息 ::
52 | 
53 |     @robot.text
54 |     def first(message, session):
55 |         if 'first' in session:
56 |             return '你之前给我发过消息'
57 |         session['first'] = True
58 |         return '你之前没给我发过消息'
59 | 
60 | Session 功能默认开启, 并使用 SQLite 存储 Session 数据。 详情请参考 :doc:`session` 文档
61 | 
62 | 创建自定义菜单
63 | --------------
64 | 
65 | 自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。 :class:`werobot.client.Client` 封装了微信的部分 API 接口,我们可以使用 :func:`werobot.client.Client.create_menu` 来创建自定义菜单。
66 | 在使用 Client 之前, 我们需要先提供微信公众平台内的 AppID 和 AppSecret ::
67 | 
68 |     from werobot import WeRoBot
69 |     robot = WeRoBot()
70 |     robot.config["APP_ID"] = "你的 AppID"
71 |     robot.config["APP_SECRET"] = "你的 AppSecret"
72 | 
73 |     client = robot.client
74 | 
75 | 然后, 我们就可以创建自定义菜单了 ::
76 | 
77 |     client.create_menu({
78 |         "button":[{	
79 |              "type": "click",
80 |              "name": "今日歌曲",
81 |              "key": "music"
82 |         }]
83 |     })
84 | 
85 | 注意以上代码只需要运行一次就可以了。在创建完自定义菜单之后, 我们还需要写一个 :ref:`handler` 来响应菜单的点击操作 ::
86 | 
87 |     @robot.key_click("music")
88 |     def music(message):
89 |         return '你点击了“今日歌曲”按钮'
90 | 
91 | 


--------------------------------------------------------------------------------
/docs/utils.rst:
--------------------------------------------------------------------------------
 1 | 小工具
 2 | ===============
 3 | 
 4 | 
 5 | Token 生成器
 6 | ---------------
 7 | 
 8 | WeRoBot帮你准备了一个Token生成器: ::
 9 | 
10 |     import werobot.utils
11 | 
12 |     print(werobot.utils.generate_token())
13 | 
14 | 
15 | 


--------------------------------------------------------------------------------
/example/hello_world.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | import werobot
 4 | 
 5 | robot = werobot.WeRoBot(token='tokenhere')
 6 | 
 7 | 
 8 | @robot.filter("帮助")
 9 | def show_help(message):
10 |     return """
11 |     帮助
12 |     XXXXX
13 |     """
14 | 
15 | 
16 | @robot.text
17 | def hello_world(message):
18 |     return 'Hello World!'
19 | 
20 | 
21 | robot.run()
22 | 


--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests
3 | addopts = -rw
4 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | bottle
2 | requests
3 | xmltodict
4 | 


--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | release = sdist --formats=zip,gztar bdist_wheel
3 | test=pytest
4 | 
5 | [bdist_wheel]
6 | universal = 1
7 | 


--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | # coding=utf-8
 3 | 
 4 | import io
 5 | import werobot
 6 | 
 7 | from setuptools import setup, find_packages
 8 | from setuptools.command.test import test as TestCommand
 9 | 
10 | 
11 | class PyTest(TestCommand):
12 |     user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
13 | 
14 |     def initialize_options(self):
15 |         TestCommand.initialize_options(self)
16 |         self.pytest_args = ""
17 | 
18 |     def run_tests(self):
19 |         import shlex
20 | 
21 |         # import here, cause outside the eggs aren't loaded
22 |         import pytest
23 | 
24 |         errno = pytest.main(shlex.split(self.pytest_args))
25 |         sys.exit(errno)
26 | 
27 | 
28 | with io.open("README.rst", encoding="utf8") as f:
29 |     readme = f.read()
30 | readme = readme.replace("latest", "v" + werobot.__version__)
31 | 
32 | install_requires = open("requirements.txt").readlines()
33 | setup(
34 |     name='WeRoBot',
35 |     version=werobot.__version__,
36 |     author=werobot.__author__,
37 |     author_email='whtsky@me.com',
38 |     url='https://github.com/offu/WeRoBot',
39 |     packages=find_packages(),
40 |     keywords="wechat weixin werobot",
41 |     description='WeRoBot: writing WeChat Offical Account Robots with fun',
42 |     long_description=readme,
43 |     install_requires=install_requires,
44 |     include_package_data=True,
45 |     license='MIT License',
46 |     classifiers=[
47 |         'Development Status :: 4 - Beta',
48 |         'License :: OSI Approved :: MIT License',
49 |         'Operating System :: MacOS',
50 |         'Operating System :: POSIX',
51 |         'Operating System :: POSIX :: Linux',
52 |         'Programming Language :: Python',
53 |         'Programming Language :: Python :: 3.6',
54 |         'Programming Language :: Python :: 3.7',
55 |         'Programming Language :: Python :: 3.8',
56 |         'Programming Language :: Python :: 3.9',
57 |         'Programming Language :: Python :: Implementation :: CPython',
58 |         'Intended Audience :: Developers',
59 |         'Topic :: Software Development :: Libraries',
60 |         'Topic :: Software Development :: Libraries :: Python Modules',
61 |         'Topic :: Utilities',
62 |     ],
63 |     tests_require=['pytest'],
64 |     cmdclass={"pytest": PyTest},
65 |     extras_require={'crypto': ["cryptography"]},
66 |     package_data={'werobot': ['contrib/*.html']}
67 | )
68 | 


--------------------------------------------------------------------------------
/tests/client_config.py:
--------------------------------------------------------------------------------
1 | APP_ID = "123"
2 | APP_SECRET = "321"
3 | 


--------------------------------------------------------------------------------
/tests/django_test_env/django_test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/offu/WeRoBot/7b3d62d7ae126fc8bc9e033e6358dec2852d1d5a/tests/django_test_env/django_test/__init__.py


--------------------------------------------------------------------------------
/tests/django_test_env/django_test/settings.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Django settings for django_test project.
  3 | 
  4 | Generated by 'django-admin startproject' using Django 1.9.8.
  5 | 
  6 | For more information on this file, see
  7 | https://docs.djangoproject.com/en/1.9/topics/settings/
  8 | 
  9 | For the full list of settings and their values, see
 10 | https://docs.djangoproject.com/en/1.9/ref/settings/
 11 | """
 12 | 
 13 | import os
 14 | 
 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 17 | 
 18 | # Quick-start development settings - unsuitable for production
 19 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
 20 | 
 21 | # SECURITY WARNING: keep the secret key used in production secret!
 22 | SECRET_KEY = 'eg@1m&jgpt)t1$l&=vw3r*v@$j14zdiih8!d17_tvd!3ksb43t'
 23 | 
 24 | # SECURITY WARNING: don't run with debug turned on in production!
 25 | DEBUG = True
 26 | 
 27 | ALLOWED_HOSTS = []
 28 | 
 29 | # Application definition
 30 | 
 31 | INSTALLED_APPS = [
 32 |     'django.contrib.admin',
 33 |     'django.contrib.auth',
 34 |     'django.contrib.contenttypes',
 35 |     'django.contrib.sessions',
 36 |     'django.contrib.messages',
 37 |     'django.contrib.staticfiles',
 38 | ]
 39 | 
 40 | MIDDLEWARE_CLASSES = [
 41 |     'django.contrib.sessions.middleware.SessionMiddleware',
 42 |     'django.middleware.common.CommonMiddleware',
 43 |     'django.middleware.csrf.CsrfViewMiddleware',
 44 |     'django.contrib.auth.middleware.AuthenticationMiddleware',
 45 |     'django.contrib.messages.middleware.MessageMiddleware',
 46 |     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 47 | ]
 48 | 
 49 | ROOT_URLCONF = 'django_test.urls'
 50 | 
 51 | TEMPLATES = [
 52 |     {
 53 |         'BACKEND': 'django.template.backends.django.DjangoTemplates',
 54 |         'DIRS': [],
 55 |         'APP_DIRS': True,
 56 |         'OPTIONS': {
 57 |             'context_processors': [
 58 |                 'django.template.context_processors.debug',
 59 |                 'django.template.context_processors.request',
 60 |                 'django.contrib.auth.context_processors.auth',
 61 |                 'django.contrib.messages.context_processors.messages',
 62 |             ],
 63 |         },
 64 |     },
 65 | ]
 66 | 
 67 | WSGI_APPLICATION = 'django_test.wsgi.application'
 68 | 
 69 | # Database
 70 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases
 71 | 
 72 | DATABASES = {
 73 |     'default': {
 74 |         'ENGINE': 'django.db.backends.sqlite3',
 75 |         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
 76 |     }
 77 | }
 78 | 
 79 | # Password validation
 80 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
 81 | 
 82 | AUTH_PASSWORD_VALIDATORS = [
 83 |     {
 84 |         'NAME': 'django.contrib.auth.'
 85 |         'password_validation.UserAttributeSimilarityValidator',
 86 |     },
 87 |     {
 88 |         'NAME': 'django.contrib.auth.'
 89 |         'password_validation.MinimumLengthValidator',
 90 |     },
 91 |     {
 92 |         'NAME': 'django.contrib.auth.'
 93 |         'password_validation.CommonPasswordValidator',
 94 |     },
 95 |     {
 96 |         'NAME': 'django.contrib.auth.'
 97 |         'password_validation.NumericPasswordValidator',
 98 |     },
 99 | ]
100 | 
101 | # Internationalization
102 | # https://docs.djangoproject.com/en/1.9/topics/i18n/
103 | 
104 | LANGUAGE_CODE = 'en-us'
105 | 
106 | TIME_ZONE = 'UTC'
107 | 
108 | USE_I18N = True
109 | 
110 | USE_L10N = True
111 | 
112 | USE_TZ = True
113 | 
114 | # Static files (CSS, JavaScript, Images)
115 | # https://docs.djangoproject.com/en/1.9/howto/static-files/
116 | 
117 | STATIC_URL = '/static/'
118 | 


--------------------------------------------------------------------------------
/tests/django_test_env/django_test/urls.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | """django_test URL Configuration
 3 | 
 4 | The `urlpatterns` list routes URLs to views. For more information please see:
 5 |     https://docs.djangoproject.com/en/1.9/topics/http/urls/
 6 | Examples:
 7 | Function views
 8 |     1. Add an import:  from my_app import views
 9 |     2. Add a URL to urlpatterns:  url(r'^
#39;, views.home, name='home')
10 | Class-based views
11 |     1. Add an import:  from other_app.views import Home
12 |     2. Add a URL to urlpatterns:  url(r'^
#39;, Home.as_view(), name='home')
13 | Including another URLconf
14 |     1. Import the include() function: from django.conf.urls import url, include
15 |     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
16 | """
17 | from django.conf.urls import url
18 | from django.contrib import admin
19 | from werobot import WeRoBot
20 | from werobot.contrib.django import make_view
21 | from werobot.utils import generate_token
22 | 
23 | robot = WeRoBot(
24 |     SESSION_STORAGE=False,
25 |     token="TestDjango",
26 |     app_id="9998877",
27 |     encoding_aes_key=generate_token(32)
28 | )
29 | 
30 | 
31 | @robot.text
32 | def text_handler():
33 |     return 'hello'
34 | 
35 | 
36 | @robot.error_page
37 | def make_error_page(url):
38 |     return '喵'
39 | 
40 | 
41 | urlpatterns = [
42 |     url(r'^admin/', admin.site.urls),
43 |     url(r'^robot/', make_view(robot))
44 | ]
45 | 


--------------------------------------------------------------------------------
/tests/django_test_env/django_test/wsgi.py:
--------------------------------------------------------------------------------
 1 | """
 2 | WSGI config for django_test project.
 3 | 
 4 | It exposes the WSGI callable as a module-level variable named ``application``.
 5 | 
 6 | For more information on this file, see
 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
 8 | """
 9 | 
10 | import os
11 | 
12 | from django.core.wsgi import get_wsgi_application
13 | 
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_test.settings")
15 | 
16 | application = get_wsgi_application()
17 | 


--------------------------------------------------------------------------------
/tests/django_test_env/manage.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | import os
 3 | import sys
 4 | 
 5 | if __name__ == "__main__":
 6 |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_test.settings")
 7 | 
 8 |     from django.core.management import execute_from_command_line
 9 | 
10 |     execute_from_command_line(sys.argv)
11 | 


--------------------------------------------------------------------------------
/tests/messages/test_entries.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.messages.entries import get_value, StringEntry, FloatEntry, IntEntry
 4 | from werobot.utils import to_text
 5 | 
 6 | 
 7 | class NoNameMessage(object):
 8 |     test_int = IntEntry("TestInt")
 9 |     test_string_to_int = IntEntry("TestStringToInt")
10 |     test_float_to_int = IntEntry("TestFloatToInt")
11 |     test_int_none = IntEntry("MIAOMIAOMIAO")
12 | 
13 |     test_float = FloatEntry("TestFloat")
14 |     test_string_to_float = FloatEntry("TestStringToFloat")
15 |     test_float_none = FloatEntry("WANGWANG")
16 | 
17 |     test_string = StringEntry("TestString")
18 |     test_int_to_string = StringEntry("TestIntToString")
19 |     test_float_to_string = StringEntry("TestFloatToString")
20 |     test_chinese = StringEntry("TestChinese")
21 |     test_string_none = StringEntry("HAHAHA")
22 | 
23 |     def __init__(self):
24 |         message = {
25 |             "TestInt": 123,
26 |             "TestFloat": 0.00001,
27 |             "TestString": "hello",
28 |             "TestStringToInt": "123",
29 |             "TestFloatToInt": 123.000,
30 |             "TestStringToFloat": "0.00001",
31 |             "TestIntToString": 123,
32 |             "TestFloatToString": 0.00001,
33 |             "TestChinese": "喵",
34 |         }
35 |         self.__dict__.update(message)
36 | 
37 | 
38 | t = NoNameMessage()
39 | 
40 | 
41 | def test_int_entry():
42 |     assert isinstance(t.test_int, int)
43 |     assert t.test_int == 123
44 |     assert isinstance(t.test_string_to_int, int)
45 |     assert t.test_string_to_int == 123
46 |     assert isinstance(t.test_float_to_int, int)
47 |     assert t.test_float_to_int == 123
48 |     assert t.test_int_none is None
49 | 
50 | 
51 | def test_float_entry():
52 |     assert isinstance(t.test_float, float)
53 |     assert t.test_float == 0.00001
54 |     assert isinstance(t.test_string_to_float, float)
55 |     assert t.test_string_to_float == 0.00001
56 |     assert t.test_float_none is None
57 | 
58 | 
59 | def test_string_entry():
60 |     assert isinstance(t.test_string, str)
61 |     assert t.test_string == "hello"
62 |     assert isinstance(t.test_int_to_string, str)
63 |     assert t.test_int_to_string == "123"
64 |     assert isinstance(t.test_float_to_string, str)
65 |     assert t.test_float_to_string == "1e-05"
66 |     assert isinstance(t.test_chinese, str)
67 |     assert t.test_chinese == to_text("喵")
68 |     assert t.test_string_none is None
69 | 
70 | 
71 | class FakeIntance:
72 |     pass
73 | 
74 | 
75 | def test_get_value():
76 |     instance = FakeIntance()
77 |     instance.b = 6
78 |     instance.a = {'c': 'd'}
79 |     assert get_value(instance, 'd', 'default') == 'default'
80 |     assert get_value(instance, 'b', 'default') == 6
81 |     assert get_value(instance, 'a.c') == 'd'
82 | 


--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | 
 3 | from werobot import WeRoBot
 4 | from werobot.config import Config
 5 | from werobot.utils import generate_token
 6 | 
 7 | basedir = os.path.dirname(os.path.abspath(__file__))
 8 | 
 9 | TOKEN = "123"
10 | 
11 | 
12 | def test_from_pyfile():
13 |     config = Config()
14 |     assert "TOKEN" not in config
15 |     config.from_pyfile(os.path.join(basedir, "test_config.py"))
16 |     assert config["TOKEN"] == "123"
17 | 
18 | 
19 | def test_from_object():
20 |     config = Config()
21 |     config.from_pyfile(os.path.join(basedir, "test_config.py"))
22 | 
23 |     class ConfigObject:
24 |         TOKEN = "456"
25 | 
26 |     config.from_object(ConfigObject())
27 |     assert config["TOKEN"] == "456"
28 | 
29 | 
30 | def test_config_attribute():
31 |     robot = WeRoBot(SESSION_STORAGE=False)
32 |     assert not robot.token
33 |     token = generate_token()
34 |     robot.config["TOKEN"] = token
35 |     assert robot.token == token
36 | 
37 |     token = generate_token()
38 |     robot.token = token
39 |     assert robot.config["TOKEN"] == token
40 | 


--------------------------------------------------------------------------------
/tests/test_contrib.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | 
  3 | import os
  4 | import random
  5 | import sys
  6 | import time
  7 | 
  8 | import pytest
  9 | import tornado
 10 | import webtest
 11 | from tornado.testing import AsyncHTTPSTestCase
 12 | from webtest.app import AppError
 13 | 
 14 | from werobot.parser import parse_xml, process_message
 15 | from werobot.utils import generate_token, get_signature
 16 | 
 17 | 
 18 | @pytest.fixture
 19 | def wsgi_tester():
 20 |     def tester(app, token, endpoint):
 21 |         test_app = webtest.TestApp(app)
 22 | 
 23 |         response = test_app.get(endpoint, expect_errors=True)
 24 |         assert response.status_code == 403
 25 | 
 26 |         timestamp = str(time.time())
 27 |         nonce = str(random.randint(0, 10000))
 28 |         signature = get_signature(token, timestamp, nonce)
 29 |         echostr = generate_token()
 30 | 
 31 |         params = "?timestamp=%s&nonce=%s&signature=%s&echostr=%s" % (
 32 |             timestamp, nonce, signature, echostr
 33 |         )
 34 |         response = test_app.get(endpoint + params)
 35 | 
 36 |         assert response.status_code == 200
 37 |         assert response.body.decode('utf-8') == echostr
 38 | 
 39 |         response = test_app.get(endpoint, expect_errors=True)
 40 | 
 41 |         assert response.status_code == 403
 42 |         assert response.body.decode('utf-8') == u'喵'
 43 | 
 44 |         xml = """
 45 |                 <xml>
 46 |                     <ToUserName><![CDATA[toUser]]></ToUserName>
 47 |                     <FromUserName><![CDATA[fromUser]]></FromUserName>
 48 |                     <CreateTime>1348831860</CreateTime>
 49 |                     <MsgType><![CDATA[text]]></MsgType>
 50 |                     <Content><![CDATA[this is a test]]></Content>
 51 |                     <MsgId>1234567890123456</MsgId>
 52 |                 </xml>
 53 |                 """
 54 |         with pytest.raises(AppError):
 55 |             # WebTest will raise an AppError
 56 |             # if the status_code is not >= 200 and < 400.
 57 |             test_app.post(endpoint, xml, content_type="text/xml")
 58 | 
 59 |         response = test_app.post(
 60 |             endpoint + params, xml, content_type="text/xml"
 61 |         )
 62 | 
 63 |         assert response.status_code == 200
 64 |         response = process_message(parse_xml(response.body))
 65 |         assert response.content == 'hello'
 66 | 
 67 |     return tester
 68 | 
 69 | 
 70 | @pytest.fixture(scope="module")
 71 | def hello_robot():
 72 |     from werobot import WeRoBot
 73 |     robot = WeRoBot(token='', SESSION_STORAGE=False)
 74 | 
 75 |     @robot.text
 76 |     def hello():
 77 |         return 'hello'
 78 | 
 79 |     @robot.error_page
 80 |     def make_error_page(url):
 81 |         return '喵'
 82 | 
 83 |     return robot
 84 | 
 85 | 
 86 | def test_django():
 87 |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_test.settings")
 88 |     sys.path.append(
 89 |         os.path.join(
 90 |             os.path.abspath(os.path.dirname(__file__)), 'django_test_env'
 91 |         )
 92 |     )
 93 | 
 94 |     from django.test.utils import setup_test_environment
 95 |     setup_test_environment()
 96 |     from django.test.client import Client
 97 |     from werobot.parser import parse_xml, process_message
 98 |     import django
 99 | 
100 |     django.setup()
101 |     client = Client()
102 | 
103 |     token = 'TestDjango'
104 |     timestamp = str(time.time())
105 |     nonce = str(random.randint(0, 10000))
106 |     signature = get_signature(token, timestamp, nonce)
107 |     echostr = generate_token()
108 | 
109 |     response = client.get(
110 |         '/robot/', {
111 |             'signature': signature,
112 |             'timestamp': timestamp,
113 |             'nonce': nonce,
114 |             'echostr': echostr
115 |         }
116 |     )
117 |     assert response.status_code == 200
118 |     assert response.content.decode('utf-8') == echostr
119 | 
120 |     xml = """
121 |     <xml>
122 |         <ToUserName><![CDATA[toUser]]></ToUserName>
123 |         <FromUserName><![CDATA[fromUser]]></FromUserName>
124 |         <CreateTime>1348831860</CreateTime>
125 |         <MsgType><![CDATA[text]]></MsgType>
126 |         <Content><![CDATA[this is a test]]></Content>
127 |         <MsgId>1234567890123456</MsgId>
128 |     </xml>"""
129 |     params = "?timestamp=%s&nonce=%s&signature=%s" % \
130 |              (timestamp, nonce, signature)
131 |     url = '/robot/'
132 |     response = client.post(url, data=xml, content_type="text/xml")
133 | 
134 |     assert response.status_code == 403
135 |     assert response.content.decode('utf-8') == u'喵'
136 | 
137 |     url += params
138 |     response = client.post(url, data=xml, content_type="text/xml")
139 | 
140 |     assert response.status_code == 200
141 |     response = process_message(parse_xml(response.content))
142 |     assert response.content == 'hello'
143 | 
144 |     response = client.options(url)
145 |     assert response.status_code == 405
146 | 
147 | 
148 | def test_flask(wsgi_tester, hello_robot):
149 |     from flask import Flask
150 |     from werobot.contrib.flask import make_view
151 | 
152 |     token = generate_token()
153 |     endpoint = '/werobot_flask'
154 | 
155 |     hello_robot.token = token
156 |     flask_app = Flask(__name__)
157 |     flask_app.debug = True
158 | 
159 |     flask_app.add_url_rule(
160 |         rule=endpoint,
161 |         endpoint='werobot',
162 |         view_func=make_view(hello_robot),
163 |         methods=['GET', 'POST']
164 |     )
165 | 
166 |     wsgi_tester(flask_app, token=token, endpoint=endpoint)
167 | 
168 | 
169 | def test_bottle(wsgi_tester, hello_robot):
170 |     from werobot.contrib.bottle import make_view
171 |     from bottle import Bottle
172 | 
173 |     token = generate_token()
174 |     endpoint = '/werobot_bottle'
175 | 
176 |     hello_robot.token = token
177 | 
178 |     bottle_app = Bottle()
179 |     bottle_app.route(endpoint, ['GET', 'POST'], make_view(hello_robot))
180 | 
181 |     wsgi_tester(bottle_app, token=token, endpoint=endpoint)
182 | 
183 | 
184 | def test_werobot_wsgi(wsgi_tester, hello_robot):
185 |     token = generate_token()
186 |     endpoint = r'/rand'
187 |     hello_robot.token = token
188 | 
189 |     wsgi_tester(hello_robot.wsgi, token=token, endpoint=endpoint)
190 | 
191 | 
192 | # workaround to make Tornado work in Python 3.8
193 | # https://github.com/tornadoweb/tornado/issues/2608
194 | if sys.platform == 'win32' and sys.version_info >= (3, 8):
195 |     import asyncio
196 |     asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
197 | 
198 | if tornado.version_info[0] < 6:
199 | 
200 |     def test_tornado(wsgi_tester, hello_robot):
201 |         from tornado.wsgi import WSGIAdapter
202 |         import tornado.web
203 |         from werobot.contrib.tornado import make_handler
204 | 
205 |         token = generate_token()
206 |         endpoint = r'/werobot_tornado'
207 |         hello_robot.token = token
208 | 
209 |         tornado_app = tornado.web.Application(
210 |             [
211 |                 (endpoint, make_handler(hello_robot)),
212 |             ], debug=True
213 |         )
214 |         wsgi_tester(WSGIAdapter(tornado_app), token=token, endpoint=endpoint)
215 | else:
216 | 
217 |     class TestTornado(AsyncHTTPSTestCase):
218 |         token = 'TestTornado'
219 |         endpoint = '/werobot_tornado'
220 | 
221 |         @property
222 |         def robot(self):
223 |             from werobot import WeRoBot
224 |             robot = WeRoBot(token=self.token, SESSION_STORAGE=False)
225 | 
226 |             @robot.text
227 |             def hello():
228 |                 return 'hello'
229 | 
230 |             @robot.error_page
231 |             def make_error_page(url):
232 |                 return '喵'
233 | 
234 |             return robot
235 | 
236 |         def get_app(self):
237 |             import tornado.web
238 |             from werobot.contrib.tornado import make_handler
239 | 
240 |             tornado_app = tornado.web.Application(
241 |                 [
242 |                     (self.endpoint, make_handler(self.robot)),
243 |                 ], debug=True
244 |             )
245 |             return tornado_app
246 | 
247 |         def test_tornado(self):
248 |             token = self.token
249 |             timestamp = str(time.time())
250 |             nonce = str(random.randint(0, 10000))
251 |             signature = get_signature(token, timestamp, nonce)
252 |             echostr = generate_token()
253 | 
254 |             params = "?timestamp=%s&nonce=%s&signature=%s&echostr=%s" % (
255 |                 timestamp, nonce, signature, echostr
256 |             )
257 | 
258 |             response = self.fetch(path=self.endpoint + params)
259 |             assert response.code == 200
260 |             assert response.body.decode('utf-8') == echostr
261 | 
262 |             response = self.fetch(path=self.endpoint, )
263 |             assert response.code == 403
264 |             assert response.body.decode('utf-8') == u'喵'
265 | 
266 |             xml = """
267 |             <xml>
268 |                 <ToUserName><![CDATA[toUser]]></ToUserName>
269 |                 <FromUserName><![CDATA[fromUser]]></FromUserName>
270 |                 <CreateTime>1348831860</CreateTime>
271 |                 <MsgType><![CDATA[text]]></MsgType>
272 |                 <Content><![CDATA[this is a test]]></Content>
273 |                 <MsgId>1234567890123456</MsgId>
274 |             </xml>"""
275 | 
276 |             response = self.fetch(
277 |                 path=self.endpoint + params,
278 |                 method='POST',
279 |                 body=xml,
280 |                 headers={'Content-Type': 'text/xml'}
281 |             )
282 |             self.assertEqual(response.code, 200)
283 |             self.assertEqual(
284 |                 process_message(parse_xml(response.body)).content, 'hello'
285 |             )
286 | 
287 |             response = self.fetch(
288 |                 path=self.endpoint,
289 |                 method='POST',
290 |                 body=xml,
291 |                 headers={'Content-Type': 'text/xml'}
292 |             )
293 |             self.assertEqual(response.code, 403)
294 | 


--------------------------------------------------------------------------------
/tests/test_crypto.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.crypto import PrpCrypto, MessageCrypt
 4 | from werobot.utils import generate_token, to_binary, to_text
 5 | from werobot.parser import parse_xml
 6 | import time
 7 | 
 8 | 
 9 | def test_prpcrypto():
10 |     key = "ReUrr0NKeHkppBQq"
11 | 
12 |     assert len(key) == 16
13 | 
14 |     crypto = PrpCrypto(key)
15 |     text = generate_token(32)
16 |     app_id = generate_token(32)
17 |     assert crypto.decrypt(crypto.encrypt(text, app_id),
18 |                           app_id) == to_binary(text)
19 | 
20 | 
21 | def test_message_crypt():
22 |     encoding_aes_key = generate_token(32) + generate_token(11)
23 |     token = generate_token()
24 |     timestamp = to_text(int(time.time()))
25 |     nonce = generate_token(5)
26 |     app_id = generate_token(18)
27 |     crypt = MessageCrypt(
28 |         token=token, encoding_aes_key=encoding_aes_key, app_id=app_id
29 |     )
30 | 
31 |     message = crypt.encrypt_message('hello', timestamp, nonce)
32 |     assert message is not None
33 |     message = parse_xml(message)
34 |     assert message is not None
35 |     message = crypt.decrypt_message(
36 |         message['TimeStamp'], message['Nonce'], message['MsgSignature'],
37 |         message['Encrypt']
38 |     )
39 |     assert message == to_binary('hello')
40 | 


--------------------------------------------------------------------------------
/tests/test_logger.py:
--------------------------------------------------------------------------------
 1 | import time
 2 | import logging
 3 | 
 4 | from werobot.logger import enable_pretty_logging, _LogFormatter
 5 | 
 6 | 
 7 | def get_new_logger():
 8 |     return logging.getLogger(str(time.time()))
 9 | 
10 | 
11 | def test_logger_level():
12 |     for level in ('debug', 'info', 'warning', 'error'):
13 |         logger = get_new_logger()
14 |         enable_pretty_logging(logger, level=level)
15 |         assert logger.level == getattr(logging, level.upper())
16 | 
17 | 
18 | def test_handlers():
19 |     logger = get_new_logger()
20 |     enable_pretty_logging(logger)
21 |     assert isinstance(logger.handlers[0].formatter, _LogFormatter)
22 | 


--------------------------------------------------------------------------------
/tests/test_replies.py:
--------------------------------------------------------------------------------
  1 | # -*- coding:utf-8 -*-
  2 | 
  3 | import time
  4 | import pytest
  5 | 
  6 | from werobot.parser import parse_user_msg
  7 | from werobot.replies import WeChatReply, TextReply, ImageReply, MusicReply
  8 | from werobot.replies import VoiceReply, VideoReply
  9 | from werobot.replies import Article, ArticlesReply
 10 | from werobot.replies import TransferCustomerServiceReply, SuccessReply
 11 | from werobot.replies import process_function_reply
 12 | from werobot.utils import to_binary, to_text
 13 | 
 14 | 
 15 | def test_wechat_reply():
 16 |     message = parse_user_msg(
 17 |         """
 18 |         <xml>
 19 |         <ToUserName><![CDATA[toUser]]></ToUserName>
 20 |         <FromUserName><![CDATA[fromUser]]></FromUserName>
 21 |         <CreateTime>1348831860</CreateTime>
 22 |         <MsgType><![CDATA[image]]></MsgType>
 23 |         <PicUrl><![CDATA[this is a url]]></PicUrl>
 24 |         <MediaId><![CDATA[media_id]]></MediaId>
 25 |         <MsgId>1234567890123456</MsgId>
 26 |         </xml>
 27 |     """
 28 |     )
 29 |     s = to_binary("喵fdsjaklfsk")
 30 |     reply = WeChatReply(message=message, s=s)
 31 |     assert reply._args['source'] == 'toUser'
 32 |     assert reply._args['target'] == 'fromUser'
 33 |     assert reply._args['s'] == to_text(s)
 34 |     assert isinstance(reply._args['time'], int)
 35 | 
 36 | 
 37 | def test_text_reply():
 38 |     t = int(time.time())
 39 |     reply = TextReply(target='fromUser', source='toUser', content="aa", time=t)
 40 |     assert reply.render().strip() == """
 41 |     <xml>
 42 |     <ToUserName><![CDATA[fromUser]]></ToUserName>
 43 |     <FromUserName><![CDATA[toUser]]></FromUserName>
 44 |     <CreateTime>{time}</CreateTime>
 45 |     <MsgType><![CDATA[text]]></MsgType>
 46 |     <Content><![CDATA[aa]]></Content>
 47 |     </xml>""".format(time=t).strip()
 48 | 
 49 | 
 50 | def test_image_reply():
 51 |     t = int(time.time())
 52 |     reply = ImageReply(
 53 |         target='fromUser', source='toUser', media_id="fdasfdasfasd", time=t
 54 |     )
 55 |     assert reply.render().strip() == """
 56 |     <xml>
 57 |     <ToUserName><![CDATA[fromUser]]></ToUserName>
 58 |     <FromUserName><![CDATA[toUser]]></FromUserName>
 59 |     <CreateTime>{time}</CreateTime>
 60 |     <MsgType><![CDATA[image]]></MsgType>
 61 |     <Image>
 62 |     <MediaId><![CDATA[fdasfdasfasd]]></MediaId>
 63 |     </Image>
 64 |     </xml>""".format(time=t).strip()
 65 | 
 66 | 
 67 | def test_voice_reply():
 68 |     t = int(time.time())
 69 |     reply = VoiceReply(
 70 |         target='tgu', source='su', media_id="fdasfdasfasd", time=t
 71 |     )
 72 |     assert reply.render().strip() == """
 73 |     <xml>
 74 |     <ToUserName><![CDATA[tgu]]></ToUserName>
 75 |     <FromUserName><![CDATA[su]]></FromUserName>
 76 |     <CreateTime>{time}</CreateTime>
 77 |     <MsgType><![CDATA[voice]]></MsgType>
 78 |     <Voice>
 79 |     <MediaId><![CDATA[fdasfdasfasd]]></MediaId>
 80 |     </Voice>
 81 |     </xml>""".format(time=t).strip()
 82 | 
 83 | 
 84 | def test_video_reply():
 85 |     t = int(time.time())
 86 |     reply = VideoReply(
 87 |         target='tgu',
 88 |         source='su',
 89 |         media_id="fdasfdasfasd",
 90 |         time=t,
 91 |     )
 92 |     assert reply.render().strip() == """
 93 |     <xml>
 94 |     <ToUserName><![CDATA[tgu]]></ToUserName>
 95 |     <FromUserName><![CDATA[su]]></FromUserName>
 96 |     <CreateTime>{time}</CreateTime>
 97 |     <MsgType><![CDATA[video]]></MsgType>
 98 |     <Video>
 99 |     <MediaId><![CDATA[fdasfdasfasd]]></MediaId>
100 |     <Title><![CDATA[]]></Title>
101 |     <Description><![CDATA[]]></Description>
102 |     </Video>
103 |     </xml>""".format(time=t).strip()
104 | 
105 |     reply_2 = VideoReply(
106 |         target='tgu',
107 |         source='su',
108 |         media_id="fdasfdasfasd",
109 |         time=t,
110 |         title='meow'
111 |     )
112 | 
113 |     assert reply_2.render().strip() == """
114 |     <xml>
115 |     <ToUserName><![CDATA[tgu]]></ToUserName>
116 |     <FromUserName><![CDATA[su]]></FromUserName>
117 |     <CreateTime>{time}</CreateTime>
118 |     <MsgType><![CDATA[video]]></MsgType>
119 |     <Video>
120 |     <MediaId><![CDATA[fdasfdasfasd]]></MediaId>
121 |     <Title><![CDATA[meow]]></Title>
122 |     <Description><![CDATA[]]></Description>
123 |     </Video>
124 |     </xml>""".format(time=t).strip()
125 | 
126 |     reply_3 = VideoReply(
127 |         target='tgu',
128 |         source='su',
129 |         media_id="fdasfdasfasd",
130 |         time=t,
131 |         title='meow',
132 |         description='www'
133 |     )
134 |     assert reply_3.render().strip() == """
135 |     <xml>
136 |     <ToUserName><![CDATA[tgu]]></ToUserName>
137 |     <FromUserName><![CDATA[su]]></FromUserName>
138 |     <CreateTime>{time}</CreateTime>
139 |     <MsgType><![CDATA[video]]></MsgType>
140 |     <Video>
141 |     <MediaId><![CDATA[fdasfdasfasd]]></MediaId>
142 |     <Title><![CDATA[meow]]></Title>
143 |     <Description><![CDATA[www]]></Description>
144 |     </Video>
145 |     </xml>""".format(time=t).strip()
146 | 
147 | 
148 | def test_video_reply_process_args():
149 |     reply = VideoReply(
150 |         target='tgu',
151 |         source='su',
152 |         media_id="fdasfdasfasd",
153 |     )
154 |     assert reply._args['title'] == ''
155 |     assert reply._args['description'] == ''
156 | 
157 | 
158 | def test_music_reply():
159 |     t = int(time.time())
160 |     reply = MusicReply(
161 |         target='tg',
162 |         source='ss',
163 |         time=t,
164 |         title='tt',
165 |         description='ds',
166 |         url='u1',
167 |         hq_url='u2',
168 |     )
169 |     assert reply.render().strip() == """
170 |     <xml>
171 |     <ToUserName><![CDATA[tg]]></ToUserName>
172 |     <FromUserName><![CDATA[ss]]></FromUserName>
173 |     <CreateTime>{time}</CreateTime>
174 |     <MsgType><![CDATA[music]]></MsgType>
175 |     <Music>
176 |     <Title><![CDATA[tt]]></Title>
177 |     <Description><![CDATA[ds]]></Description>
178 |     <MusicUrl><![CDATA[u1]]></MusicUrl>
179 |     <HQMusicUrl><![CDATA[u2]]></HQMusicUrl>
180 |     </Music>
181 |     </xml>""".format(time=t).strip()
182 | 
183 | 
184 | def test_music_reply_process_args():
185 |     reply = MusicReply(
186 |         target='tg',
187 |         source='ss',
188 |         title='tt',
189 |         description='ds',
190 |         url='u1',
191 |     )
192 |     assert reply._args['hq_url'] == 'u1'
193 | 
194 |     reply_2 = MusicReply(
195 |         target='tg',
196 |         source='ss',
197 |         title='tt',
198 |         description='ds',
199 |         url='u1',
200 |         hq_url='u2'
201 |     )
202 |     assert reply_2._args['hq_url'] == 'u2'
203 | 
204 | 
205 | def test_article():
206 |     article = Article(
207 |         title="tt", description=to_binary("附近的萨卡里发生"), img="http", url="uuu"
208 |     )
209 |     assert article.render().strip() == to_text(
210 |         """
211 |     <item>
212 |     <Title><![CDATA[tt]]></Title>
213 |     <Description><![CDATA[附近的萨卡里发生]]></Description>
214 |     <PicUrl><![CDATA[http]]></PicUrl>
215 |     <Url><![CDATA[uuu]]></Url>
216 |     </item>
217 |     """
218 |     ).strip()
219 | 
220 | 
221 | def test_articles_reply():
222 |     article = Article(
223 |         title="tt", description="附近的萨卡里发生", img="http", url="uuu"
224 |     )
225 |     t = int(time.time())
226 |     reply = ArticlesReply(target='tg', source='ss', time=t)
227 |     assert reply.render().strip() == """
228 |     <xml>
229 |     <ToUserName><![CDATA[tg]]></ToUserName>
230 |     <FromUserName><![CDATA[ss]]></FromUserName>
231 |     <CreateTime>{time}</CreateTime>
232 |     <MsgType><![CDATA[news]]></MsgType>
233 |     <Content><![CDATA[]]></Content>
234 |     <ArticleCount>0</ArticleCount>
235 |     <Articles></Articles>
236 |     </xml>""".format(time=t).strip()
237 | 
238 |     reply._args['content'] = 'wwww'
239 |     assert '<Content><![CDATA[wwww]]></Content>' in reply.render()
240 |     reply.add_article(article)
241 |     assert '<ArticleCount>1</ArticleCount>' in reply.render()
242 |     assert article.render() in reply.render()
243 |     for _ in range(9):
244 |         reply.add_article(article)
245 |     assert '<ArticleCount>10</ArticleCount>' in reply.render()
246 |     with pytest.raises(AttributeError):
247 |         reply.add_article(article)
248 | 
249 | 
250 | def test_transfer_customer_service_reply():
251 |     t = int(time.time())
252 |     reply = TransferCustomerServiceReply(
253 |         source='aaa', target='bbb', time=t, account="test1@test"
254 |     )
255 |     assert reply.render().strip() == """
256 |             <xml>
257 |             <ToUserName><![CDATA[bbb]]></ToUserName>
258 |             <FromUserName><![CDATA[aaa]]></FromUserName>
259 |             <CreateTime>{time}</CreateTime>
260 |             <MsgType><![CDATA[transfer_customer_service]]></MsgType>
261 |             <TransInfo>
262 |                  <KfAccount><![CDATA[test1@test]]></KfAccount>
263 |              </TransInfo>
264 |             </xml>
265 |     """.format(time=t).strip()
266 | 
267 | 
268 | def test_transfer_customer_service_reply_without_account():
269 |     t = int(time.time())
270 |     reply = TransferCustomerServiceReply(source='aaa', target='bbb', time=t)
271 |     assert reply.render().strip() == """
272 |             <xml>
273 |             <ToUserName><![CDATA[bbb]]></ToUserName>
274 |             <FromUserName><![CDATA[aaa]]></FromUserName>
275 |             <CreateTime>{time}</CreateTime>
276 |             <MsgType><![CDATA[transfer_customer_service]]></MsgType>
277 |             </xml>
278 |         """.format(time=t).strip()
279 | 
280 | 
281 | def test_success_reply():
282 |     assert SuccessReply().render() == "success"
283 | 
284 | 
285 | def test_process_unknown_function_reply():
286 |     reply = SuccessReply()
287 |     assert process_function_reply(reply) == reply
288 | 
289 | 
290 | def test_process_text_function_reply():
291 |     reply = process_function_reply("test")
292 |     assert isinstance(reply, TextReply)
293 |     assert reply.content == "test"
294 | 
295 | 
296 | def test_process_music_function_reply():
297 |     reply = process_function_reply(["title", "desc", "url"])
298 |     assert isinstance(reply, MusicReply)
299 |     assert reply.title == "title"
300 |     assert reply.description == "desc"
301 |     assert reply.url == reply.hq_url == "url"
302 | 
303 |     reply = process_function_reply(["title", "desc", "url", "hq"])
304 |     assert isinstance(reply, MusicReply)
305 |     assert reply.title == "title"
306 |     assert reply.description == "desc"
307 |     assert reply.url == "url"
308 |     assert reply.hq_url == "hq"
309 | 
310 | 
311 | def test_process_articles_function_reply():
312 |     reply = process_function_reply(
313 |         [["tt1", 'ds1', 'img', 'url'], ["tt2", 'ds2', 'im2g', 'u2rl']]
314 |     )
315 |     assert isinstance(reply, ArticlesReply)
316 |     assert len(reply._articles) == 2
317 |     article_1, article_2 = reply._articles
318 |     assert isinstance(article_1, Article), isinstance(article_2, Article)
319 |     assert article_1.title == "tt1", article_2.title == "tt2"
320 |     assert article_1.description == "ds1", article_2.description == "ds2"
321 |     assert article_1.img == "img", article_2.img == "im2g"
322 |     assert article_1.url == "url", article_2.url == "u2rl"
323 | 
324 |     process_function_reply([[1, 2, 3, 4]] * 10)
325 | 
326 |     with pytest.raises(AttributeError):
327 |         process_function_reply([[1, 2, 3, 4]] * 11)
328 | 


--------------------------------------------------------------------------------
/tests/test_robot.py:
--------------------------------------------------------------------------------
  1 | # -*- coding:utf-8 -*-
  2 | 
  3 | import hashlib
  4 | import time
  5 | import os
  6 | import pytest
  7 | 
  8 | from werobot import WeRoBot
  9 | from werobot.utils import generate_token, to_text
 10 | 
 11 | 
 12 | def _make_xml(content):
 13 |     return """
 14 |         <xml>
 15 |         <ToUserName><![CDATA[toUser]]></ToUserName>
 16 |         <FromUserName><![CDATA[fromUser]]></FromUserName>
 17 |         <CreateTime>1348831860</CreateTime>
 18 |         <MsgType><![CDATA[text]]></MsgType>
 19 |         <Content><![CDATA[%s]]></Content>
 20 |         <MsgId>1234567890123456</MsgId>
 21 |         </xml>
 22 |     """ % content
 23 | 
 24 | 
 25 | def test_signature_checker():
 26 |     token = generate_token()
 27 | 
 28 |     robot = WeRoBot(token, SESSION_STORAGE=False)
 29 | 
 30 |     timestamp = str(int(time.time()))
 31 |     nonce = '12345678'
 32 | 
 33 |     sign = [token, timestamp, nonce]
 34 |     sign.sort()
 35 |     sign = ''.join(sign)
 36 |     sign = sign.encode()
 37 |     sign = hashlib.sha1(sign).hexdigest()
 38 | 
 39 |     assert robot.check_signature(timestamp, nonce, sign)
 40 | 
 41 | 
 42 | def test_register_handlers():  # noqa: C901
 43 |     robot = WeRoBot(SESSION_STORAGE=False)
 44 | 
 45 |     for type in robot.message_types:
 46 |         assert hasattr(robot,
 47 |                        type) or hasattr(robot, type.replace('_event', ''))
 48 | 
 49 |     @robot.text
 50 |     def text_handler():
 51 |         return "Hi"
 52 | 
 53 |     assert robot._handlers["text"] == [(text_handler, 0)]
 54 | 
 55 |     @robot.image
 56 |     def image_handler(message):
 57 |         return 'nice pic'
 58 | 
 59 |     assert robot._handlers["image"] == [(image_handler, 1)]
 60 | 
 61 |     assert robot.get_handlers("text") == [(text_handler, 0)]
 62 | 
 63 |     @robot.handler
 64 |     def handler(message, session):
 65 |         pass
 66 | 
 67 |     assert robot.get_handlers("text") == [(text_handler, 0), (handler, 2)]
 68 | 
 69 |     @robot.video
 70 |     def video_handler():
 71 |         pass
 72 | 
 73 |     assert robot._handlers["video"] == [(video_handler, 0)]
 74 |     assert robot.get_handlers("video") == [(video_handler, 0), (handler, 2)]
 75 | 
 76 |     @robot.shortvideo
 77 |     def shortvideo_handler():
 78 |         pass
 79 | 
 80 |     assert robot._handlers["shortvideo"] == [(shortvideo_handler, 0)]
 81 |     assert robot.get_handlers("shortvideo") == [
 82 |         (shortvideo_handler, 0), (handler, 2)
 83 |     ]
 84 | 
 85 |     @robot.location
 86 |     def location_handler():
 87 |         pass
 88 | 
 89 |     assert robot._handlers["location"] == [(location_handler, 0)]
 90 | 
 91 |     @robot.link
 92 |     def link_handler():
 93 |         pass
 94 | 
 95 |     assert robot._handlers["link"] == [(link_handler, 0)]
 96 | 
 97 |     @robot.subscribe
 98 |     def subscribe_handler():
 99 |         pass
100 | 
101 |     assert robot._handlers["subscribe_event"] == [(subscribe_handler, 0)]
102 | 
103 |     @robot.unsubscribe
104 |     def unsubscribe_handler():
105 |         pass
106 | 
107 |     assert robot._handlers["unsubscribe_event"] == [(unsubscribe_handler, 0)]
108 | 
109 |     @robot.voice
110 |     def voice_handler():
111 |         pass
112 | 
113 |     assert robot._handlers["voice"] == [(voice_handler, 0)]
114 | 
115 |     @robot.click
116 |     def click_handler():
117 |         pass
118 | 
119 |     assert robot._handlers["click_event"] == [(click_handler, 0)]
120 | 
121 |     @robot.key_click("MENU")
122 |     def menu_handler():
123 |         pass
124 | 
125 |     assert len(robot._handlers["click_event"]) == 2
126 | 
127 |     @robot.scan
128 |     def scan_handler():
129 |         pass
130 | 
131 |     assert robot._handlers["scan_event"] == [(scan_handler, 0)]
132 | 
133 |     @robot.scancode_push
134 |     def scancode_push_handler():
135 |         pass
136 | 
137 |     assert robot._handlers["scancode_push_event"] == [
138 |         (scancode_push_handler, 0)
139 |     ]
140 | 
141 |     @robot.scancode_waitmsg
142 |     def scancode_waitmsg_handler():
143 |         pass
144 | 
145 |     assert robot._handlers["scancode_waitmsg_event"] == [
146 |         (scancode_waitmsg_handler, 0)
147 |     ]
148 | 
149 | 
150 | def test_filter():
151 |     import re
152 |     import werobot.testing
153 |     robot = WeRoBot(SESSION_STORAGE=False)
154 | 
155 |     @robot.filter("喵")
156 |     def _1():
157 |         return "喵"
158 | 
159 |     assert len(robot._handlers["text"]) == 1
160 | 
161 |     @robot.filter(re.compile(to_text(".*?呵呵.*?")))
162 |     def _2():
163 |         return "哼"
164 | 
165 |     assert len(robot._handlers["text"]) == 2
166 | 
167 |     @robot.text
168 |     def _3():
169 |         return "汪"
170 | 
171 |     assert len(robot._handlers["text"]) == 3
172 | 
173 |     tester = werobot.testing.WeTest(robot)
174 | 
175 |     assert tester.send_xml(_make_xml("啊"))._args['content'] == u"汪"
176 |     assert tester.send_xml(_make_xml("啊呵呵"))._args['content'] == u"哼"
177 |     assert tester.send_xml(_make_xml("喵"))._args['content'] == u"喵"
178 | 
179 |     try:
180 |         os.remove(os.path.abspath("werobot_session"))
181 |     except OSError:
182 |         pass
183 |     robot = WeRoBot(SESSION_STORAGE=False)
184 | 
185 |     @robot.filter("帮助", "跪求帮助", re.compile("(.*?)help.*?"))
186 |     def _(message, session, match):
187 |         if match and match.group(1) == u"小姐姐":
188 |             return "本小姐就帮你一下"
189 |         return "就不帮"
190 | 
191 |     assert len(robot._handlers["text"]) == 3
192 | 
193 |     @robot.text
194 |     def _4():
195 |         return "哦"
196 | 
197 |     assert len(robot._handlers["text"]) == 4
198 | 
199 |     tester = werobot.testing.WeTest(robot)
200 | 
201 |     assert tester.send_xml(_make_xml("啊"))._args['content'] == u"哦"
202 |     assert tester.send_xml(_make_xml("帮助"))._args['content'] == u"就不帮"
203 |     assert tester.send_xml(_make_xml("跪求帮助"))._args['content'] == u"就不帮"
204 |     assert tester.send_xml(_make_xml("ooohelp"))._args['content'] == u"就不帮"
205 |     assert tester.send_xml(_make_xml("小姐姐help")
206 |                            )._args['content'] == u"本小姐就帮你一下"
207 | 
208 | 
209 | def test_register_not_callable_object():
210 |     robot = WeRoBot(SESSION_STORAGE=False)
211 |     with pytest.raises(ValueError):
212 |         robot.add_handler("s")
213 | 
214 | 
215 | def test_error_page():
216 |     robot = WeRoBot()
217 | 
218 |     @robot.error_page
219 |     def make_error_page(url):
220 |         return url
221 | 
222 |     assert robot.make_error_page('喵') == '喵'
223 | 
224 | 
225 | def test_config_ignore():
226 |     from werobot.config import Config
227 |     config = Config(TOKEN="token from config")
228 |     robot = WeRoBot(config=config, token="token2333")
229 |     assert robot.token == "token from config"
230 | 
231 | 
232 | def test_add_filter():
233 |     import werobot.testing
234 |     import re
235 | 
236 |     robot = WeRoBot()
237 | 
238 |     def test_register():
239 |         return "test"
240 | 
241 |     robot.add_filter(test_register, ["test", re.compile(u".*?啦.*?")])
242 | 
243 |     tester = werobot.testing.WeTest(robot)
244 | 
245 |     assert tester.send_xml(_make_xml("test"))._args["content"] == "test"
246 |     assert tester.send_xml(_make_xml(u"我要测试啦"))._args["content"] == "test"
247 |     assert tester.send_xml(_make_xml(u"我要测试")) is None
248 | 
249 |     with pytest.raises(ValueError) as e:
250 |         robot.add_filter("test", ["test"])
251 |     assert e.value.args[0] == "test is not callable"
252 | 
253 |     with pytest.raises(ValueError) as e:
254 |         robot.add_filter(test_register, "test")
255 |     assert e.value.args[0] == "test is not list"
256 | 
257 |     with pytest.raises(TypeError) as e:
258 |         robot.add_filter(test_register, [["bazinga"]])
259 |     assert e.value.args[0] == "[\'bazinga\'] is not a valid rule"
260 | 


--------------------------------------------------------------------------------
/tests/test_session.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | 
  3 | import os
  4 | import mongomock
  5 | import mockredis
  6 | import pytest
  7 | import sqlite3
  8 | 
  9 | import werobot
 10 | import werobot.testing
 11 | import werobot.utils
 12 | from werobot.session import SessionStorage
 13 | from werobot.session import filestorage, mongodbstorage, redisstorage, saekvstorage
 14 | from werobot.session import sqlitestorage
 15 | from werobot.session import mysqlstorage
 16 | from werobot.session import postgresqlstorage
 17 | from werobot.utils import to_binary
 18 | 
 19 | 
 20 | def teardown_module():
 21 |     try:
 22 |         os.remove("werobot_session")
 23 |         os.remove("werobot_session.sqlite3")
 24 |     except:
 25 |         pass
 26 | 
 27 | 
 28 | def remove_session(session):
 29 |     try:
 30 |         del session[to_binary("fromUser")]
 31 |     except:
 32 |         pass
 33 | 
 34 | 
 35 | def test_session():
 36 |     robot = werobot.WeRoBot(
 37 |         token=werobot.utils.generate_token(), enable_session=True
 38 |     )
 39 | 
 40 |     @robot.text
 41 |     def first(message, session):
 42 |         if 'last' in session:
 43 |             return
 44 |         session['last'] = message.content
 45 |         return message.content
 46 | 
 47 |     @robot.text
 48 |     def second(_, session):
 49 |         return session['last']
 50 | 
 51 |     tester = werobot.testing.WeTest(robot)
 52 |     xml_1 = """
 53 |         <xml>
 54 |         <ToUserName><![CDATA[toUser]]></ToUserName>
 55 |         <FromUserName><![CDATA[fromUser]]></FromUserName>
 56 |         <CreateTime>1348831860</CreateTime>
 57 |         <MsgType><![CDATA[text]]></MsgType>
 58 |         <Content><![CDATA[ss]]></Content>
 59 |         <MsgId>1234567890123456</MsgId>
 60 |         </xml>
 61 |     """
 62 |     xml_2 = """
 63 |         <xml>
 64 |         <ToUserName><![CDATA[toUser]]></ToUserName>
 65 |         <FromUserName><![CDATA[fromUser]]></FromUserName>
 66 |         <CreateTime>1348831860</CreateTime>
 67 |         <MsgType><![CDATA[text]]></MsgType>
 68 |         <Content><![CDATA[dd]]></Content>
 69 |         <MsgId>1234567890123456</MsgId>
 70 |         </xml>
 71 |     """
 72 | 
 73 |     reply_1 = tester.send_xml(xml_1)._args['content']
 74 |     assert reply_1 == 'ss'
 75 |     reply_2 = tester.send_xml(xml_2)._args['content']
 76 |     assert reply_2 == 'ss'
 77 | 
 78 | 
 79 | def test_session_storage_get():
 80 |     session = SessionStorage()
 81 |     with pytest.raises(NotImplementedError):
 82 |         session.get('s')
 83 |     with pytest.raises(NotImplementedError):
 84 |         session['s']
 85 | 
 86 | 
 87 | def test_session_storage_set():
 88 |     session = SessionStorage()
 89 |     with pytest.raises(NotImplementedError):
 90 |         session.set('s', {})
 91 |     with pytest.raises(NotImplementedError):
 92 |         session['s'] = {}
 93 | 
 94 | 
 95 | def test_session_storage_delete():
 96 |     session = SessionStorage()
 97 |     with pytest.raises(NotImplementedError):
 98 |         session.delete('s')
 99 | 
100 |     with pytest.raises(NotImplementedError):
101 |         del session['s']
102 | 
103 | 
104 | class MockPyMySQL:
105 |     def __init__(self):
106 |         self.db = sqlite3.connect("werobot_session.sqlite3")
107 |         self.db.text_factory = str
108 |         from werobot.session.sqlitestorage import __CREATE_TABLE_SQL__
109 |         self.db.execute(__CREATE_TABLE_SQL__)
110 |         self.cache_result = None
111 | 
112 |     def cursor(self):
113 |         return self
114 | 
115 |     def execute(self, *args, **kwargs):
116 |         if "CREATE TABLE" not in args[0]:
117 |             args = list(args)
118 |             args[0] = args[0].replace('%s', '?')
119 |             if "SELECT" in args[0]:
120 |                 self.cache_result = self.db.execute(*args, **kwargs).fetchone()
121 |             elif "INSERT" in args[0]:
122 |                 args = [
123 |                     "INSERT OR REPLACE INTO WeRoBot (id, value) VALUES (?,?);",
124 |                     (args[1][0], args[1][1])
125 |                 ]
126 |                 self.db.execute(*args, **kwargs)
127 |             else:
128 |                 self.db.execute(*args, **kwargs)
129 | 
130 |     def fetchone(self):
131 |         return self.cache_result
132 | 
133 |     def commit(self):
134 |         return self.db.commit()
135 | 
136 | 
137 | class MockPostgreSQL(MockPyMySQL):
138 |     pass
139 | 
140 | 
141 | @pytest.mark.parametrize(
142 |     "storage", [
143 |         filestorage.FileStorage(),
144 |         mongodbstorage.MongoDBStorage(mongomock.MongoClient().t.t),
145 |         redisstorage.RedisStorage(mockredis.mock_redis_client()),
146 |         sqlitestorage.SQLiteStorage(),
147 |         mysqlstorage.MySQLStorage(MockPyMySQL()),
148 |         postgresqlstorage.PostgreSQLStorage(MockPostgreSQL())
149 |     ]
150 | )
151 | def test_storage(storage):
152 |     assert storage.get("喵") == {}
153 |     storage.set("喵", "喵喵")
154 |     assert storage.get("喵") == u"喵喵"
155 |     storage.delete("喵")
156 |     assert storage.get("喵") == {}
157 | 
158 |     assert storage["榴莲"] == {}
159 |     storage["榴莲"] = "榴莲"
160 |     assert storage["榴莲"] == u"榴莲"
161 |     del storage["榴莲"]
162 |     assert storage["榴莲"] == {}
163 | 


--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | import re
 4 | 
 5 | from werobot.utils import generate_token, check_token, to_text, to_binary
 6 | from werobot.utils import pay_sign_dict, make_error_page, is_regex
 7 | 
 8 | 
 9 | def test_token_generator():
10 |     assert not check_token('AA C')
11 |     assert check_token(generate_token())
12 |     assert 3 <= len(generate_token()) <= 32
13 | 
14 | 
15 | def test_to_text():
16 |     assert to_text(6) == str(6)
17 |     assert to_text(b"aa") == "aa"
18 |     assert to_text("cc") == "cc"
19 |     assert to_text(u"喵") == u"喵"
20 |     assert to_text("喵") == u"喵"
21 | 
22 | 
23 | def test_to_binary():
24 |     assert to_binary(6) == bytes(6)
25 |     assert to_binary(b"aa") == b"aa"
26 |     assert to_binary("cc") == b"cc"
27 |     assert to_binary(u"喵") == b"\xe5\x96\xb5"
28 |     assert to_binary("喵") == b"\xe5\x96\xb5"
29 | 
30 | 
31 | def test_pay_sign_dict():
32 |     appid = {"id": "nothing"}
33 |     key = "test_key"
34 | 
35 |     pay_sign = pay_sign_dict(appid, key)
36 | 
37 |     assert "timestamp" in pay_sign[0]
38 |     assert "noncestr" in pay_sign[0]
39 |     assert "appid" in pay_sign[0]
40 |     assert pay_sign[0]["appid"] == appid
41 |     assert pay_sign[2] == u"SHA1"
42 | 
43 |     pay_sign = pay_sign_dict(
44 |         appid, key, add_noncestr=False, add_timestamp=False, gadd_appid=False
45 |     )
46 | 
47 |     assert "timestamp" not in pay_sign[0]
48 |     assert "noncestr" not in pay_sign[0]
49 |     assert "appid" in pay_sign[0]
50 | 
51 | 
52 | def test_make_error_page():
53 |     rand_string = generate_token()
54 |     content = make_error_page(rand_string)
55 |     assert rand_string in content
56 | 
57 | 
58 | def test_is_regex():
59 |     regex = re.compile(r"test")
60 |     assert not is_regex("test")
61 |     assert is_regex(regex)
62 | 


--------------------------------------------------------------------------------
/tox-requirements.txt:
--------------------------------------------------------------------------------
 1 | -r requirements.txt
 2 | coverage==5.3.1
 3 | cryptography==36.0.2
 4 | pytest==7.0.1
 5 | WebTest==2.0.35
 6 | Flask
 7 | tornado
 8 | responses==0.12.1
 9 | multipart==0.1
10 | pytest-mock==3.5.1
11 | mongomock==4.0.0
12 | mockredispy==2.9.3
13 | 


--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
 1 | [tox]
 2 | envlist = {py36,py37,py38,py39}--{dj22,dj30,dj31}
 3 | 
 4 | [testenv]
 5 | commands = coverage run --source werobot -m py.test
 6 | commands_post = codecov
 7 | passenv = DATABASE_MYSQL_USERNAME DATABASE_MYSQL_PASSWORD CODECOV_*
 8 | deps =
 9 |   dj22: Django < 2.3
10 |   dj30: Django < 3.1
11 |   dj31: Django < 3.2
12 |   -rtox-requirements.txt
13 |   codecov>=1.4.0
14 | 


--------------------------------------------------------------------------------
/werobot/__init__.py:
--------------------------------------------------------------------------------
 1 | __version__ = '1.13.1'
 2 | __author__ = 'whtsky'
 3 | __license__ = 'MIT'
 4 | 
 5 | __all__ = ["WeRoBot"]
 6 | 
 7 | try:
 8 |     from werobot.robot import WeRoBot
 9 | except ImportError:  # pragma: no cover
10 |     pass  # pragma: no cover
11 | 


--------------------------------------------------------------------------------
/werobot/config.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | import types
 4 | 
 5 | 
 6 | class ConfigAttribute(object):
 7 |     """
 8 |     让一个属性指向一个配置
 9 |     """
10 |     def __init__(self, name):
11 |         self.__name__ = name
12 | 
13 |     def __get__(self, obj, type=None):
14 |         if obj is None:
15 |             return self
16 |         rv = obj.config[self.__name__]
17 |         return rv
18 | 
19 |     def __set__(self, obj, value):
20 |         obj.config[self.__name__] = value
21 | 
22 | 
23 | class Config(dict):
24 |     def from_pyfile(self, filename):
25 |         """
26 |         在一个 Python 文件中读取配置。
27 | 
28 |         :param filename: 配置文件的文件名
29 |         :return: 如果读取成功,返回 ``True``,如果失败,会抛出错误异常
30 |         """
31 |         d = types.ModuleType('config')
32 |         d.__file__ = filename
33 |         with open(filename) as config_file:
34 |             exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
35 |         self.from_object(d)
36 |         return True
37 | 
38 |     def from_object(self, obj):
39 |         """
40 |         在给定的 Python 对象中读取配置。
41 | 
42 |         :param obj: 一个 Python 对象
43 |         """
44 |         for key in dir(obj):
45 |             if key.isupper():
46 |                 self[key] = getattr(obj, key)
47 | 


--------------------------------------------------------------------------------
/werobot/contrib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/offu/WeRoBot/7b3d62d7ae126fc8bc9e033e6358dec2852d1d5a/werobot/contrib/__init__.py


--------------------------------------------------------------------------------
/werobot/contrib/bottle.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | from bottle import request, HTTPResponse
 3 | 
 4 | import html
 5 | 
 6 | 
 7 | def make_view(robot):
 8 |     """
 9 |     为一个 BaseRoBot 生成 Bottle view。
10 | 
11 |     Usage ::
12 | 
13 |         from werobot import WeRoBot
14 | 
15 |         robot = WeRoBot(token='token')
16 | 
17 | 
18 |         @robot.handler
19 |         def hello(message):
20 |             return 'Hello World!'
21 | 
22 |         from bottle import Bottle
23 |         from werobot.contrib.bottle import make_view
24 | 
25 |         app = Bottle()
26 |         app.route(
27 |             '/robot',  # WeRoBot 挂载地址
28 |             ['GET', 'POST'],
29 |             make_view(robot)
30 |         )
31 | 
32 | 
33 |     :param robot: 一个 BaseRoBot 实例
34 |     :return: 一个标准的 Bottle view
35 |     """
36 |     def werobot_view(*args, **kwargs):
37 |         if not robot.check_signature(
38 |             request.query.timestamp, request.query.nonce,
39 |             request.query.signature
40 |         ):
41 |             return HTTPResponse(
42 |                 status=403,
43 |                 body=robot.make_error_page(html.escape(request.url))
44 |             )
45 |         if request.method == 'GET':
46 |             return request.query.echostr
47 |         else:
48 |             body = request.body.read()
49 |             message = robot.parse_message(
50 |                 body,
51 |                 timestamp=request.query.timestamp,
52 |                 nonce=request.query.nonce,
53 |                 msg_signature=request.query.msg_signature
54 |             )
55 |             return robot.get_encrypted_reply(message)
56 | 
57 |     return werobot_view
58 | 


--------------------------------------------------------------------------------
/werobot/contrib/django.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseForbidden
 4 | from django.views.decorators.csrf import csrf_exempt
 5 | 
 6 | import html
 7 | 
 8 | 
 9 | def make_view(robot):
10 |     """
11 |     为一个 BaseRoBot 生成 Django view。
12 | 
13 |     :param robot: 一个 BaseRoBot 实例。
14 |     :return: 一个标准的 Django view
15 |     """
16 |     @csrf_exempt
17 |     def werobot_view(request):
18 |         timestamp = request.GET.get("timestamp", "")
19 |         nonce = request.GET.get("nonce", "")
20 |         signature = request.GET.get("signature", "")
21 | 
22 |         if not robot.check_signature(
23 |             timestamp=timestamp, nonce=nonce, signature=signature
24 |         ):
25 |             return HttpResponseForbidden(
26 |                 robot.make_error_page(
27 |                     html.escape(request.build_absolute_uri())
28 |                 )
29 |             )
30 |         if request.method == "GET":
31 |             return HttpResponse(request.GET.get("echostr", ""))
32 |         elif request.method == "POST":
33 |             message = robot.parse_message(
34 |                 request.body,
35 |                 timestamp=timestamp,
36 |                 nonce=nonce,
37 |                 msg_signature=request.GET.get("msg_signature", "")
38 |             )
39 |             return HttpResponse(
40 |                 robot.get_encrypted_reply(message),
41 |                 content_type="application/xml;charset=utf-8"
42 |             )
43 |         return HttpResponseNotAllowed(['GET', 'POST'])
44 | 
45 |     return werobot_view
46 | 


--------------------------------------------------------------------------------
/werobot/contrib/error.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="zh">
 3 |     <head>
 4 |         <meta charset="UTF-8" />
 5 |         <title>WeRoBot</title>
 6 |         <style type="text/css">
 7 |             body,
 8 |             html {
 9 |                 height: 100%;
10 |                 width: 100%;
11 |                 color: #424242;
12 |                 margin: 0;
13 |             }
14 | 
15 |             body {
16 |                 display: -webkit-flex;
17 |                 display: -moz-flex;
18 |                 display: -ms-flex;
19 |                 display: -o-flex;
20 |                 display: flex;
21 |                 -webkit-flex-direction: column;
22 |                 -moz-flex-direction: column;
23 |                 -ms-flex-direction: column;
24 |                 -o-flex-direction: column;
25 |                 flex-direction: column;
26 |                 -ms-align-items: center;
27 |                 align-items: center;
28 |                 justify-content: center;
29 |                 background-color: #f5f5f5;
30 |             }
31 | 
32 |             a {
33 |                 color: #424242;
34 |                 text-decoration: none;
35 |                 border-bottom: 0;
36 |                 transition: all ease-in-out 0.5s;
37 |                 -webkit-transition: all ease-in-out 0.5s;
38 |                 border-color: #f5f5f5;
39 |             }
40 | 
41 |             a:hover {
42 |                 border-bottom: 1px solid transparent;
43 |                 border-color: #424242;
44 |             }
45 | 
46 |             img {
47 |                 margin-bottom: 1em;
48 |                 height: 300px;
49 |                 width: 300px;
50 |             }
51 |         </style>
52 |     </head>
53 |     <body>
54 |         <img
55 |             src="https://cdn.rawgit.com/whtsky/WeRoBot/master/artwork/logo.svg"
56 |             alt=""
57 |         />
58 |         <h1>
59 |             这是一个
60 |             <a href="https://github.com/whtsky/WeRoBot/">WeRoBot</a> 应用
61 |         </h1>
62 |         <p>想要使用本机器人,请在微信后台中将 URL 设置为</p>
63 |         <pre>{url}</pre>
64 |         <p>并将 Token 值设置正确。</p>
65 |         <p>更多信息请与网站所有者联系</p>
66 |     </body>
67 | </html>
68 | 


--------------------------------------------------------------------------------
/werobot/contrib/flask.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from flask import request, make_response
 4 | 
 5 | import html
 6 | 
 7 | 
 8 | def make_view(robot):
 9 |     """
10 |     为一个 BaseRoBot 生成 Flask view。
11 | 
12 |     Usage ::
13 | 
14 |         from werobot import WeRoBot
15 | 
16 |         robot = WeRoBot(token='token')
17 | 
18 | 
19 |         @robot.handler
20 |         def hello(message):
21 |             return 'Hello World!'
22 | 
23 |         from flask import Flask
24 |         from werobot.contrib.flask import make_view
25 | 
26 |         app = Flask(__name__)
27 |         app.add_url_rule(rule='/robot/', # WeRoBot 的绑定地址
28 |                         endpoint='werobot', # Flask 的 endpoint
29 |                         view_func=make_view(robot),
30 |                         methods=['GET', 'POST'])
31 | 
32 |     :param robot: 一个 BaseRoBot 实例
33 |     :return: 一个标准的 Flask view
34 |     """
35 |     def werobot_view():
36 |         timestamp = request.args.get('timestamp', '')
37 |         nonce = request.args.get('nonce', '')
38 |         signature = request.args.get('signature', '')
39 |         if not robot.check_signature(
40 |             timestamp,
41 |             nonce,
42 |             signature,
43 |         ):
44 |             return robot.make_error_page(html.escape(request.url)), 403
45 |         if request.method == 'GET':
46 |             return request.args['echostr']
47 | 
48 |         message = robot.parse_message(
49 |             request.data,
50 |             timestamp=timestamp,
51 |             nonce=nonce,
52 |             msg_signature=request.args.get('msg_signature', '')
53 |         )
54 |         response = make_response(robot.get_encrypted_reply(message))
55 |         response.headers['content_type'] = 'application/xml'
56 |         return response
57 | 
58 |     return werobot_view
59 | 


--------------------------------------------------------------------------------
/werobot/contrib/tornado.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from tornado.web import RequestHandler
 4 | 
 5 | import html
 6 | 
 7 | 
 8 | def make_handler(robot):
 9 |     """
10 |     为一个 BaseRoBot 生成 Tornado Handler。
11 | 
12 |     Usage ::
13 | 
14 |         import tornado.ioloop
15 |         import tornado.web
16 |         from werobot import WeRoBot
17 |         from tornado_werobot import make_handler
18 | 
19 |         robot = WeRoBot(token='token')
20 | 
21 | 
22 |         @robot.handler
23 |         def hello(message):
24 |             return 'Hello World!'
25 | 
26 |         application = tornado.web.Application([
27 |             (r"/", make_handler(robot)),
28 |         ])
29 | 
30 |     :param robot: 一个 BaseRoBot 实例。
31 |     :return: 一个标准的 Tornado Handler
32 |     """
33 |     class WeRoBotHandler(RequestHandler):
34 |         def prepare(self):
35 |             timestamp = self.get_argument('timestamp', '')
36 |             nonce = self.get_argument('nonce', '')
37 |             signature = self.get_argument('signature', '')
38 | 
39 |             if not robot.check_signature(
40 |                 timestamp=timestamp, nonce=nonce, signature=signature
41 |             ):
42 |                 self.set_status(403)
43 |                 self.write(
44 |                     robot.make_error_page(
45 |                         html.escape(
46 |                             self.request.protocol + "://" + self.request.host +
47 |                             self.request.uri
48 |                         )
49 |                     )
50 |                 )
51 |                 return
52 | 
53 |         def get(self):
54 |             echostr = self.get_argument('echostr', '')
55 |             self.write(echostr)
56 | 
57 |         def post(self):
58 |             timestamp = self.get_argument('timestamp', '')
59 |             nonce = self.get_argument('nonce', '')
60 |             msg_signature = self.get_argument('msg_signature', '')
61 |             message = robot.parse_message(
62 |                 self.request.body,
63 |                 timestamp=timestamp,
64 |                 nonce=nonce,
65 |                 msg_signature=msg_signature
66 |             )
67 |             self.set_header("Content-Type", "application/xml;charset=utf-8")
68 |             self.write(robot.get_encrypted_reply(message))
69 | 
70 |     return WeRoBotHandler
71 | 


--------------------------------------------------------------------------------
/werobot/crypto/__init__.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | 
  3 | import base64
  4 | import socket
  5 | import struct
  6 | import time
  7 | 
  8 | try:
  9 |     from cryptography.hazmat.primitives.ciphers import (
 10 |         Cipher, algorithms, modes
 11 |     )
 12 |     from cryptography.hazmat.backends import default_backend
 13 | except ImportError:  # pragma: no cover
 14 |     raise RuntimeError("You need to install Cryptography.")  # pragma: no cover
 15 | 
 16 | from . import pkcs7
 17 | from .exceptions import (
 18 |     UnvalidEncodingAESKey, AppIdValidationError, InvalidSignature
 19 | )
 20 | from werobot.utils import (
 21 |     to_text, to_binary, generate_token, byte2int, get_signature
 22 | )
 23 | 
 24 | 
 25 | class PrpCrypto(object):
 26 |     """
 27 |     提供接收和推送给公众平台消息的加解密接口
 28 |     """
 29 |     def __init__(self, key):
 30 |         key = to_binary(key)
 31 |         self.cipher = Cipher(
 32 |             algorithms.AES(key),
 33 |             modes.CBC(key[:16]),
 34 |             backend=default_backend()
 35 |         )
 36 | 
 37 |     def get_random_string(self):
 38 |         """
 39 |         :return: 长度为16的随即字符串
 40 |         """
 41 |         return generate_token(16)
 42 | 
 43 |     def encrypt(self, text, app_id):
 44 |         """
 45 |         对明文进行加密
 46 |         :param text: 需要加密的明文
 47 |         :param app_id: 微信公众平台的 AppID
 48 |         :return: 加密后的字符串
 49 |         """
 50 |         text = b"".join(
 51 |             [
 52 |                 to_binary(self.get_random_string()),
 53 |                 struct.pack(b"I", socket.htonl(len(to_binary(text)))),
 54 |                 to_binary(text),
 55 |                 to_binary(app_id)
 56 |             ]
 57 |         )
 58 |         text = pkcs7.encode(text)
 59 |         encryptor = self.cipher.encryptor()
 60 |         ciphertext = to_binary(encryptor.update(text) + encryptor.finalize())
 61 |         return base64.b64encode(ciphertext)
 62 | 
 63 |     def decrypt(self, text, app_id):
 64 |         """
 65 |         对密文进行解密
 66 |         :param text: 需要解密的密文
 67 |         :param app_id: 微信公众平台的 AppID
 68 |         :return: 解密后的字符串
 69 |         """
 70 |         text = to_binary(text)
 71 |         decryptor = self.cipher.decryptor()
 72 |         plain_text = decryptor.update(base64.b64decode(text)
 73 |                                       ) + decryptor.finalize()
 74 | 
 75 |         padding = byte2int(plain_text, -1)
 76 |         content = plain_text[16:-padding]
 77 | 
 78 |         xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
 79 |         xml_content = content[4:xml_len + 4]
 80 |         from_appid = content[xml_len + 4:]
 81 | 
 82 |         if to_text(from_appid) != app_id:
 83 |             raise AppIdValidationError(text, app_id)
 84 | 
 85 |         return xml_content
 86 | 
 87 | 
 88 | class MessageCrypt(object):
 89 |     ENCRYPTED_MESSAGE_XML = """
 90 | <xml>
 91 | <Encrypt><![CDATA[{encrypt}]]></Encrypt>
 92 | <MsgSignature><![CDATA[{signature}]]></MsgSignature>
 93 | <TimeStamp>{timestamp}</TimeStamp>
 94 | <Nonce><![CDATA[{nonce}]]></Nonce>
 95 | </xml>
 96 |     """.strip()
 97 | 
 98 |     def __init__(self, token, encoding_aes_key, app_id):
 99 |         key = base64.b64decode(to_binary(encoding_aes_key + '='))
100 |         if len(key) != 32:
101 |             raise UnvalidEncodingAESKey(encoding_aes_key)
102 |         self.prp_crypto = PrpCrypto(key)
103 | 
104 |         self.token = token
105 |         self.app_id = app_id
106 | 
107 |     def decrypt_message(self, timestamp, nonce, msg_signature, encrypt_msg):
108 |         """
109 |         解密收到的微信消息
110 |         :param timestamp: 请求 URL 中收到的 timestamp
111 |         :param nonce: 请求 URL 中收到的 nonce
112 |         :param msg_signature: 请求 URL 中收到的 msg_signature
113 |         :param encrypt_msg: 收到的加密文本. ( XML 中的 <Encrypt> 部分 )
114 |         :return: 解密后的 XML 文本
115 |         """
116 |         signature = get_signature(self.token, timestamp, nonce, encrypt_msg)
117 |         if signature != msg_signature:
118 |             raise InvalidSignature(msg_signature)
119 |         return self.prp_crypto.decrypt(encrypt_msg, self.app_id)
120 | 
121 |     def encrypt_message(self, reply, timestamp=None, nonce=None):
122 |         """
123 |         加密微信回复
124 |         :param reply: 加密前的回复
125 |         :type reply: WeChatReply 或 XML 文本
126 |         :return: 加密后的回复文本
127 |         """
128 |         if hasattr(reply, "render"):
129 |             reply = reply.render()
130 | 
131 |         timestamp = timestamp or to_text(int(time.time()))
132 |         nonce = nonce or generate_token(5)
133 |         encrypt = to_text(self.prp_crypto.encrypt(reply, self.app_id))
134 |         signature = get_signature(self.token, timestamp, nonce, encrypt)
135 |         return to_text(
136 |             self.ENCRYPTED_MESSAGE_XML.format(
137 |                 encrypt=encrypt,
138 |                 signature=signature,
139 |                 timestamp=timestamp,
140 |                 nonce=nonce
141 |             )
142 |         )
143 | 


--------------------------------------------------------------------------------
/werobot/crypto/exceptions.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | 
 4 | class UnvalidEncodingAESKey(Exception):
 5 |     pass
 6 | 
 7 | 
 8 | class AppIdValidationError(Exception):
 9 |     pass
10 | 
11 | 
12 | class InvalidSignature(Exception):
13 |     pass
14 | 


--------------------------------------------------------------------------------
/werobot/crypto/pkcs7.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.utils import to_binary
 4 | 
 5 | _BLOCK_SIZE = 32
 6 | 
 7 | 
 8 | def encode(text):
 9 |     # 计算需要填充的位数
10 |     amount_to_pad = _BLOCK_SIZE - (len(text) % _BLOCK_SIZE)
11 |     if not amount_to_pad:
12 |         amount_to_pad = _BLOCK_SIZE
13 |     # 获得补位所用的字符
14 |     pad = chr(amount_to_pad)
15 |     return text + to_binary(pad * amount_to_pad)
16 | 


--------------------------------------------------------------------------------
/werobot/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | 
3 | 
4 | class ConfigError(Exception):
5 |     pass
6 | 


--------------------------------------------------------------------------------
/werobot/logger.py:
--------------------------------------------------------------------------------
 1 | # -*- coding:utf-8 -*-
 2 | 
 3 | import sys
 4 | import time
 5 | import logging
 6 | 
 7 | try:
 8 |     import curses
 9 | 
10 |     assert curses
11 | except ImportError:
12 |     curses = None
13 | 
14 | logger = logging.getLogger("WeRoBot")
15 | 
16 | 
17 | def enable_pretty_logging(logger, level='info'):
18 |     """
19 |     按照配置开启 log 的格式化优化。
20 | 
21 |     :param logger: 配置的 logger 对象
22 |     :param level: 要为 logger 设置的等级
23 |     """
24 |     logger.setLevel(getattr(logging, level.upper()))
25 | 
26 |     if not logger.handlers:
27 |         # Set up color if we are in a tty and curses is installed
28 |         color = False
29 |         if curses and sys.stderr.isatty():
30 |             try:
31 |                 curses.setupterm()
32 |                 if curses.tigetnum("colors") > 0:
33 |                     color = True
34 |             finally:
35 |                 pass
36 |         channel = logging.StreamHandler()
37 |         channel.setFormatter(_LogFormatter(color=color))
38 |         logger.addHandler(channel)
39 | 
40 | 
41 | class _LogFormatter(logging.Formatter):
42 |     def __init__(self, color, *args, **kwargs):
43 |         logging.Formatter.__init__(self, *args, **kwargs)
44 |         self._color = color
45 |         if color:
46 |             fg_color = (
47 |                 curses.tigetstr("setaf") or curses.tigetstr("setf") or b""
48 |             )
49 |             self._colors = {
50 |                 logging.DEBUG: str(curses.tparm(fg_color, 4), "ascii"),  # Blue
51 |                 logging.INFO: str(curses.tparm(fg_color, 2), "ascii"),  # Green
52 |                 logging.WARNING: str(curses.tparm(fg_color, 3),
53 |                                      "ascii"),  # Yellow
54 |                 logging.ERROR: str(curses.tparm(fg_color, 1), "ascii"),  # Red
55 |             }
56 |             self._normal = str(curses.tigetstr("sgr0"), "ascii")
57 | 
58 |     def format(self, record):
59 |         try:
60 |             record.message = record.getMessage()
61 |         except Exception as e:
62 |             record.message = "Bad message (%r): %r" % (e, record.__dict__)
63 |         record.asctime = time.strftime(
64 |             "%y%m%d %H:%M:%S", self.converter(record.created)
65 |         )
66 |         prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % record.__dict__
67 |         if self._color:
68 |             prefix = (
69 |                 self._colors.get(record.levelno, self._normal) + prefix +
70 |                 self._normal
71 |             )
72 |         formatted = prefix + " " + record.message
73 |         if record.exc_info:
74 |             if not record.exc_text:
75 |                 record.exc_text = self.formatException(record.exc_info)
76 |         if record.exc_text:
77 |             formatted = formatted.rstrip() + "\n" + record.exc_text
78 |         return formatted.replace("\n", "\n    ")
79 | 


--------------------------------------------------------------------------------
/werobot/messages/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | 


--------------------------------------------------------------------------------
/werobot/messages/base.py:
--------------------------------------------------------------------------------
 1 | class WeRoBotMetaClass(type):
 2 |     TYPES = {}
 3 | 
 4 |     def __new__(mcs, name, bases, attrs):
 5 |         return type.__new__(mcs, name, bases, attrs)
 6 | 
 7 |     def __init__(cls, name, bases, attrs):
 8 |         if '__type__' in attrs:
 9 |             if isinstance(attrs['__type__'], list):
10 |                 for _type in attrs['__type__']:
11 |                     cls.TYPES[_type] = cls
12 |             else:
13 |                 cls.TYPES[attrs['__type__']] = cls
14 |         type.__init__(cls, name, bases, attrs)
15 | 


--------------------------------------------------------------------------------
/werobot/messages/entries.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | from werobot.utils import to_text
 3 | 
 4 | 
 5 | def get_value(instance, path, default=None):
 6 |     dic = instance.__dict__
 7 |     for entry in path.split('.'):
 8 |         dic = dic.get(entry)
 9 |         if dic is None:
10 |             return default
11 |     return dic or default
12 | 
13 | 
14 | class BaseEntry(object):
15 |     def __init__(self, entry, default=None):
16 |         self.entry = entry
17 |         self.default = default
18 | 
19 | 
20 | class IntEntry(BaseEntry):
21 |     def __get__(self, instance, owner):
22 |         try:
23 |             return int(get_value(instance, self.entry, self.default))
24 |         except TypeError:
25 |             return
26 | 
27 | 
28 | class FloatEntry(BaseEntry):
29 |     def __get__(self, instance, owner):
30 |         try:
31 |             return float(get_value(instance, self.entry, self.default))
32 |         except TypeError:
33 |             return
34 | 
35 | 
36 | class StringEntry(BaseEntry):
37 |     def __get__(self, instance, owner):
38 |         v = get_value(instance, self.entry, self.default)
39 |         if v is not None:
40 |             return to_text(v)
41 |         return v
42 | 


--------------------------------------------------------------------------------
/werobot/messages/events.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | 
  3 | from werobot.messages.entries import StringEntry, IntEntry, FloatEntry
  4 | from werobot.messages.base import WeRoBotMetaClass
  5 | 
  6 | 
  7 | class EventMetaClass(WeRoBotMetaClass):
  8 |     pass
  9 | 
 10 | 
 11 | class WeChatEvent(object, metaclass=EventMetaClass):
 12 |     target = StringEntry('ToUserName')
 13 |     source = StringEntry('FromUserName')
 14 |     time = IntEntry('CreateTime')
 15 |     message_id = IntEntry('MsgID', 0)
 16 | 
 17 |     def __init__(self, message):
 18 |         self.__dict__.update(message)
 19 | 
 20 | 
 21 | class SimpleEvent(WeChatEvent):
 22 |     key = StringEntry('EventKey')
 23 | 
 24 | 
 25 | class TicketEvent(WeChatEvent):
 26 |     key = StringEntry('EventKey')
 27 |     ticket = StringEntry('Ticket')
 28 | 
 29 | 
 30 | class SubscribeEvent(TicketEvent):
 31 |     __type__ = 'subscribe_event'
 32 | 
 33 | 
 34 | class UnSubscribeEvent(WeChatEvent):
 35 |     __type__ = 'unsubscribe_event'
 36 | 
 37 | 
 38 | class ScanEvent(TicketEvent):
 39 |     __type__ = 'scan_event'
 40 | 
 41 | 
 42 | class ScanCodePushEvent(SimpleEvent):
 43 |     __type__ = 'scancode_push_event'
 44 |     scan_type = StringEntry('ScanCodeInfo.ScanType')
 45 |     scan_result = StringEntry('ScanCodeInfo.ScanResult')
 46 | 
 47 | 
 48 | class ScanCodeWaitMsgEvent(ScanCodePushEvent):
 49 |     __type__ = 'scancode_waitmsg_event'
 50 |     scan_type = StringEntry('ScanCodeInfo.ScanType')
 51 |     scan_result = StringEntry('ScanCodeInfo.ScanResult')
 52 | 
 53 | 
 54 | class BasePicEvent(SimpleEvent):
 55 |     count = IntEntry('SendPicsInfo.Count')
 56 | 
 57 |     def __init__(self, message):
 58 |         super(BasePicEvent, self).__init__(message)
 59 |         self.pic_list = list()
 60 |         if self.count > 1:
 61 |             for item in message['SendPicsInfo']['PicList'].pop('item'):
 62 |                 self.pic_list.append({'pic_md5_sum': item['PicMd5Sum']})
 63 |         else:
 64 |             self.pic_list.append(
 65 |                 {
 66 |                     'pic_md5_sum': message['SendPicsInfo']
 67 |                     ['PicList'].pop('item')['PicMd5Sum']
 68 |                 }
 69 |             )
 70 | 
 71 | 
 72 | class PicSysphotoEvent(BasePicEvent):
 73 |     __type__ = 'pic_sysphoto_event'
 74 | 
 75 | 
 76 | class PicPhotoOrAlbumEvent(BasePicEvent):
 77 |     __type__ = 'pic_photo_or_album_event'
 78 | 
 79 | 
 80 | class PicWeixinEvent(BasePicEvent):
 81 |     __type__ = 'pic_weixin_event'
 82 | 
 83 | 
 84 | class LocationSelectEvent(SimpleEvent):
 85 |     __type__ = 'location_select_event'
 86 |     location_x = StringEntry('SendLocationInfo.Location_X')
 87 |     location_y = StringEntry('SendLocationInfo.Location_Y')
 88 |     scale = StringEntry('SendLocationInfo.Scale')
 89 |     label = StringEntry('SendLocationInfo.Label')
 90 |     poi_name = StringEntry('SendLocationInfo.Poiname')
 91 | 
 92 | 
 93 | class ClickEvent(SimpleEvent):
 94 |     __type__ = 'click_event'
 95 | 
 96 | 
 97 | class ViewEvent(SimpleEvent):
 98 |     __type__ = 'view_event'
 99 | 
100 | 
101 | class LocationEvent(WeChatEvent):
102 |     __type__ = 'location_event'
103 |     latitude = FloatEntry('Latitude')
104 |     longitude = FloatEntry('Longitude')
105 |     precision = FloatEntry('Precision')
106 | 
107 | 
108 | class TemplateSendJobFinishEvent(WeChatEvent):
109 |     __type__ = 'templatesendjobfinish_event'
110 |     status = StringEntry('Status')
111 | 
112 | 
113 | class BaseProductEvent(WeChatEvent):
114 |     key_standard = StringEntry('KeyStandard')
115 |     key_str = StringEntry('KeyStr')
116 |     ext_info = StringEntry('ExtInfo')
117 | 
118 | 
119 | class UserScanProductEvent(BaseProductEvent):
120 |     __type__ = 'user_scan_product_event'
121 |     country = StringEntry('Country')
122 |     province = StringEntry('Province')
123 |     city = StringEntry('City')
124 |     sex = IntEntry('Sex')
125 |     scene = IntEntry('Scene')
126 | 
127 | 
128 | class UserScanProductEnterSessionEvent(BaseProductEvent):
129 |     __type__ = 'user_scan_product_enter_session_event'
130 | 
131 | 
132 | class UserScanProductAsyncEvent(BaseProductEvent):
133 |     __type__ = 'user_scan_product_async_event'
134 |     region_code = StringEntry('RegionCode')
135 | 
136 | 
137 | class UserScanProductVerifyActionEvent(WeChatEvent):
138 |     __type__ = 'user_scan_product_verify_action_event'
139 |     key_standard = StringEntry('KeyStandard')
140 |     key_str = StringEntry('KeyStr')
141 |     result = StringEntry('Result')
142 |     reason_msg = StringEntry('ReasonMsg')
143 | 
144 | 
145 | class BaseCardCheckEvent(WeChatEvent):
146 |     card_id = StringEntry('CardId')
147 |     refuse_reason = StringEntry('RefuseReason')
148 | 
149 | 
150 | class CardPassCheckEvent(BaseCardCheckEvent):
151 |     __type__ = 'card_pass_check_event'
152 | 
153 | 
154 | class CardNotPassCheckEvent(BaseCardCheckEvent):
155 |     __type__ = 'card_not_pass_check_event'
156 | 
157 | 
158 | class BaseCardEvent(WeChatEvent):
159 |     card_id = StringEntry('CardId')
160 |     user_card_code = StringEntry('UserCardCode')
161 | 
162 | 
163 | class UserGetCardEvent(BaseCardEvent):
164 |     __type__ = 'user_get_card_event'
165 |     is_give_by_friend = IntEntry('IsGiveByFriend')
166 |     friend_user_name = StringEntry('FriendUserName')
167 |     outer_id = IntEntry('OuterId')
168 |     old_user_card_code = StringEntry('OldUserCardCode')
169 |     outer_str = StringEntry('OuterStr')
170 |     is_restore_member_card = IntEntry('IsRestoreMemberCard')
171 |     is_recommend_by_friend = IntEntry('IsRecommendByFriend')
172 | 
173 | 
174 | class UserGiftingCardEvent(BaseCardEvent):
175 |     __type__ = 'user_gifting_card_event'
176 |     is_return_back = IntEntry('IsReturnBack')
177 |     friend_user_name = StringEntry('FriendUserName')
178 |     is_chat_room = IntEntry('IsChatRoom')
179 | 
180 | 
181 | class UserDelCardEvent(BaseCardEvent):
182 |     __type__ = 'user_del_card_event'
183 | 
184 | 
185 | class UserConsumeCardEvent(BaseCardEvent):
186 |     __type__ = 'user_consume_card_event'
187 |     consume_source = StringEntry('ConsumeSource')
188 |     location_name = StringEntry('LocationName')
189 |     staff_open_id = StringEntry('StaffOpenId')
190 |     verify_code = StringEntry('VerifyCode')
191 |     remark_amount = StringEntry('RemarkAmount')
192 |     outer_str = StringEntry('OuterStr')
193 | 
194 | 
195 | class UserPayFromPayCellEvent(BaseCardEvent):
196 |     __type__ = 'user_pay_from_pay_cell_event'
197 |     trans_id = StringEntry('TransId')
198 |     location_id = IntEntry('LocationId')
199 |     fee = StringEntry('Fee')
200 |     original_fee = StringEntry('OriginalFee')
201 | 
202 | 
203 | class UserViewCardEvent(BaseCardEvent):
204 |     __type__ = 'user_view_card_event'
205 |     outer_str = StringEntry('OuterStr')
206 | 
207 | 
208 | class UserEnterSessionFromCardEvent(BaseCardEvent):
209 |     __type__ = 'user_enter_session_from_card_event'
210 | 
211 | 
212 | class UpdateMemberCardEvent(BaseCardEvent):
213 |     __type__ = 'update_member_card_event'
214 |     modify_bonus = IntEntry('ModifyBonus')
215 |     modify_balance = IntEntry('ModifyBalance')
216 | 
217 | 
218 | class CardSkuRemindEvent(WeChatEvent):
219 |     __type__ = 'card_sku_remind_event'
220 |     card_id = StringEntry('CardId')
221 |     detail = StringEntry('Detail')
222 | 
223 | 
224 | class CardPayOrderEvent(WeChatEvent):
225 |     __type__ = 'card_pay_order_event'
226 |     order_id = StringEntry('OrderId')
227 |     status = StringEntry('Status')
228 |     create_order_time = IntEntry('CreateOrderTime')
229 |     pay_finish_time = IntEntry('PayFinishTime')
230 |     desc = StringEntry('Desc')
231 |     free_coin_count = StringEntry('FreeCoinCount')
232 |     pay_coin_count = StringEntry('PayCoinCount')
233 |     refund_free_coin_count = StringEntry('RefundFreeCoinCount')
234 |     refund_pay_coin_count = StringEntry('RefundPayCoinCount')
235 |     order_type = StringEntry('OrderType')
236 |     memo = StringEntry('Memo')
237 |     receipt_info = StringEntry('ReceiptInfo')
238 | 
239 | 
240 | class SubmitMembercardUserInfoEvent(BaseCardEvent):
241 |     __type__ = 'submit_membercard_user_info_event'
242 | 
243 | 
244 | class UnknownEvent(WeChatEvent):
245 |     __type__ = 'unknown_event'
246 | 


--------------------------------------------------------------------------------
/werobot/messages/messages.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.messages.entries import StringEntry, IntEntry, FloatEntry
 4 | from werobot.messages.base import WeRoBotMetaClass
 5 | 
 6 | 
 7 | class MessageMetaClass(WeRoBotMetaClass):
 8 |     pass
 9 | 
10 | 
11 | class WeChatMessage(object, metaclass=MessageMetaClass):
12 |     message_id = IntEntry('MsgId', 0)
13 |     target = StringEntry('ToUserName')
14 |     source = StringEntry('FromUserName')
15 |     time = IntEntry('CreateTime', 0)
16 | 
17 |     def __init__(self, message):
18 |         self.__dict__.update(message)
19 | 
20 | 
21 | class TextMessage(WeChatMessage):
22 |     __type__ = 'text'
23 |     content = StringEntry('Content')
24 | 
25 | 
26 | class ImageMessage(WeChatMessage):
27 |     __type__ = 'image'
28 |     img = StringEntry('PicUrl')
29 | 
30 | 
31 | class LocationMessage(WeChatMessage):
32 |     __type__ = 'location'
33 |     location_x = FloatEntry('Location_X')
34 |     location_y = FloatEntry('Location_Y')
35 |     label = StringEntry('Label')
36 |     scale = IntEntry('Scale')
37 | 
38 |     @property
39 |     def location(self):
40 |         return self.location_x, self.location_y
41 | 
42 | 
43 | class LinkMessage(WeChatMessage):
44 |     __type__ = 'link'
45 |     title = StringEntry('Title')
46 |     description = StringEntry('Description')
47 |     url = StringEntry('Url')
48 | 
49 | 
50 | class VoiceMessage(WeChatMessage):
51 |     __type__ = 'voice'
52 |     media_id = StringEntry('MediaId')
53 |     format = StringEntry('Format')
54 |     recognition = StringEntry('Recognition')
55 | 
56 | 
57 | class VideoMessage(WeChatMessage):
58 |     __type__ = ['video', 'shortvideo']
59 |     media_id = StringEntry('MediaId')
60 |     thumb_media_id = StringEntry('ThumbMediaId')
61 | 
62 | 
63 | class UnknownMessage(WeChatMessage):
64 |     __type__ = 'unknown'
65 | 


--------------------------------------------------------------------------------
/werobot/parser.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | import xmltodict
 4 | from werobot.messages.messages import MessageMetaClass, UnknownMessage
 5 | from werobot.messages.events import EventMetaClass, UnknownEvent
 6 | 
 7 | 
 8 | def parse_user_msg(xml):
 9 |     message = process_message(parse_xml(xml)) if xml else None
10 |     return message
11 | 
12 | 
13 | def parse_xml(text):
14 |     xml_dict = xmltodict.parse(text)["xml"]
15 |     xml_dict["raw"] = text
16 |     return xml_dict
17 | 
18 | 
19 | def process_message(message):
20 |     """
21 |     Process a message dict and return a Message Object
22 |     :param message: Message dict returned by `parse_xml` function
23 |     :return: Message Object
24 |     """
25 |     message["type"] = message.pop("MsgType").lower()
26 |     if message["type"] == 'event':
27 |         message["type"] = str(message.pop("Event")).lower() + '_event'
28 |         message_type = EventMetaClass.TYPES.get(message["type"], UnknownEvent)
29 |     else:
30 |         message_type = MessageMetaClass.TYPES.get(
31 |             message["type"], UnknownMessage
32 |         )
33 |     return message_type(message)
34 | 


--------------------------------------------------------------------------------
/werobot/pay.py:
--------------------------------------------------------------------------------
  1 | # -*- coding:utf-8 -*-
  2 | 
  3 | from hashlib import sha1, md5
  4 | from urllib import urlencode
  5 | import time
  6 | from werobot.client import Client
  7 | from werobot.utils import pay_sign_dict, generate_token
  8 | from functools import partial
  9 | 
 10 | NATIVE_BASE_URL = 'weixin://wxpay/bizpayurl?'
 11 | 
 12 | 
 13 | class WeixinPayClient(Client):
 14 |     """
 15 |     简化微信支付API操作
 16 |     """
 17 |     def __init__(self, appid, pay_sign_key, pay_partner_id, pay_partner_key):
 18 |         self.pay_sign_key = pay_sign_key
 19 |         self.pay_partner_id = pay_partner_id
 20 |         self.pay_partner_key = pay_partner_key
 21 |         self._pay_sign_dict = partial(pay_sign_dict, appid, pay_sign_key)
 22 | 
 23 |         self._token = None
 24 |         self.token_expires_at = None
 25 | 
 26 |     def create_js_pay_package(self, **package):
 27 |         """
 28 |         签名 pay package 需要的参数
 29 |         详情请参考 支付开发文档
 30 | 
 31 |         :param package: 需要签名的的参数
 32 |         :return: 可以使用的packagestr
 33 |         """
 34 |         assert self.pay_partner_id, "PAY_PARTNER_ID IS EMPTY"
 35 |         assert self.pay_partner_key, "PAY_PARTNER_KEY IS EMPTY"
 36 | 
 37 |         package.update({
 38 |             'partner': self.pay_partner_id,
 39 |         })
 40 | 
 41 |         package.setdefault('bank_type', 'WX')
 42 |         package.setdefault('fee_type', '1')
 43 |         package.setdefault('input_charset', 'UTF-8')
 44 | 
 45 |         params = package.items()
 46 |         params.sort()
 47 | 
 48 |         sign = md5(
 49 |             '&'.join(
 50 |                 [
 51 |                     "%s=%s" % (str(p[0]), str(p[1]))
 52 |                     for p in params + [('key', self.pay_partner_key)]
 53 |                 ]
 54 |             )
 55 |         ).hexdigest().upper()
 56 | 
 57 |         return urlencode(params + [('sign', sign)])
 58 | 
 59 |     def create_js_pay_params(self, **package):
 60 |         """
 61 |         签名 js 需要的参数
 62 |         详情请参考 支付开发文档
 63 | 
 64 |         ::
 65 | 
 66 |             wxclient.create_js_pay_params(
 67 |                 body=标题, out_trade_no=本地订单号, total_fee=价格单位分,
 68 |                 notify_url=通知url,
 69 |                 spbill_create_ip=建议为支付人ip,
 70 |             )
 71 | 
 72 |         :param package: 需要签名的的参数
 73 |         :return: 支付需要的对象
 74 |         """
 75 |         pay_param, sign, sign_type = self._pay_sign_dict(
 76 |             package=self.create_js_pay_package(**package)
 77 |         )
 78 |         pay_param['paySign'] = sign
 79 |         pay_param['signType'] = sign_type
 80 | 
 81 |         # 腾讯这个还得转成大写 JS 才认
 82 |         for key in ['appId', 'timeStamp', 'nonceStr']:
 83 |             pay_param[key] = str(pay_param.pop(key.lower()))
 84 | 
 85 |         return pay_param
 86 | 
 87 |     def create_js_edit_address_param(self, accesstoken, **params):
 88 |         """
 89 |         alpha
 90 |         暂时不建议使用
 91 |         这个接口使用起来十分不友好
 92 |         而且会引起巨大的误解
 93 | 
 94 |         url 需要带上 code 和 state (url?code=xxx&state=1)
 95 |         code 和state 是 oauth 时候回来的
 96 | 
 97 |         token 要传用户的 token
 98 | 
 99 |         这尼玛 你能相信这些支付接口都是腾讯出的?
100 |         """
101 |         params.update(
102 |             {
103 |                 'appId': self.appid,
104 |                 'nonceStr': generate_token(8),
105 |                 'timeStamp': int(time.time())
106 |             }
107 |         )
108 | 
109 |         _params = [(k.lower(), str(v)) for k, v in params.items()]
110 |         _params += [('accesstoken', accesstoken)]
111 |         _params.sort()
112 | 
113 |         string1 = '&'.join(["%s=%s" % (p[0], p[1]) for p in _params])
114 |         sign = sha1(string1).hexdigest()
115 | 
116 |         params = dict([(k, str(v)) for k, v in params.items()])
117 | 
118 |         params['addrSign'] = sign
119 |         params['signType'] = 'sha1'
120 |         params['scope'] = params.get('scope', 'jsapi_address')
121 | 
122 |         return params
123 | 
124 |     def create_native_pay_url(self, productid):
125 |         """
126 |         创建 native pay url
127 |         详情请参考 支付开发文档
128 | 
129 |         :param productid: 本地商品ID
130 |         :return: 返回URL
131 |         """
132 | 
133 |         params, sign, = self._pay_sign_dict(productid=productid)
134 | 
135 |         params['sign'] = sign
136 | 
137 |         return NATIVE_BASE_URL + urlencode(params)
138 | 
139 |     def pay_deliver_notify(self, **deliver_info):
140 |         """
141 |         通知 腾讯发货
142 | 
143 |         一般形式 ::
144 |             wxclient.pay_delivernotify(
145 |                 openid=openid,
146 |                 transid=transaction_id,
147 |                 out_trade_no=本地订单号,
148 |                 deliver_timestamp=int(time.time()),
149 |                 deliver_status="1",
150 |                 deliver_msg="ok"
151 |             )
152 | 
153 |         :param 需要签名的的参数
154 |         :return: 支付需要的对象
155 |         """
156 |         params, sign, _ = self._pay_sign_dict(
157 |             add_noncestr=False, add_timestamp=False, **deliver_info
158 |         )
159 | 
160 |         params['app_signature'] = sign
161 |         params['sign_method'] = 'sha1'
162 | 
163 |         return self.post(
164 |             url="https://api.weixin.qq.com/pay/delivernotify", data=params
165 |         )
166 | 
167 |     def pay_order_query(self, out_trade_no):
168 |         """
169 |         查询订单状态
170 |         一般用于无法确定 订单状态时候补偿
171 | 
172 |         :param out_trade_no: 本地订单号
173 |         :return: 订单信息dict
174 |         """
175 | 
176 |         package = {
177 |             'partner': self.pay_partner_id,
178 |             'out_trade_no': out_trade_no,
179 |         }
180 | 
181 |         _package = package.items()
182 |         _package.sort()
183 | 
184 |         s = '&'.join(
185 |             [
186 |                 "%s=%s" % (p[0], str(p[1]))
187 |                 for p in (_package + [('key', self.pay_partner_key)])
188 |             ]
189 |         )
190 |         package['sign'] = md5(s).hexdigest().upper()
191 | 
192 |         package = '&'.join(["%s=%s" % (p[0], p[1]) for p in package.items()])
193 | 
194 |         params, sign, _ = self._pay_sign_dict(
195 |             add_noncestr=False, package=package
196 |         )
197 | 
198 |         params['app_signature'] = sign
199 |         params['sign_method'] = 'sha1'
200 | 
201 |         return self.post(
202 |             url="https://api.weixin.qq.com/pay/orderquery", data=params
203 |         )
204 | 


--------------------------------------------------------------------------------
/werobot/replies.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | import time
  3 | 
  4 | from collections import defaultdict, namedtuple
  5 | from werobot.utils import is_string, to_text
  6 | 
  7 | 
  8 | def renderable_named_tuple(typename, field_names, tempalte):
  9 |     class TMP(namedtuple(typename=typename, field_names=field_names)):
 10 |         __TEMPLATE__ = tempalte
 11 | 
 12 |         @property
 13 |         def args(self):
 14 |             # https://bugs.python.org/issue24931
 15 |             return dict(zip(self._fields, self))
 16 | 
 17 |         def process_args(self, kwargs):
 18 |             args = defaultdict(str)
 19 |             for k, v in kwargs.items():
 20 |                 if is_string(v):
 21 |                     v = to_text(v)
 22 |                 args[k] = v
 23 |             return args
 24 | 
 25 |         def render(self):
 26 |             return to_text(
 27 |                 self.__TEMPLATE__.format(**self.process_args(self.args))
 28 |             )
 29 | 
 30 |     TMP.__name__ = typename
 31 |     return TMP
 32 | 
 33 | 
 34 | class WeChatReply(object):
 35 |     def process_args(self, args):
 36 |         pass
 37 | 
 38 |     def __init__(self, message=None, **kwargs):
 39 |         if message and "source" not in kwargs:
 40 |             kwargs["source"] = message.target
 41 | 
 42 |         if message and "target" not in kwargs:
 43 |             kwargs["target"] = message.source
 44 | 
 45 |         if 'time' not in kwargs:
 46 |             kwargs["time"] = int(time.time())
 47 | 
 48 |         args = defaultdict(str)
 49 |         for k, v in kwargs.items():
 50 |             if is_string(v):
 51 |                 v = to_text(v)
 52 |             args[k] = v
 53 |         self.process_args(args)
 54 |         self._args = args
 55 | 
 56 |     def render(self):
 57 |         return to_text(self.TEMPLATE.format(**self._args))
 58 | 
 59 |     def __getattr__(self, item):
 60 |         if item in self._args:
 61 |             return self._args[item]
 62 | 
 63 | 
 64 | class TextReply(WeChatReply):
 65 |     TEMPLATE = to_text(
 66 |         """
 67 |     <xml>
 68 |     <ToUserName><![CDATA[{target}]]></ToUserName>
 69 |     <FromUserName><![CDATA[{source}]]></FromUserName>
 70 |     <CreateTime>{time}</CreateTime>
 71 |     <MsgType><![CDATA[text]]></MsgType>
 72 |     <Content><![CDATA[{content}]]></Content>
 73 |     </xml>
 74 |     """
 75 |     )
 76 | 
 77 | 
 78 | class ImageReply(WeChatReply):
 79 |     TEMPLATE = to_text(
 80 |         """
 81 |     <xml>
 82 |     <ToUserName><![CDATA[{target}]]></ToUserName>
 83 |     <FromUserName><![CDATA[{source}]]></FromUserName>
 84 |     <CreateTime>{time}</CreateTime>
 85 |     <MsgType><![CDATA[image]]></MsgType>
 86 |     <Image>
 87 |     <MediaId><![CDATA[{media_id}]]></MediaId>
 88 |     </Image>
 89 |     </xml>
 90 |     """
 91 |     )
 92 | 
 93 | 
 94 | class VoiceReply(WeChatReply):
 95 |     TEMPLATE = to_text(
 96 |         """
 97 |     <xml>
 98 |     <ToUserName><![CDATA[{target}]]></ToUserName>
 99 |     <FromUserName><![CDATA[{source}]]></FromUserName>
100 |     <CreateTime>{time}</CreateTime>
101 |     <MsgType><![CDATA[voice]]></MsgType>
102 |     <Voice>
103 |     <MediaId><![CDATA[{media_id}]]></MediaId>
104 |     </Voice>
105 |     </xml>
106 |     """
107 |     )
108 | 
109 | 
110 | class VideoReply(WeChatReply):
111 |     TEMPLATE = to_text(
112 |         """
113 |     <xml>
114 |     <ToUserName><![CDATA[{target}]]></ToUserName>
115 |     <FromUserName><![CDATA[{source}]]></FromUserName>
116 |     <CreateTime>{time}</CreateTime>
117 |     <MsgType><![CDATA[video]]></MsgType>
118 |     <Video>
119 |     <MediaId><![CDATA[{media_id}]]></MediaId>
120 |     <Title><![CDATA[{title}]]></Title>
121 |     <Description><![CDATA[{description}]]></Description>
122 |     </Video>
123 |     </xml>
124 |     """
125 |     )
126 | 
127 |     def process_args(self, args):
128 |         args.setdefault('title', '')
129 |         args.setdefault('description', '')
130 | 
131 | 
132 | Article = renderable_named_tuple(
133 |     typename="Article",
134 |     field_names=("title", "description", "img", "url"),
135 |     tempalte=to_text(
136 |         """
137 |     <item>
138 |     <Title><![CDATA[{title}]]></Title>
139 |     <Description><![CDATA[{description}]]></Description>
140 |     <PicUrl><![CDATA[{img}]]></PicUrl>
141 |     <Url><![CDATA[{url}]]></Url>
142 |     </item>
143 |     """
144 |     )
145 | )
146 | 
147 | 
148 | class ArticlesReply(WeChatReply):
149 |     TEMPLATE = to_text(
150 |         """
151 |     <xml>
152 |     <ToUserName><![CDATA[{target}]]></ToUserName>
153 |     <FromUserName><![CDATA[{source}]]></FromUserName>
154 |     <CreateTime>{time}</CreateTime>
155 |     <MsgType><![CDATA[news]]></MsgType>
156 |     <Content><![CDATA[{content}]]></Content>
157 |     <ArticleCount>{count}</ArticleCount>
158 |     <Articles>{items}</Articles>
159 |     </xml>
160 |     """
161 |     )
162 | 
163 |     def __init__(self, message=None, **kwargs):
164 |         super(ArticlesReply, self).__init__(message, **kwargs)
165 |         self._articles = []
166 | 
167 |     def add_article(self, article):
168 |         if len(self._articles) >= 10:
169 |             raise AttributeError(
170 |                 "Can't add more than 10 articles"
171 |                 " in an ArticlesReply"
172 |             )
173 |         else:
174 |             self._articles.append(article)
175 | 
176 |     def render(self):
177 |         items = []
178 |         for article in self._articles:
179 |             items.append(article.render())
180 |         self._args["items"] = ''.join(items)
181 |         self._args["count"] = len(items)
182 |         if "content" not in self._args:
183 |             self._args["content"] = ''
184 |         return ArticlesReply.TEMPLATE.format(**self._args)
185 | 
186 | 
187 | class MusicReply(WeChatReply):
188 |     TEMPLATE = to_text(
189 |         """
190 |     <xml>
191 |     <ToUserName><![CDATA[{target}]]></ToUserName>
192 |     <FromUserName><![CDATA[{source}]]></FromUserName>
193 |     <CreateTime>{time}</CreateTime>
194 |     <MsgType><![CDATA[music]]></MsgType>
195 |     <Music>
196 |     <Title><![CDATA[{title}]]></Title>
197 |     <Description><![CDATA[{description}]]></Description>
198 |     <MusicUrl><![CDATA[{url}]]></MusicUrl>
199 |     <HQMusicUrl><![CDATA[{hq_url}]]></HQMusicUrl>
200 |     </Music>
201 |     </xml>
202 |     """
203 |     )
204 | 
205 |     def process_args(self, args):
206 |         if 'hq_url' not in args:
207 |             args['hq_url'] = args['url']
208 | 
209 | 
210 | class TransferCustomerServiceReply(WeChatReply):
211 |     @property
212 |     def TEMPLATE(self):
213 |         if 'account' in self._args:
214 |             return to_text(
215 |                 """
216 |             <xml>
217 |             <ToUserName><![CDATA[{target}]]></ToUserName>
218 |             <FromUserName><![CDATA[{source}]]></FromUserName>
219 |             <CreateTime>{time}</CreateTime>
220 |             <MsgType><![CDATA[transfer_customer_service]]></MsgType>
221 |             <TransInfo>
222 |                  <KfAccount><![CDATA[{account}]]></KfAccount>
223 |              </TransInfo>
224 |             </xml>
225 |             """
226 |             )
227 |         else:
228 |             return to_text(
229 |                 """
230 |             <xml>
231 |             <ToUserName><![CDATA[{target}]]></ToUserName>
232 |             <FromUserName><![CDATA[{source}]]></FromUserName>
233 |             <CreateTime>{time}</CreateTime>
234 |             <MsgType><![CDATA[transfer_customer_service]]></MsgType>
235 |             </xml>
236 |             """
237 |             )
238 | 
239 | 
240 | class SuccessReply(WeChatReply):
241 |     def render(self):
242 |         return "success"
243 | 
244 | 
245 | def process_function_reply(reply, message=None):
246 |     if is_string(reply):
247 |         return TextReply(message=message, content=reply)
248 |     elif isinstance(reply, list) and all([len(x) == 4 for x in reply]):
249 |         if len(reply) > 10:
250 |             raise AttributeError(
251 |                 "Can't add more than 10 articles"
252 |                 " in an ArticlesReply"
253 |             )
254 |         r = ArticlesReply(message=message)
255 |         for article in reply:
256 |             article = Article(*article)
257 |             r.add_article(article)
258 |         return r
259 |     elif isinstance(reply, list) and 3 <= len(reply) <= 4:
260 |         if len(reply) == 3:
261 |             # 如果数组长度为3, 那么高质量音乐链接的网址和普通质量的网址相同。
262 |             reply.append(reply[-1])
263 |         title, description, url, hq_url = reply
264 |         return MusicReply(
265 |             message=message,
266 |             title=title,
267 |             description=description,
268 |             url=url,
269 |             hq_url=hq_url
270 |         )
271 |     return reply
272 | 


--------------------------------------------------------------------------------
/werobot/session/__init__.py:
--------------------------------------------------------------------------------
 1 | class SessionStorage(object):
 2 |     def get(self, id):
 3 |         raise NotImplementedError()
 4 | 
 5 |     def set(self, id, value):
 6 |         raise NotImplementedError()
 7 | 
 8 |     def delete(self, id):
 9 |         raise NotImplementedError()
10 | 
11 |     def __getitem__(self, id):
12 |         return self.get(id)
13 | 
14 |     def __setitem__(self, id, session):
15 |         self.set(id, session)
16 | 
17 |     def __delitem__(self, id):
18 |         self.delete(id)
19 | 


--------------------------------------------------------------------------------
/werobot/session/filestorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | try:
 4 |     import anydbm as dbm
 5 | 
 6 |     assert dbm
 7 | except ImportError:
 8 |     import dbm
 9 | 
10 | from werobot.session import SessionStorage
11 | from werobot.utils import json_loads, json_dumps, to_binary
12 | 
13 | 
14 | class FileStorage(SessionStorage):
15 |     """
16 |     FileStorage 会把你的 Session 数据以 dbm 形式储存在文件中。
17 | 
18 |     :param filename: 文件名, 默认为 ``werobot_session``
19 |     """
20 |     def __init__(self, filename: str = 'werobot_session'):
21 |         try:
22 |             self.db = dbm.open(filename, "c")
23 |         except TypeError:  # pragma: no cover
24 |             # dbm in PyPy requires filename to be binary
25 |             self.db = dbm.open(to_binary(filename), "c")
26 | 
27 |     def get(self, id):
28 |         """
29 |         根据 id 获取数据。
30 | 
31 |         :param id: 要获取的数据的 id
32 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
33 |         """
34 |         try:
35 |             session_json = self.db[id]
36 |         except KeyError:
37 |             session_json = "{}"
38 |         return json_loads(session_json)
39 | 
40 |     def set(self, id, value):
41 |         """
42 |         根据 id 写入数据。
43 | 
44 |         :param id: 要写入的 id
45 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
46 |         """
47 |         self.db[id] = json_dumps(value)
48 | 
49 |     def delete(self, id):
50 |         """
51 |         根据 id 删除数据。
52 | 
53 |         :param id: 要删除的数据的 id
54 |         """
55 |         del self.db[id]
56 | 


--------------------------------------------------------------------------------
/werobot/session/mongodbstorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.session import SessionStorage
 4 | from werobot.utils import json_loads, json_dumps
 5 | 
 6 | 
 7 | class MongoDBStorage(SessionStorage):
 8 |     """
 9 |     MongoDBStorage 会把你的 Session 数据储存在一个 MongoDB Collection 中 ::
10 | 
11 |         import pymongo
12 |         import werobot
13 |         from werobot.session.mongodbstorage import MongoDBStorage
14 | 
15 |         collection = pymongo.MongoClient()["wechat"]["session"]
16 |         session_storage = MongoDBStorage(collection)
17 |         robot = werobot.WeRoBot(token="token", enable_session=True,
18 |                                 session_storage=session_storage)
19 | 
20 | 
21 |     你需要安装 ``pymongo`` 才能使用 MongoDBStorage 。
22 | 
23 |     :param collection: 一个 MongoDB Collection。
24 |     """
25 |     def __init__(self, collection):
26 |         self.collection = collection
27 |         collection.create_index("wechat_id")
28 | 
29 |     def _get_document(self, id):
30 |         return self.collection.find_one({"wechat_id": id})
31 | 
32 |     def get(self, id):
33 |         """
34 |         根据 id 获取数据。
35 | 
36 |         :param id: 要获取的数据的 id
37 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
38 |         """
39 |         document = self._get_document(id)
40 |         if document:
41 |             session_json = document["session"]
42 |             return json_loads(session_json)
43 |         return {}
44 | 
45 |     def set(self, id, value):
46 |         """
47 |         根据 id 写入数据。
48 | 
49 |         :param id: 要写入的 id
50 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
51 |                 """
52 |         session = json_dumps(value)
53 |         self.collection.replace_one(
54 |             {"wechat_id": id}, {
55 |                 "wechat_id": id,
56 |                 "session": session
57 |             },
58 |             upsert=True
59 |         )
60 | 
61 |     def delete(self, id):
62 |         """
63 |         根据 id 删除数据。
64 | 
65 |         :param id: 要删除的数据的 id
66 |         """
67 |         self.collection.delete_one({"wechat_id": id})
68 | 


--------------------------------------------------------------------------------
/werobot/session/mysqlstorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.session import SessionStorage
 4 | from werobot.utils import json_loads, json_dumps
 5 | 
 6 | __CREATE_TABLE_SQL__ = """
 7 | CREATE TABLE IF NOT EXISTS WeRoBot(
 8 | id VARCHAR(100) NOT NULL ,
 9 | value BLOB NOT NULL,
10 | PRIMARY KEY (id)
11 | )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
12 | """
13 | 
14 | 
15 | class MySQLStorage(SessionStorage):
16 |     """
17 |     MySQLStorage 会把你的 Session 数据储存在 MySQL 中 ::
18 | 
19 |         import MySQLdb # 使用 mysqlclient
20 |         import werobot
21 |         from werobot.session.mysqlstorage import MySQLStorage
22 | 
23 |         conn = MySQLdb.connect(user='', db='', passwd='', host='')
24 |         session_storage = MySQLStorage(conn)
25 |         robot = werobot.WeRoBot(token="token", enable_session=True,
26 |                                 session_storage=session_storage)
27 | 
28 |     或者 ::
29 | 
30 |         import pymysql # 使用 pymysql
31 |         import werobot
32 |         from werobot.session.mysqlstorage import MySQLStorage
33 | 
34 |         session_storage = MySQLStorage(
35 |         conn=pymysql.connect(
36 |             user='喵',
37 |             password='喵喵',
38 |             db='werobot',
39 |             host='127.0.0.1',
40 |             charset='utf8'
41 |         ))
42 |         robot = werobot.WeRoBot(token="token", enable_session=True,
43 |                                 session_storage=session_storage)
44 | 
45 |     你需要安装一个 MySQL Client 才能使用 MySQLStorage,比如 ``pymysql``,``mysqlclient`` 。
46 | 
47 |     理论上符合 `PEP-249 <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ 的库都可以使用,\
48 |     测试时使用的是 ``pymysql``。
49 | 
50 |     :param conn: `PEP-249 <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_\
51 |     定义的 Connection 对象
52 |     """
53 |     def __init__(self, conn):
54 |         self.conn = conn
55 |         self.conn.cursor().execute(__CREATE_TABLE_SQL__)
56 | 
57 |     def get(self, id):
58 |         """
59 |         根据 id 获取数据。
60 | 
61 |         :param id: 要获取的数据的 id
62 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
63 |         """
64 |         cur = self.conn.cursor()
65 |         cur.execute("SELECT value FROM WeRoBot WHERE id=%s LIMIT 1;", (id, ))
66 |         session_json = cur.fetchone()
67 |         if session_json is None:
68 |             return {}
69 |         return json_loads(session_json[0])
70 | 
71 |     def set(self, id, value):
72 |         """
73 |         根据 id 写入数据。
74 | 
75 |         :param id: 要写入的 id
76 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
77 |         """
78 |         value = json_dumps(value)
79 |         self.conn.cursor().execute(
80 |             "INSERT INTO WeRoBot (id, value) VALUES (%s,%s) \
81 |                 ON DUPLICATE KEY UPDATE value=%s", (
82 |                 id,
83 |                 value,
84 |                 value,
85 |             )
86 |         )
87 |         self.conn.commit()
88 | 
89 |     def delete(self, id):
90 |         """
91 |         根据 id 删除数据。
92 | 
93 |         :param id: 要删除的数据的 id
94 |         """
95 |         self.conn.cursor().execute("DELETE FROM WeRoBot WHERE id=%s", (id, ))
96 |         self.conn.commit()
97 | 


--------------------------------------------------------------------------------
/werobot/session/postgresqlstorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.session import SessionStorage
 4 | from werobot.utils import json_loads, json_dumps
 5 | 
 6 | __CREATE_TABLE_SQL__ = """
 7 | CREATE TABLE IF NOT EXISTS WeRoBot
 8 | (
 9 | id VARCHAR(100) PRIMARY KEY,
10 | value TEXT NOT NULL
11 | );
12 | """
13 | 
14 | 
15 | class PostgreSQLStorage(SessionStorage):
16 |     """
17 |     PostgreSQLStorage 会把你的 Session 数据储存在 PostgreSQL 中 ::
18 | 
19 |         import psycopg2  # pip install psycopg2-binary
20 |         import werobot
21 |         from werobot.session.postgresqlstorage import PostgreSQLStorage
22 | 
23 |         conn = psycopg2.connect(host='127.0.0.1', port='5432', dbname='werobot', user='nya', password='nyanya')
24 |         session_storage = PostgreSQLStorage(conn)
25 |         robot = werobot.WeRoBot(token="token", enable_session=True,
26 |                                 session_storage=session_storage)
27 | 
28 |     你需要安装一个 ``PostgreSQL Client`` 才能使用 PostgreSQLStorage,比如 ``psycopg2``。
29 | 
30 |     理论上符合 `PEP-249 <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ 的库都可以使用,\
31 |     测试时使用的是 ``psycopg2``。
32 | 
33 |     :param conn: `PEP-249 <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_\
34 |     定义的 Connection 对象
35 |     """
36 |     def __init__(self, conn):
37 |         self.conn = conn
38 |         self.conn.cursor().execute(__CREATE_TABLE_SQL__)
39 | 
40 |     def get(self, id):
41 |         """
42 |         根据 id 获取数据。
43 | 
44 |         :param id: 要获取的数据的 id
45 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
46 |         """
47 |         cur = self.conn.cursor()
48 |         cur.execute("SELECT value FROM WeRoBot WHERE id=%s LIMIT 1;", (id, ))
49 |         session_json = cur.fetchone()
50 |         if session_json is None:
51 |             return {}
52 |         return json_loads(session_json[0])
53 | 
54 |     def set(self, id, value):
55 |         """
56 |         根据 id 写入数据。
57 | 
58 |         :param id: 要写入的 id
59 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
60 |         """
61 |         value = json_dumps(value)
62 |         self.conn.cursor().execute(
63 |             "INSERT INTO WeRoBot (id, value) values (%s, %s) ON CONFLICT (id) DO UPDATE SET value = %s;",
64 |             (
65 |                 id,
66 |                 value,
67 |                 value,
68 |             )
69 |         )
70 |         self.conn.commit()
71 | 
72 |     def delete(self, id):
73 |         """
74 |         根据 id 删除数据。
75 | 
76 |         :param id: 要删除的数据的 id
77 |         """
78 |         self.conn.cursor().execute("DELETE FROM WeRoBot WHERE id=%s", (id, ))
79 |         self.conn.commit()
80 | 


--------------------------------------------------------------------------------
/werobot/session/redisstorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.session import SessionStorage
 4 | from werobot.utils import json_loads, json_dumps
 5 | 
 6 | 
 7 | class RedisStorage(SessionStorage):
 8 |     """
 9 |     RedisStorage 会把你的 Session 数据储存在 Redis 中 ::
10 | 
11 |         import redis
12 |         import werobot
13 |         from werobot.session.redisstorage import RedisStorage
14 | 
15 |         db = redis.Redis()
16 |         session_storage = RedisStorage(db, prefix="my_prefix_")
17 |         robot = werobot.WeRoBot(token="token", enable_session=True,
18 |                                 session_storage=session_storage)
19 | 
20 | 
21 |     你需要安装 ``redis`` 才能使用 RedisStorage 。
22 | 
23 |     :param redis: 一个 Redis Client。
24 |     :param prefix: Reids 中 Session 数据 key 的 prefix 。默认为 ``ws_``
25 |     """
26 |     def __init__(self, redis, prefix='ws_'):
27 |         for method_name in ['get', 'set', 'delete']:
28 |             assert hasattr(redis, method_name)
29 |         self.redis = redis
30 |         self.prefix = prefix
31 | 
32 |     def key_name(self, s):
33 |         return '{prefix}{s}'.format(prefix=self.prefix, s=s)
34 | 
35 |     def get(self, id):
36 |         """
37 |         根据 id 获取数据。
38 | 
39 |         :param id: 要获取的数据的 id
40 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
41 |         """
42 |         id = self.key_name(id)
43 |         session_json = self.redis.get(id) or '{}'
44 |         return json_loads(session_json)
45 | 
46 |     def set(self, id, value):
47 |         """
48 |         根据 id 写入数据。
49 | 
50 |         :param id: 要写入的 id
51 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
52 |         """
53 |         id = self.key_name(id)
54 |         self.redis.set(id, json_dumps(value))
55 | 
56 |     def delete(self, id):
57 |         """
58 |         根据 id 删除数据。
59 | 
60 |         :param id: 要删除的数据的 id
61 |         """
62 |         id = self.key_name(id)
63 |         self.redis.delete(id)
64 | 


--------------------------------------------------------------------------------
/werobot/session/saekvstorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from . import SessionStorage
 4 | 
 5 | 
 6 | class SaeKVDBStorage(SessionStorage):
 7 |     """
 8 |     SaeKVDBStorage 使用SAE 的 KVDB 来保存你的session ::
 9 | 
10 |         import werobot
11 |         from werobot.session.saekvstorage import SaeKVDBStorage
12 | 
13 |         session_storage = SaeKVDBStorage()
14 |         robot = werobot.WeRoBot(token="token", enable_session=True,
15 |                                 session_storage=session_storage)
16 | 
17 |     需要先在后台开启 KVDB 支持
18 | 
19 |     :param prefix: KVDB 中 Session 数据 key 的 prefix 。默认为 ``ws_``
20 |     """
21 |     def __init__(self, prefix='ws_'):
22 |         try:
23 |             import sae.kvdb
24 |         except ImportError:
25 |             raise RuntimeError("SaeKVDBStorage requires SAE environment")
26 |         self.kv = sae.kvdb.KVClient()  # pragma: no cover
27 |         self.prefix = prefix  # pragma: no cover
28 | 
29 |     def key_name(self, s):
30 |         return '{prefix}{s}'.format(prefix=self.prefix, s=s)
31 | 
32 |     def get(self, id):
33 |         """
34 |         根据 id 获取数据。
35 | 
36 |         :param id: 要获取的数据的 id
37 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
38 |         """
39 |         return self.kv.get(self.key_name(id)) or {}
40 | 
41 |     def set(self, id, value):
42 |         """
43 |         根据 id 写入数据。
44 | 
45 |         :param id: 要写入的 id
46 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
47 |         """
48 |         return self.kv.set(self.key_name(id), value)
49 | 
50 |     def delete(self, id):
51 |         """
52 |         根据 id 删除数据。
53 | 
54 |         :param id: 要删除的数据的 id
55 |         """
56 |         return self.kv.delete(self.key_name(id))
57 | 


--------------------------------------------------------------------------------
/werobot/session/sqlitestorage.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | from werobot.session import SessionStorage
 4 | from werobot.utils import json_loads, json_dumps
 5 | import sqlite3
 6 | 
 7 | __CREATE_TABLE_SQL__ = """
 8 | CREATE TABLE IF NOT EXISTS WeRoBot
 9 | (id TEXT PRIMARY KEY NOT NULL ,
10 | value TEXT NOT NULL );
11 | """
12 | 
13 | 
14 | class SQLiteStorage(SessionStorage):
15 |     """
16 |     SQLiteStorge 会把 Session 数据储存在一个 SQLite 数据库文件中 ::
17 | 
18 |         import werobot
19 |         from werobot.session.sqlitestorage import SQLiteStorage
20 | 
21 |         session_storage = SQLiteStorage
22 |         robot = werobot.WeRoBot(token="token", enable_session=True,
23 |                                 session_storage=session_storage)
24 | 
25 |     :param filename: SQLite数据库的文件名, 默认是 ``werobot_session.sqlite3``
26 |     """
27 |     def __init__(self, filename='werobot_session.sqlite3'):
28 |         self.db = sqlite3.connect(filename, check_same_thread=False)
29 |         self.db.text_factory = str
30 |         self.db.execute(__CREATE_TABLE_SQL__)
31 | 
32 |     def get(self, id):
33 |         """
34 |         根据 id 获取数据。
35 | 
36 |         :param id: 要获取的数据的 id
37 |         :return: 返回取到的数据,如果是空则返回一个空的 ``dict`` 对象
38 |         """
39 |         session_json = self.db.execute(
40 |             "SELECT value FROM WeRoBot WHERE id=? LIMIT 1;", (id, )
41 |         ).fetchone()
42 |         if session_json is None:
43 |             return {}
44 |         return json_loads(session_json[0])
45 | 
46 |     def set(self, id, value):
47 |         """
48 |         根据 id 写入数据。
49 | 
50 |         :param id: 要写入的 id
51 |         :param value: 要写入的数据,可以是一个 ``dict`` 对象
52 |         """
53 |         self.db.execute(
54 |             "INSERT OR REPLACE INTO WeRoBot (id, value) VALUES (?,?);",
55 |             (id, json_dumps(value))
56 |         )
57 |         self.db.commit()
58 | 
59 |     def delete(self, id):
60 |         """
61 |         根据 id 删除数据。
62 | 
63 |         :param id: 要删除的数据的 id
64 |         """
65 |         self.db.execute("DELETE FROM WeRoBot WHERE id=?;", (id, ))
66 |         self.db.commit()
67 | 


--------------------------------------------------------------------------------
/werobot/testing.py:
--------------------------------------------------------------------------------
 1 | from .parser import parse_user_msg
 2 | 
 3 | __all__ = ['WeTest']
 4 | 
 5 | 
 6 | class WeTest(object):
 7 |     def __init__(self, app):
 8 |         self._app = app
 9 | 
10 |     def send_xml(self, xml):
11 |         message = parse_user_msg(xml)
12 |         return self._app.get_reply(message)
13 | 


--------------------------------------------------------------------------------
/werobot/utils.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | 
  3 | import io
  4 | import json
  5 | import os
  6 | import random
  7 | import re
  8 | import string
  9 | import time
 10 | from functools import wraps
 11 | from hashlib import sha1
 12 | 
 13 | try:
 14 |     from secrets import choice
 15 | except ImportError:
 16 |     from random import choice
 17 | 
 18 | string_types = (str, bytes)
 19 | 
 20 | re_type = type(re.compile("regex_test"))
 21 | 
 22 | 
 23 | def get_signature(token, timestamp, nonce, *args):
 24 |     sign = [token, timestamp, nonce] + list(args)
 25 |     sign.sort()
 26 |     sign = to_binary(''.join(sign))
 27 |     return sha1(sign).hexdigest()
 28 | 
 29 | 
 30 | def check_signature(token, timestamp, nonce, signature):
 31 |     if not (token and timestamp and nonce and signature):
 32 |         return False
 33 |     sign = get_signature(token, timestamp, nonce)
 34 |     return sign == signature
 35 | 
 36 | 
 37 | def check_token(token):
 38 |     return re.match('^[A-Za-z0-9]{3,32}
#39;, token)
 39 | 
 40 | 
 41 | def cached_property(method):
 42 |     prop_name = '_{}'.format(method.__name__)
 43 | 
 44 |     @wraps(method)
 45 |     def wrapped_func(self, *args, **kwargs):
 46 |         if not hasattr(self, prop_name):
 47 |             setattr(self, prop_name, method(self, *args, **kwargs))
 48 |         return getattr(self, prop_name)
 49 | 
 50 |     return property(wrapped_func)
 51 | 
 52 | 
 53 | def to_text(value, encoding="utf-8") -> str:
 54 |     if isinstance(value, str):
 55 |         return value
 56 |     if isinstance(value, bytes):
 57 |         return value.decode(encoding)
 58 |     return str(value)
 59 | 
 60 | 
 61 | def to_binary(value, encoding="utf-8") -> bytes:
 62 |     if isinstance(value, bytes):
 63 |         return value
 64 |     if isinstance(value, str):
 65 |         return value.encode(encoding)
 66 |     return bytes(value)
 67 | 
 68 | 
 69 | def is_string(value) -> bool:
 70 |     """Check if value's type is `str` or `bytes`
 71 |     """
 72 |     return isinstance(value, string_types)
 73 | 
 74 | 
 75 | def byte2int(s, index=0):
 76 |     """Get the ASCII int value of a character in a string.
 77 | 
 78 |     :param s: a string
 79 |     :param index: the position of desired character
 80 | 
 81 |     :return: ASCII int value
 82 |     """
 83 |     return s[index]
 84 | 
 85 | 
 86 | def generate_token(length=''):
 87 |     if not length:
 88 |         length = random.randint(3, 32)
 89 |     length = int(length)
 90 |     assert 3 <= length <= 32
 91 |     letters = string.ascii_letters + string.digits
 92 |     return ''.join(choice(letters) for _ in range(length))
 93 | 
 94 | 
 95 | def json_loads(s):
 96 |     s = to_text(s)
 97 |     return json.loads(s)
 98 | 
 99 | 
100 | def json_dumps(d):
101 |     return json.dumps(d)
102 | 
103 | 
104 | def pay_sign_dict(
105 |     appid,
106 |     pay_sign_key,
107 |     add_noncestr=True,
108 |     add_timestamp=True,
109 |     add_appid=True,
110 |     **kwargs
111 | ):
112 |     """
113 |     支付参数签名
114 |     """
115 |     assert pay_sign_key, "PAY SIGN KEY IS EMPTY"
116 | 
117 |     if add_appid:
118 |         kwargs.update({'appid': appid})
119 | 
120 |     if add_noncestr:
121 |         kwargs.update({'noncestr': generate_token()})
122 | 
123 |     if add_timestamp:
124 |         kwargs.update({'timestamp': int(time.time())})
125 | 
126 |     params = kwargs.items()
127 | 
128 |     _params = [
129 |         (k.lower(), v) for k, v in kwargs.items() if k.lower() != "appid"
130 |     ]
131 |     _params += [('appid', appid), ('appkey', pay_sign_key)]
132 |     _params.sort()
133 | 
134 |     sign = '&'.join(["%s=%s" % (str(p[0]), str(p[1]))
135 |                      for p in _params]).encode("utf-8")
136 |     sign = sha1(sign).hexdigest()
137 |     sign_type = 'SHA1'
138 | 
139 |     return dict(params), sign, sign_type
140 | 
141 | 
142 | def make_error_page(url):
143 |     with io.open(
144 |         os.path.join(os.path.dirname(__file__), 'contrib/error.html'),
145 |         'r',
146 |         encoding='utf-8'
147 |     ) as error_page:
148 |         return error_page.read().replace('{url}', url)
149 | 
150 | 
151 | def is_regex(value):
152 |     return isinstance(value, re_type)
153 | 


--------------------------------------------------------------------------------