├── .gitignore ├── LICENSE ├── README.md ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── command.rst │ ├── conf.py │ ├── index.rst │ ├── listen.rst │ ├── object.rst │ ├── practice.rst │ └── sender.rst ├── qr.jpeg ├── requirements.txt ├── setup.py └── wechat_sender ├── __init__.py ├── compatible.py ├── listener.py ├── objects.py ├── sender.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.sqlite3 4 | .idea/ 5 | .DS_Store 6 | src/dist/ 7 | src/build/ 8 | .coverage 9 | fabfile.py 10 | test_main.py 11 | main.py 12 | test_client.py 13 | *.pkl 14 | *.puid 15 | build/ 16 | dist/ 17 | wechat_sender.egg-info/ 18 | docs/build/ 19 | docs/source/_static/ 20 | docs/source/_templates/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © RaPoSpectre. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wechat_Sender 2 | 3 | 随时随地发送消息到微信 4 | 5 | http://wechat-sender.readthedocs.io/zh_CN/latest/ 6 | 7 | ## 简介 8 | 9 | wechat_sender 是基于 [wxpy][1] 和 [tornado][2] 实现的一个可以将你的网站、爬虫、脚本等其他应用中各种消息 (日志、报警、运行结果等) 发给到微信的工具 10 | 11 | ## 初衷 12 | 13 | wxpy 基于 itchat 提供了较为完备的微信个人号 API ,而我想使用个人微信来接收我的网站的报警信息以及一些爬虫的结果,因此我写了这个工具。 14 | 15 | ## 安装 16 | 17 | ```python 18 | pip install wechat_sender 19 | ``` 20 | 21 | ## 运行环境 22 | 23 | Python 2.7 及以上 24 | Python 3 及以上 25 | 26 | ## 使用 27 | 28 | 1. 登录微信并启动 wechat_sender 服务. 29 | 30 | ```python 31 | 32 | from wxpy import * 33 | from wechat_sender import * 34 | bot = Bot() 35 | listen(bot) 36 | # 之后 wechat_sender 将持续运行等待接收外部消息 37 | ``` 38 | 39 | 2. 在外部向微信发送消息. 40 | 41 | ```python 42 | 43 | from wechat_sender import Sender 44 | Sender().send('Hello From Wechat Sender') 45 | # Hello From Wechat Sender 这条消息将通过 1 中登录微信的文件助手发送给你 46 | ``` 47 | 48 | 如果你是 wxpy 的使用者,只需更改一句即可使用 wechat_sender: 49 | 50 | 例如这是你本来的代码: 51 | 52 | ```python 53 | # coding: utf-8 54 | from __future__ import unicode_literals 55 | 56 | from wxpy import * 57 | bot = Bot('bot.pkl') 58 | 59 | my_friend = bot.friends().search('xxx')[0] 60 | 61 | my_friend.send('Hello WeChat!') 62 | 63 | @bot.register(Friend) 64 | def reply_test(msg): 65 | msg.reply('test') 66 | 67 | bot.join() 68 | ``` 69 | 70 | 使用 wechat_sender: 71 | 72 | ```python 73 | # coding: utf-8 74 | from __future__ import unicode_literals 75 | 76 | from wxpy import * 77 | from wechat_sender import listen 78 | bot = Bot('bot.pkl') 79 | 80 | my_friend = bot.friends().search('xxx')[0] 81 | 82 | my_friend.send('Hello WeChat!') 83 | 84 | @bot.register(Friend) 85 | def reply_test(msg): 86 | msg.reply('test') 87 | 88 | listen(bot) # 只需改变最后一行代码 89 | ``` 90 | 91 | 之后如果你想在其他程序或脚本中发送微信消息,只需要: 92 | 93 | ```python 94 | # coding: utf-8 95 | from wechat_sender import Sender 96 | Sender().send("test message") 97 | ``` 98 | ## 文档 99 | 100 | http://wechat-sender.readthedocs.io/zh_CN/latest/ 101 | 102 | 103 | ## 交流 104 | 105 | **扫描二维码,验证信息输入 'wechat_sender' 或 '加群' 进入微信交流群** 106 | 107 | ![screenshot](https://raw.githubusercontent.com/bluedazzle/wechat_sender/master/qr.jpeg) 108 | 109 | 110 | ## TODO LIST 111 | 112 | - [x] 多 receiver 113 | - [x] log handler 支持 114 | - [ ] wxpy 掉线邮件通知 115 | - [ ] wxpy 掉线重连 116 | 117 | ## 历史 118 | 119 | **当前版本: 0.1.4** 120 | 121 | 2017.06.12 0.1.4: 122 | 123 | 修复 Python 3 下 sender 发送成功后报错问题 #8 124 | 125 | Sender().send_to 方法增加支持搜索群发送 126 | 127 | Sender 支持指定多个 receivers 128 | 129 | 2017.06.07 0.1.3: 130 | 131 | 优化代码,完善文档、注释 132 | 133 | 2017.06.04 0.1.2: 134 | 135 | 修复 sender timeout 时间过短问题; 136 | 137 | 修复初始化 listen 无 receiver 报错问题 138 | 139 | 增加 LoggingSenderHandler, 提供 log handler 支持 140 | 141 | 2017.05.27 0.1.1: 142 | 143 | 增加多 receiver 支持; 144 | 145 | 2017.05.27 0.1.0: 146 | 147 | 增加延时消息; 148 | 149 | 增加周期消息; 150 | 151 | 增加指定接收方消息; 152 | 153 | 增加 wechat_sender 控制命令; 154 | 155 | 增加 wxpy 状态监测功能; 156 | 157 | 优化代码; 158 | 159 | 2017.05.17 0.0.2: 160 | 161 | 优化代码 162 | 163 | 2017.05.11 0.0.1: 164 | 165 | 发布初版 166 | 167 | 168 | [1]:https://github.com/youfou/wxpy 169 | [2]:https://github.com/tornadoweb/tornado 170 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Wechat\_Sender 2 | ============== 3 | 4 | 随时随地发送消息到微信 5 | 6 | 简介 7 | ---- 8 | 9 | wechat\_sender 是基于 `wxpy`_ 和 `tornado`_ 10 | 实现的一个可以将你的网站、爬虫、脚本等其他应用中各种消息 11 | (日志、报警、运行结果等) 发送到微信的工具 12 | 13 | 初衷 14 | ---- 15 | 16 | wxpy 基于 itchat 提供了较为完备的微信个人号 API 17 | ,而我想使用个人微信来接收我的网站的报警信息以及一些爬虫的结果,因此我写了这个工具。 18 | 19 | 安装 20 | ---- 21 | 22 | .. code:: python 23 | 24 | pip install wechat_sender 25 | 26 | 运行环境 27 | -------- 28 | 29 | Python 2.7 及以上 30 | Python 3 及以上 31 | 32 | 33 | 使用 34 | ---- 35 | 36 | 1. 登录微信并启动 wechat_sender 服务. 37 | 38 | .. code:: python 39 | 40 | from wxpy import * 41 | from wechat_sender import * 42 | bot = Bot() 43 | listen(bot) 44 | # 之后 wechat_sender 将持续运行等待接收外部消息 45 | 46 | 2. 在外部向微信发送消息. 47 | 48 | .. code:: python 49 | 50 | from wechat_sender import Sender 51 | Sender().send('Hello From Wechat Sender') 52 | # Hello From Wechat Sender 这条消息将通过 1 中登录微信的文件助手发送给你 53 | 54 | 55 | 如果你是 wxpy 的使用者,只需更改一句即可使用 wechat\_sender: 56 | 57 | 例如这是你本来的代码: 58 | 59 | .. code:: python 60 | 61 | # coding: utf-8 62 | from __future__ import unicode_literals 63 | 64 | from wxpy import * 65 | bot = Bot('bot.pkl') 66 | 67 | my_friend = bot.friends().search('xxx')[0] 68 | 69 | my_friend.send('Hello WeChat!') 70 | 71 | @bot.register(Friend) 72 | def reply_test(msg): 73 | msg.reply('test') 74 | 75 | bot.join() 76 | 77 | 使用 wechat\_sender: 78 | 79 | .. code:: python 80 | 81 | # coding: utf-8 82 | from __future__ import unicode_literals 83 | 84 | from wxpy import * 85 | from wechat_sender import listen 86 | bot = Bot('bot.pkl') 87 | 88 | my_friend = bot.friends().search('xxx')[0] 89 | 90 | my_friend.send('Hello WeChat!') 91 | 92 | @bot.register(Friend) 93 | def reply_test(msg): 94 | msg.reply('test') 95 | 96 | listen(bot) # 只需改变最后一行代码 97 | 98 | 之后如果你想在其他程序或脚本中向微信发消息,只需要: 99 | 100 | .. code:: python 101 | 102 | # coding: utf-8 103 | from wechat_sender import Sender 104 | Sender().send("Hello From Wechat Sender") 105 | 106 | 107 | 交流 108 | ---- 109 | 110 | **扫描二维码,验证信息输入 ‘wechat\_sender’ 或 ‘加群’ 进入微信交流群** 111 | 112 | |screenshot| 113 | 114 | .. _wxpy: https://github.com/youfou/wxpy 115 | .. _tornado: https://github.com/tornadoweb/tornado 116 | 117 | .. |screenshot| image:: https://raw.githubusercontent.com/bluedazzle/wechat_sender/master/qr.jpeg 118 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = wechat_sender 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=wechat_sender 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | 底层 API 2 | ============== 3 | 4 | | wechat_sender 是基于 http 提供的服务,因此底层 API 也是基于 http 服务构建的。 5 | | 所以非 Python 项目也可以使用 wechat_sender 进行信息发送,你只需要自己封装一个简单的 sender 即可 6 | 7 | :func:`listen` 中设定的 port 与 wechat_sender 部署地址即为服务地址,默认: http://localhost:10245 8 | 9 | ADDR = http://localhost:10245 10 | 11 | 返回码说明 12 | ------------- 13 | 14 | +----------------+---------------------------+ 15 | | status结果码 | 状态 | 16 | +================+===========================+ 17 | | 0 | 成功 | 18 | +----------------+---------------------------+ 19 | | 1 | 权限不足(token 不正确) | 20 | +----------------+---------------------------+ 21 | | 2 | bot 故障 | 22 | +----------------+---------------------------+ 23 | | 3 | wechat\_sender 服务故障 | 24 | +----------------+---------------------------+ 25 | | 4 | 未知错误 | 26 | +----------------+---------------------------+ 27 | 28 | 发送普通消息 29 | -------------- 30 | 31 | POST ADDR/message 32 | 33 | 参数 34 | ~~~~~~~~~~~~~~ 35 | 36 | :content: (必填|str) - 需要发送的消息 37 | :token: (选填|str) - 令牌 38 | :receiver: (选填|str) - 接收者名称 39 | 40 | 返回 41 | ~~~~~~~~~~~ 42 | 43 | .. code:: 44 | 45 | { 46 | "status": 1, 47 | "message": "Token is missing" 48 | } 49 | or 50 | { 51 | "status": 0, 52 | "message": "Success" 53 | } 54 | 55 | 发送延时消息 56 | -------------- 57 | 58 | POST ADDR/delay_message 59 | 60 | 参数 61 | ~~~~~~~~~~~~~~ 62 | 63 | :content: (必填|str) - 需要发送的消息 64 | :title: (选填|str) - 消息标题 65 | :time: (选填|str) - 消息时间, "XXXX-XX-XX XX:XX:XX" 形式 66 | :remind: (选填|int) - 提醒时移,integer 表示的秒 67 | :token: (选填|str) - 令牌 68 | :receiver: (选填|str) - 接收者名称 69 | 70 | 返回 71 | ~~~~~~~~~~~ 72 | 73 | .. code:: 74 | 75 | { 76 | "status": 1, 77 | "message": "Token is missing" 78 | } 79 | or 80 | { 81 | "status": 0, 82 | "message": "Success" 83 | } 84 | 85 | 发送周期消息 86 | -------------- 87 | 88 | POST ADDR/periodic_message 89 | 90 | 参数 91 | ~~~~~~~~~~~~~~ 92 | 93 | :content: (必填|str) - 需要发送的消息 94 | :title: (选填|str) - 消息标题 95 | :interval: (选填|int) - 提醒周期,integer 表示的秒 96 | :token: (选填|str) - 令牌 97 | :receiver: (选填|str) - 接收者名称 98 | 99 | 返回 100 | ~~~~~~~~~~~ 101 | 102 | .. code:: 103 | 104 | { 105 | "status": 1, 106 | "message": "Token is missing" 107 | } 108 | or 109 | { 110 | "status": 0, 111 | "message": "Success" 112 | } 113 | 114 | 115 | 发送定向消息 116 | -------------- 117 | 118 | POST ADDR/send_to_message 119 | 120 | 参数 121 | ~~~~~~~~~~~~~~ 122 | 123 | :content: (必填|str) - 需要发送的消息 124 | :search: (选填|str) - 好友搜索条件 125 | 126 | 返回 127 | ~~~~~~~~~~~~~~ 128 | 129 | .. code:: 130 | 131 | { 132 | "status": 1, 133 | "message": "Token is missing" 134 | } 135 | or 136 | { 137 | "status": 0, 138 | "message": "Success" 139 | } 140 | -------------------------------------------------------------------------------- /docs/source/command.rst: -------------------------------------------------------------------------------- 1 | Wechat Sender 的控制命令 2 | =============================== 3 | 4 | 我们向 wechat_sender 发送支持的命令来获取 wechat_sender 的信息 5 | 6 | .. note:: 7 | 8 | wechat_sender 只会响应 default_recevier 的命令 9 | 10 | .. note:: 11 | 12 | | 关于 default_recevier: 13 | | 当 :func:`listen` 传入 receivers 时会把第一个 receiver 当做 default_recevier 14 | | 所有未指定接收者的 :class:`Sender` 都将把消息发给默认接收者 15 | 16 | 获取运行状态信息 17 | --------------------- 18 | 19 | @wss 20 | ^^^^^^^^^^^ 21 | 22 | 使用 listen 中绑定的 default_receiver 向 wecaht_sender 发送 @wss 即可返回当前运行状态:: 23 | 24 | #[当前时间] 09:22:41 25 | #[运行时间] 1 day, 13:33:47 26 | #[内存占用] 28.00 MB 27 | #[发送消息] 67 28 | 29 | 30 | 获取已注册的延时\周期消息 31 | ------------------------------- 32 | 33 | @wsr 34 | ^^^^^^^^^^ 35 | 36 | 使用 listen 中绑定的 default_receiver 向 wecaht_sender 发送 @wsr 即可返回已注册的延时\周期消息:: 37 | 38 | #当前已注册延时消息共有1条 39 | #[ID (序号) ]:D0 40 | #[消息接收]:rapospectre 41 | #[发送时间]:2017-06-07 11:57:16 42 | #[消息时间]:2017-06-07 12:56:16 43 | #[消息标题]:延迟消息测试 44 | 45 | #当前已注册周期消息共有0条 46 | 47 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | sys.path.insert(0, os.path.abspath('../../')) 5 | import wechat_sender 6 | 7 | # wechat_sender documentation build configuration file, created by 8 | # sphinx-quickstart on Tue Jun 6 19:03:28 2017. 9 | # 10 | # This file is execfile()d with the current directory set to its 11 | # containing dir. 12 | # 13 | # Note that not all possible configuration values are present in this 14 | # autogenerated file. 15 | # 16 | # All configuration values have a default; values that are commented out 17 | # serve to show the default. 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # 23 | # import os 24 | # import sys 25 | # sys.path.insert(0, os.path.abspath('.')) 26 | 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = ['sphinx.ext.autodoc', 38 | 'sphinx.ext.viewcode'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'wechat_sender' 54 | copyright = u'2017, rapospectre' 55 | author = u'rapospectre' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = u'0.1.3' 63 | # The full version, including alpha/beta/rc tags. 64 | release = u'0.1.3' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = 'zh_CN' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = [] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = False 83 | 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | html_theme = 'sphinx_rtd_theme' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | 104 | # -- Options for HTMLHelp output ------------------------------------------ 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'wechat_senderdoc' 108 | 109 | 110 | # -- Options for LaTeX output --------------------------------------------- 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'wechat_sender.tex', u'wechat\\_sender Documentation', 135 | u'rapospectre', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output --------------------------------------- 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'wechat_sender', u'wechat_sender Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'wechat_sender', u'wechat_sender Documentation', 156 | author, 'wechat_sender', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. wechat_sender documentation master file, created by 2 | sphinx-quickstart on Tue Jun 6 19:03:28 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Wechat\_Sender 7 | ============== 8 | 9 | 随时随地发送消息到微信 10 | 11 | 简介 12 | ---- 13 | 14 | wechat\_sender 是基于 `wxpy`_ 和 `tornado`_ 15 | 实现的一个可以将你的网站、爬虫、脚本等其他应用中各种消息 16 | (日志、报警、运行结果等) 发送到微信的工具 17 | 18 | 初衷 19 | ---- 20 | 21 | wxpy 基于 itchat 提供了较为完备的微信个人号 API 22 | ,而我想使用个人微信来接收我的网站的报警信息以及一些爬虫的结果,因此我写了这个工具。 23 | 24 | 安装 25 | ---- 26 | 27 | .. code:: python 28 | 29 | pip install wechat_sender 30 | 31 | 运行环境 32 | -------- 33 | 34 | Python 2.7 及以上 35 | Python 3 及以上 36 | 37 | 38 | 使用 39 | ---- 40 | 41 | 1. 登录微信并启动 wechat_sender 服务. 42 | 43 | .. code:: python 44 | 45 | from wxpy import * 46 | from wechat_sender import * 47 | bot = Bot() 48 | listen(bot) 49 | # 之后 wechat_sender 将持续运行等待接收外部消息 50 | 51 | 2. 在外部向微信发送消息. 52 | 53 | .. code:: python 54 | 55 | from wechat_sender import Sender 56 | Sender().send('Hello From Wechat Sender') 57 | # Hello From Wechat Sender 这条消息将通过 1 中登录微信的文件助手发送给你 58 | 59 | 60 | 如果你是 wxpy 的使用者,只需更改一句即可使用 wechat\_sender: 61 | 62 | 例如这是你本来的代码: 63 | 64 | .. code:: python 65 | 66 | # coding: utf-8 67 | from __future__ import unicode_literals 68 | 69 | from wxpy import * 70 | bot = Bot('bot.pkl') 71 | 72 | my_friend = bot.friends().search('xxx')[0] 73 | 74 | my_friend.send('Hello WeChat!') 75 | 76 | @bot.register(Friend) 77 | def reply_test(msg): 78 | msg.reply('test') 79 | 80 | bot.join() 81 | 82 | 使用 wechat\_sender: 83 | 84 | .. code:: python 85 | 86 | # coding: utf-8 87 | from __future__ import unicode_literals 88 | 89 | from wxpy import * 90 | from wechat_sender import listen 91 | bot = Bot('bot.pkl') 92 | 93 | my_friend = bot.friends().search('xxx')[0] 94 | 95 | my_friend.send('Hello WeChat!') 96 | 97 | @bot.register(Friend) 98 | def reply_test(msg): 99 | msg.reply('test') 100 | 101 | listen(bot) # 只需改变最后一行代码 102 | 103 | 之后如果你想在其他程序或脚本中向微信发消息,只需要: 104 | 105 | .. code:: python 106 | 107 | # coding: utf-8 108 | from wechat_sender import Sender 109 | Sender().send("Hello From Wechat Sender") 110 | 111 | 112 | 交流 113 | ---- 114 | 115 | **扫描二维码,验证信息输入 ‘wechat\_sender’ 或 ‘加群’ 进入微信交流群** 116 | 117 | |screenshot| 118 | 119 | .. _wxpy: https://github.com/youfou/wxpy 120 | .. _tornado: https://github.com/tornadoweb/tornado 121 | 122 | .. |screenshot| image:: https://raw.githubusercontent.com/bluedazzle/wechat_sender/master/qr.jpeg 123 | 124 | .. toctree:: 125 | :maxdepth: 2 126 | :caption: 目录: 127 | 128 | listen 129 | sender 130 | command 131 | object 132 | practice 133 | api 134 | -------------------------------------------------------------------------------- /docs/source/listen.rst: -------------------------------------------------------------------------------- 1 | listen 方法 2 | ============= 3 | 4 | .. module:: wechat_sender 5 | 6 | listen() 方法用于监听 wxpy 的 bot 对象实例并启动 wechat_sender 服务,为外部发送消息到微信提供支持。 7 | 8 | .. autofunction:: listen 9 | 10 | .. tip:: 11 | 12 | | 专门申请一个微信号负责发送应用消息及日志信息 13 | | 避免使用自己的个人微信造成不便 14 | | 可以把接收者指定为个人微信 15 | 16 | 指定接收者 17 | -------------- 18 | 19 | .. code:: python 20 | 21 | from wxpy import * 22 | from wechat_sender import listen 23 | 24 | # 这里登录单独申请的微信号 25 | bot = Bot() 26 | # 这里查询你的个人微信, search 填入你的微信昵称 27 | my = bot.friends().search('your name')[0] 28 | # 传入你的私人微信作为接收者 29 | listen(bot, receivers=my) 30 | 31 | 向你的私人微信发送消息:: 32 | 33 | from wechat_sender import Sender 34 | Sender().send('hello') 35 | 36 | 指定多个接收者 37 | --------------------- 38 | 39 | .. code:: python 40 | 41 | from wxpy import * 42 | from wechat_sender import listen 43 | 44 | # 这里登录单独申请的微信号 45 | bot = Bot() 46 | # 这里查询你的个人微信, search 填入你的微信昵称 47 | my = bot.friends().search('your name')[0] 48 | group = bot.groups().search('group name')[0] 49 | # 传入接收者列表 50 | listen(bot, receivers=[my, group]) 51 | 52 | 向 group 发送消息:: 53 | 54 | from wechat_sender import Sender 55 | Sender('group name').send('hello') 56 | 57 | .. note:: 58 | 59 | | 关于接收者: 60 | | 当 :func:`listen` 传入 receivers 时会把第一个 receiver 当做默认接收者,所有未指定接收者的 :class:`Sender` 都将把消息发给默认接收者 61 | 62 | 63 | 使用 token 以防 sender 被滥用 64 | ------------------------------------------ 65 | 66 | .. warning:: 67 | 68 | wechat_sender 基于 http 服务提供消息发送服务,如果部署在服务器上有潜在盗用风险,所以 :func:`listen` 初始化时务必传入 token 以防止盗用 69 | 70 | 71 | .. warning:: 72 | 73 | 注意保证 token 安全,不要被泄漏 74 | 75 | .. code:: python 76 | 77 | # 同样,基于 http 的服务需要一个端口与外部通信,listen 默认端口是 10245 ,你可以改成任何空闲端口,例如 8888 78 | listen(bot, receiver, token='your secret', port=8888) 79 | 80 | .. note:: 81 | 82 | 如果传入了 token 或 port 请务必保证 :class:`Sender` 在初始化时也传入相同的 token 和 port 83 | 84 | 85 | 开启 wechat_sender 的状态报告 86 | ------------------------------------------ 87 | 88 | 鉴于微信个人号接口的不稳定性,我们可以开启 wechat_sender 的状态报告,定时向 status_receiver 发送状态信息 89 | 90 | .. code:: python 91 | 92 | listen(bot, my, token='your secret', status_report=True, status_receiver=my) 93 | 94 | .. note:: 95 | 96 | | 不指定 status_receiver 时状态报告将发送到默认接收者 97 | | 默认每隔一小时进行一次状态报告 98 | -------------------------------------------------------------------------------- /docs/source/object.rst: -------------------------------------------------------------------------------- 1 | Wechat Sender 中的基本类 2 | ======================== 3 | 4 | .. module:: wechat_sender.objects 5 | 6 | 7 | .. autoclass:: WxBot 8 | 9 | .. automethod:: WxBot.__init__ 10 | 11 | .. autoclass:: Message 12 | 13 | .. automethod:: Message.__init__ 14 | 15 | .. autoclass:: Global -------------------------------------------------------------------------------- /docs/source/practice.rst: -------------------------------------------------------------------------------- 1 | 最佳实践 2 | =========== 3 | 4 | 在服务器 XXX.XXX.XXX.XXX 部署 wechat_sender 服务:: 5 | 6 | from wxpy import * 7 | from wechat_sender import * 8 | 9 | bot = Bot('bot.pkl', console_qr=True) 10 | bot.enable_puid() 11 | master = ensure_one(bot.friends().search(puid='xxxx')) 12 | log_group = ensure_one(bot.groups().search(puid='xxxxxx')) 13 | other = ensure_one(bot.friends().search('xxxxxx')) 14 | token = 'xxxxxxxxxxxxxxxxxxxxx' 15 | listen(bot, [master, other, log_group], token=token, port=9090, status_report=True, status_receiver=log_group) 16 | 17 | 18 | 在其他地方进行消息发送:: 19 | 20 | from wechat_sender import Sender 21 | host = 'XXX.XXX.XXX.XXX' 22 | token = 'xxxxxxxxxxxxxxxxxxxxx' 23 | sender = Sender(token=token, receiver='xxx', host=host, port='9090') 24 | 25 | 26 | 在其他应用中加入 wechat_sender logging handler:: 27 | 28 | # 假如这是你另一台服务器上的脚本 29 | # spider.py 30 | 31 | import logging 32 | from wechat_sender import LoggingSenderHandler 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | # spider code here 37 | def test_spider(): 38 | ... 39 | logger.exception("EXCEPTION: XXX") 40 | 41 | def init_logger(): 42 | token = 'xxxxxxxxxxxxxxxxxxxxx' 43 | sender_logger = LoggingSenderHandler('spider', token=token, port=9090, host='XXX.XXX.XXX.XXX', receiver='xxx', level=logging.EXCEPTION) 44 | logger.addHandler(sender_logger) 45 | 46 | if __name__ == '__main__': 47 | init_logger() 48 | test_spider() 49 | 50 | 只需要在原有 logger 中加入 wechat_sender 的 log handler 即可实现把日志发送到微信 -------------------------------------------------------------------------------- /docs/source/sender.rst: -------------------------------------------------------------------------------- 1 | Sender 发送者对象 2 | ================= 3 | 4 | .. module:: wechat_sender 5 | 6 | Sender 对象可以理解为在外部程序中( 非 wechat_sender 服务,例如你的个人脚本、网站等 )向微信发送消息的发送者 7 | 8 | .. autoclass:: Sender 9 | 10 | .. automethod:: Sender.__init__ 11 | 12 | 指定多个发送者 13 | 14 | Sender 的 receiver 可以指定多个发送者,由这个 Sender 发送的消息默认会广播给多个发送者:: 15 | 16 | # coding: utf-8 17 | import datetime 18 | from wechat_sender import Sender 19 | 20 | sender = Sender(token='xxx', receivers='aaa,bbb,ccc,ddd') 21 | sender.send('broadcast message') 22 | 23 | .. note:: 24 | 25 | 使用英文半角逗号分隔多个接收者 26 | 27 | .. automethod:: Sender.send 28 | 29 | .. automethod:: Sender.delay_send 30 | 31 | 发送延时消息 32 | 33 | 某些情况下,我们希望消息可以延迟发送,例如日程、会议提醒等,这时用 :func:`Sender.delay_send` 即可满足需求:: 34 | 35 | # coding: utf-8 36 | import datetime 37 | from wechat_sender import Sender 38 | 39 | sender = Sender() 40 | time = datetime.datetime.now()+datetime.timedelta(hours=1) 41 | sender.delay_send(content="测试内容", time=time, title="测试标题", remind=datetime.timedelta(minutes=59)) 42 | 43 | 如果返回正常,1 分钟后你将收到这条消息时间是 1 小时后的消息提醒:: 44 | 45 | #标题:测试标题 46 | #时间:2017-06-07 12:56:16 47 | #内容:延迟消息测试 48 | 49 | 50 | .. automethod:: Sender.periodic_send 51 | 52 | 发送周期消息 53 | 54 | 如果希望某条消息周期性发送到微信,可以使用 :func:`Sender.periodic_send`:: 55 | 56 | # coding: utf-8 57 | import datetime 58 | from wechat_sender import Sender 59 | 60 | sender = Sender() 61 | interval = datetime.timedelta(seconds=10) 62 | sender.periodic_send(content='测试消息', interval=interval, title='测试标题') 63 | 64 | 如果返回正常,每隔 10 s 你将收到一条消息如下:: 65 | 66 | # 标题:测试标题 67 | # 内容:周期消息测试test 68 | 69 | .. tip:: 70 | 71 | 使用 :doc:`command` 查看已注册的延时\周期消息 72 | 73 | .. automethod:: Sender.send_to 74 | 75 | 发送定向消息 76 | 77 | 如果你希望某条消息发送给指定的微信好友,你可以使用 :func:`Sender.send_to`:: 78 | 79 | # coding: utf-8 80 | import datetime 81 | from wechat_sender import Sender 82 | 83 | sender = Sender() 84 | sender.send_to('Hello From Wechat Sender', '微信好友昵称') 85 | 86 | 如果返回正常,你指定的微信好友将收到这条消息 87 | 88 | .. tip:: 89 | 90 | | :func:`Sender.send_to` 的 search 参数使用方法和 wxpy 的 `wxpy.chats().search() `_. 一致 91 | | 直接搜索昵称或用综合查询条件均可以搜索好友或群 92 | 93 | 使用多条件查询好友:: 94 | 95 | # coding: utf-8 96 | import datetime 97 | from wechat_sender import Sender 98 | 99 | sender = Sender() 100 | sender.send_to('Hello From Wechat Sender', search={'city': 'xx', 'nick_name': 'xxx'}) 101 | 102 | 103 | 104 | Sender 日志对象 105 | ================= 106 | 107 | Sender 日志对象可以更平滑的接入外部应用的 log 系统中,基本无需更改代码即可使应用日志发送到微信 108 | 109 | .. autoclass:: LoggingSenderHandler 110 | 111 | .. automethod:: LoggingSenderHandler.__init__ -------------------------------------------------------------------------------- /qr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluedazzle/wechat_sender/21d861735509153d6b34408157911c25a5d7018b/qr.jpeg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | wxpy 3 | psutil -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # from __future__ import unicode_literals 3 | import codecs 4 | import os 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | 12 | def read(fname): 13 | return codecs.open(os.path.join(os.path.dirname(__file__), fname), encoding='utf-8').read() 14 | 15 | 16 | NAME = "wechat_sender" 17 | 18 | PACKAGES = ["wechat_sender", ] 19 | 20 | DESCRIPTION = "随时随地发送消息到微信" 21 | 22 | LONG_DESCRIPTION = read("README.rst") 23 | 24 | KEYWORDS = ["Wechat", "微信", "监控"] 25 | 26 | AUTHOR = "RaPoSpectre" 27 | 28 | AUTHOR_EMAIL = "rapospectre@gmail.com" 29 | 30 | URL = "https://github.com/bluedazzle/wechat_sender" 31 | 32 | VERSION = "0.1.4" 33 | 34 | LICENSE = "BSD" 35 | 36 | setup( 37 | name=NAME, 38 | version=VERSION, 39 | description=DESCRIPTION, 40 | long_description=LONG_DESCRIPTION, 41 | classifiers=[ 42 | 'License :: OSI Approved :: BSD License', 43 | 'Programming Language :: Python', 44 | 'Operating System :: OS Independent', 45 | 'Topic :: Communications :: Chat', 46 | 'Topic :: Utilities', 47 | 'Programming Language :: Python :: 2.7', 48 | 'Programming Language :: Python :: 3.6' 49 | ], 50 | install_requires=[ 51 | 'tornado', 52 | 'wxpy', 53 | 'psutil', 54 | ], 55 | 56 | keywords=KEYWORDS, 57 | author=AUTHOR, 58 | author_email=AUTHOR_EMAIL, 59 | url=URL, 60 | license=LICENSE, 61 | packages=PACKAGES, 62 | include_package_data=True, 63 | zip_safe=True, 64 | ) 65 | -------------------------------------------------------------------------------- /wechat_sender/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | 5 | from wechat_sender.listener import listen 6 | from wechat_sender.sender import Sender, LoggingSenderHandler 7 | from wechat_sender.compatible import PY2, SYS_ENCODE 8 | 9 | __author__ = 'rapospectre' -------------------------------------------------------------------------------- /wechat_sender/compatible.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | import sys 4 | 5 | PY_VERSION = sys.version 6 | PY2 = PY_VERSION < '3' 7 | SYS_ENCODE = sys.stdout.encoding or 'utf-8' 8 | -------------------------------------------------------------------------------- /wechat_sender/listener.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | 5 | import copy 6 | import functools 7 | import json 8 | import os 9 | import time 10 | import datetime 11 | import psutil 12 | import logging 13 | 14 | import sys 15 | import tornado.web 16 | 17 | from wechat_sender.objects import WxBot, Message, Global 18 | from wechat_sender.utils import StatusWrapperMixin, STATUS_BOT_EXCEPTION, STATUS_PERMISSION_DENIED, \ 19 | STATUS_TORNADO_EXCEPTION, DEFAULT_REMIND_TIME, STATUS_ERROR, DEFAULT_REPORT_TIME, DELAY_TASK, PERIODIC_TASK, \ 20 | MESSAGE_REPORT_COMMAND, SYSTEM_TASK, MESSAGE_STATUS_COMMAND 21 | 22 | glb = None 23 | _logger = logging.getLogger(__name__) 24 | 25 | 26 | class Application(tornado.web.Application): 27 | """ 28 | tornado app 初始化 29 | """ 30 | 31 | def __init__(self): 32 | handlers = [ 33 | (r"/message", MessageHandle), 34 | (r"/delay_message", DelayMessageHandle), 35 | (r"/periodic_message", PeriodicMessageHandle), 36 | (r"/send_to_message", UserMessageHandle), 37 | ] 38 | settings = dict( 39 | static_path=os.path.join(os.path.dirname(__file__), "static"), 40 | ) 41 | super(Application, self).__init__(handlers, **settings) 42 | 43 | 44 | class MessageHandle(StatusWrapperMixin, tornado.web.RequestHandler): 45 | """ 46 | 普通消息处理 handle 47 | """ 48 | 49 | def post(self, *args, **kwargs): 50 | message = self.get_argument('content', None) 51 | token = self.get_argument('token', None) 52 | receivers = self.get_argument('receivers', None) 53 | if not message: 54 | self.status_code = STATUS_ERROR 55 | self.write('Content is required') 56 | return 57 | 58 | if glb.token: 59 | if glb.token != token: 60 | self.status_code = STATUS_PERMISSION_DENIED 61 | self.write('Token is missing') 62 | return 63 | try: 64 | msg = Message(message, receivers=receivers) 65 | glb.wxbot.send_msg(msg) 66 | self.write('Success') 67 | except Exception as e: 68 | _logger.exception(e) 69 | self.status_code = STATUS_BOT_EXCEPTION 70 | self.write(e.message) 71 | 72 | 73 | class DelayMessageHandle(StatusWrapperMixin, tornado.web.RequestHandler): 74 | """ 75 | 延时消息处理 handle 76 | """ 77 | 78 | def __init__(self, application, request, *args, **kwargs): 79 | self.ioloop = tornado.ioloop.IOLoop.instance() 80 | super(DelayMessageHandle, self).__init__(application, request, *args, **kwargs) 81 | 82 | def post(self, *args, **kwargs): 83 | content = self.get_argument('content', '') 84 | title = self.get_argument('title', '') 85 | task_time = self.get_argument('time', None) 86 | remind = int(self.get_argument('remind', DEFAULT_REMIND_TIME)) 87 | token = self.get_argument('token', None) 88 | receivers = self.get_argument('receivers', None) 89 | 90 | if glb.token: 91 | if glb.token != token: 92 | self.status_code = STATUS_PERMISSION_DENIED 93 | self.write('Token is missing') 94 | return 95 | if task_time: 96 | try: 97 | task_time = datetime.datetime.strptime(task_time, '%Y-%m-%d %H:%M:%S') 98 | timestamp = time.mktime( 99 | (task_time - datetime.timedelta( 100 | seconds=remind)).timetuple()) 101 | except ValueError as e: 102 | self.status_code = STATUS_ERROR 103 | self.write(e.message) 104 | _logger.exception(e) 105 | return 106 | else: 107 | task_time = datetime.datetime.now() 108 | timestamp = int(time.mktime(task_time.timetuple())) 109 | try: 110 | message = Message(content, title, task_time, datetime.timedelta(seconds=remind), receivers=receivers) 111 | self.ioloop.call_at(timestamp, self.delay_task, DELAY_TASK, message) 112 | self.write('Success') 113 | except Exception as e: 114 | self.status_code = STATUS_TORNADO_EXCEPTION 115 | self.write(e.message) 116 | _logger.exception(e) 117 | 118 | @staticmethod 119 | def delay_task(task_type, message): 120 | # try: 121 | glb.wxbot.send_msg(message) 122 | _logger.info( 123 | '{0} Send delay message {1} at {2:%Y-%m-%d %H:%M:%S}'.format(task_type, message, datetime.datetime.now())) 124 | # except Exception as e: 125 | 126 | 127 | class PeriodicMessageHandle(StatusWrapperMixin, tornado.web.RequestHandler): 128 | """ 129 | 周期消息处理 handle 130 | """ 131 | 132 | def __init__(self, application, request, *args, **kwargs): 133 | self.ioloop = tornado.ioloop.IOLoop.instance() 134 | super(PeriodicMessageHandle, self).__init__(application, request, *args, **kwargs) 135 | 136 | def post(self, *args, **kwargs): 137 | content = self.get_argument('content', '') 138 | title = self.get_argument('title', '') 139 | interval = self.get_argument('interval', None) 140 | token = self.get_argument('token', None) 141 | receivers = self.get_argument('receivers', None) 142 | 143 | if glb.token: 144 | if glb.token != token: 145 | self.status_code = STATUS_PERMISSION_DENIED 146 | self.write('Token is missing') 147 | return 148 | if not interval: 149 | self.status_code = STATUS_ERROR 150 | self.write('interval is required') 151 | return 152 | try: 153 | interval = int(interval) 154 | except Exception as e: 155 | self.status_code = STATUS_ERROR 156 | self.write('interval must be a integer') 157 | try: 158 | message = Message(content, title=title, interval=datetime.timedelta(seconds=interval), receivers=receivers) 159 | user_periodic = tornado.ioloop.PeriodicCallback( 160 | functools.partial(self.periodic_task, PERIODIC_TASK, message), 161 | interval * 1000, self.ioloop) 162 | glb.periodic_list.append(user_periodic) 163 | user_periodic.start() 164 | self.write('Success') 165 | except Exception as e: 166 | self.status_code = STATUS_TORNADO_EXCEPTION 167 | self.write(e.message) 168 | _logger.exception(e) 169 | 170 | @staticmethod 171 | def periodic_task(task_type, message): 172 | glb.wxbot.send_msg(message) 173 | _logger.info('{0} Send periodic message {1} at {2:%Y-%m-%d %H:%M:%S}'.format(task_type, message, 174 | datetime.datetime.now())) 175 | 176 | 177 | class UserMessageHandle(StatusWrapperMixin, tornado.web.RequestHandler): 178 | """ 179 | 指定消息接收处理 handle 180 | """ 181 | 182 | def post(self, *args, **kwargs): 183 | from wxpy import ensure_one 184 | 185 | content = self.get_argument('content', '') 186 | search = self.get_argument('search', '') 187 | token = self.get_argument('token', None) 188 | default_receiver = self.get_argument('receivers', None) 189 | 190 | if glb.token: 191 | if glb.token != token: 192 | self.status_code = STATUS_PERMISSION_DENIED 193 | self.write('Token is missing') 194 | return 195 | try: 196 | search = json.loads(search) 197 | except ValueError: 198 | search = search 199 | try: 200 | if isinstance(search, dict): 201 | receiver = ensure_one(glb.wxbot.bot.search(**search)) 202 | else: 203 | receiver = ensure_one(glb.wxbot.bot.search(search)) 204 | except ValueError: 205 | receiver = None 206 | if receiver: 207 | receiver.send_msg(content) 208 | else: 209 | msg = '消息发送失败,没有找到接收者。\n[搜索条件]: {0}\n[消息内容]:{1}'.format(search, content) 210 | message = Message(msg, receivers=default_receiver) 211 | glb.wxbot.send_msg(message) 212 | _logger.info(msg) 213 | self.write('Success') 214 | 215 | 216 | def generate_run_info(): 217 | """ 218 | 获取当前运行状态 219 | """ 220 | uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp(glb.run_info.create_time()) 221 | memory_usage = glb.run_info.memory_info().rss 222 | msg = '[当前时间] {now:%H:%M:%S}\n[运行时间] {uptime}\n[内存占用] {memory}\n[发送消息] {messages}'.format( 223 | now=datetime.datetime.now(), 224 | uptime=str(uptime).split('.')[0], 225 | memory='{:.2f} MB'.format(memory_usage / 1024 ** 2), 226 | messages=len(glb.wxbot.bot.messages) 227 | ) 228 | return msg 229 | 230 | 231 | def check_bot(task_type=SYSTEM_TASK): 232 | """ 233 | wxpy bot 健康检查任务 234 | """ 235 | if glb.wxbot.bot.alive: 236 | msg = generate_run_info() 237 | message = Message(content=msg, receivers='status') 238 | glb.wxbot.send_msg(message) 239 | _logger.info( 240 | '{0} Send status message {1} at {2:%Y-%m-%d %H:%M:%S}'.format(task_type, msg, datetime.datetime.now())) 241 | else: 242 | # todo 243 | pass 244 | 245 | 246 | def timeout_message_report(): 247 | """ 248 | 周期/延时 消息报告 249 | """ 250 | timeout_list = glb.ioloop._timeouts 251 | delay_task = [] 252 | for timeout in timeout_list: 253 | if not timeout.callback: 254 | continue 255 | if len(timeout.callback.args) == 2: 256 | task_type, message = timeout.callback.args 257 | delay_task.append(message) 258 | msg = '当前已注册延时消息共有{0}条'.format(len(delay_task)) 259 | for i, itm in enumerate(delay_task): 260 | msg = '{pre}\n[ID (序号) ]:D{index}\n[消息接收]:{receiver}\n[发送时间]:{remind}\n[消息时间]:{time}\n[消息标题]:{message}\n'.format( 261 | pre=msg, index=i, remind=itm.remind, time=itm.time, message=itm.title or itm.content, receiver=itm.receiver) 262 | interval_task = [(periodic.callback.args[1], periodic.is_running()) for periodic in glb.periodic_list if 263 | len(periodic.callback.args) == 2 and periodic.callback.args[0] == PERIODIC_TASK] 264 | msg = '{0}\n当前已注册周期消息共有{1}条'.format(msg, len(interval_task)) 265 | for i, itm in enumerate(interval_task): 266 | msg = '{pre}\n[ID (序号) ]:P{index}\n[消息接收]:{receiver}\n[运行状态]:{status}\n[发送周期]:{interval}\n[消息标题]:{message}\n'.format( 267 | pre=msg, index=i, interval=itm[0].interval, status='已激活' if itm[1] else '未激活', 268 | message=itm[0].title or itm[0].content, receiver=itm[0].receiver) 269 | return msg 270 | 271 | 272 | def register_listener_handle(wxbot): 273 | """ 274 | wechat_sender 向 wxpy 注册控制消息 handler 275 | """ 276 | from wxpy import TEXT 277 | 278 | @wxbot.bot.register(wxbot.default_receiver, TEXT, except_self=False) 279 | def sender_command_handle(msg): 280 | command_dict = {MESSAGE_REPORT_COMMAND: timeout_message_report(), 281 | MESSAGE_STATUS_COMMAND: generate_run_info()} 282 | message = command_dict.get(msg.text, None) 283 | if message: 284 | return message 285 | myself = wxbot.bot.registered.get_config(msg) 286 | registered_copy = copy.copy(wxbot.bot.registered) 287 | registered_copy.remove(myself) 288 | pre_conf = registered_copy.get_config(msg) 289 | if pre_conf: 290 | my_name = sys._getframe().f_code.co_name 291 | if my_name != pre_conf.func.__name__: 292 | pre_conf.func(msg) 293 | 294 | 295 | def listen(bot, receivers=None, token=None, port=10245, status_report=False, status_receiver=None, 296 | status_interval=DEFAULT_REPORT_TIME): 297 | """ 298 | 传入 bot 实例并启动 wechat_sender 服务 299 | 300 | :param bot: (必填|Bot对象) - wxpy 的 Bot 对象实例 301 | :param receivers: (选填|wxpy.Chat 对象|Chat 对象列表) - 消息接收者,wxpy 的 Chat 对象实例, 或 Chat 对象列表,如果为 list 第一个 Chat 为默认接收者。如果为 Chat 对象,则默认接收者也是此对象。 不填为当前 bot 对象的文件接收者 302 | :param token: (选填|str) - 信令,防止 receiver 被非法滥用,建议加上 token 防止非法使用,如果使用 token 请在初始化 `Sender()` 时也使用统一 token,否则无法发送。token 建议为 32 位及以上的无规律字符串 303 | :param port: (选填|int) - 监听端口, 监听端口默认为 10245 ,如有冲突或特殊需要请自行指定,需要和 `Sender()` 统一 304 | :param status_report: (选填|bool) - 是否开启状态报告,如果开启,wechat_sender 将会定时发送状态信息到 status_receiver 305 | :param status_receiver: (选填|Chat 对象) - 指定 status_receiver,不填将会发送状态消息给默认接收者 306 | :param status_interval: (选填|int|datetime.timedelta) - 指定状态报告发送间隔时间,为 integer 时代表毫秒 307 | 308 | """ 309 | global glb 310 | periodic_list = [] 311 | app = Application() 312 | wxbot = WxBot(bot, receivers, status_receiver) 313 | register_listener_handle(wxbot) 314 | process = psutil.Process() 315 | app.listen(port) 316 | 317 | if status_report: 318 | if isinstance(status_interval, datetime.timedelta): 319 | status_interval = status_interval.seconds * 1000 320 | check_periodic = tornado.ioloop.PeriodicCallback(functools.partial(check_bot, SYSTEM_TASK), status_interval) 321 | check_periodic.start() 322 | periodic_list.append(check_periodic) 323 | 324 | glb = Global(wxbot=wxbot, run_info=process, periodic_list=periodic_list, ioloop=tornado.ioloop.IOLoop.instance(), 325 | token=token) 326 | tornado.ioloop.IOLoop.current().start() 327 | -------------------------------------------------------------------------------- /wechat_sender/objects.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import json 5 | 6 | 7 | class WxBot(object): 8 | """ 9 | 储存微信 bot 相关信息及 wechat_sender 各类 receiver 的类 10 | """ 11 | 12 | def __new__(cls, *args, **kwargs): 13 | if not hasattr(cls, '_instance'): 14 | orig = super(WxBot, cls) 15 | cls._instance = orig.__new__(cls) 16 | return cls._instance 17 | 18 | def __init__(self, bot=None, receivers=None, status_receiver=None, *args, **kwargs): 19 | """ 20 | :param bot: wxpy bot 对象实例 21 | :param receivers: wxpy chat 对象实例 22 | :param status_receiver: wxpy chat 对象实例 23 | """ 24 | self.bot = bot 25 | self.receivers = {} 26 | self.default_receiver = None 27 | self.init_receivers(receivers) 28 | self.status_receiver = status_receiver if status_receiver else self.default_receiver 29 | self.receivers['status'] = self.status_receiver 30 | super(WxBot, self).__init__(*args, **kwargs) 31 | 32 | def init_receivers(self, receivers): 33 | """ 34 | 初始化 receivers 35 | """ 36 | if not receivers: 37 | self.default_receiver = self.bot.file_helper 38 | return True 39 | if isinstance(receivers, list): 40 | self.default_receiver = receivers[0] 41 | for receiver in receivers: 42 | if self.bot.puid_map: 43 | self.receivers[receiver.puid] = receiver 44 | self.receivers[receiver.name] = receiver 45 | else: 46 | self.default_receiver = receivers 47 | if self.bot.puid_map: 48 | self.receivers[receivers.puid] = receivers 49 | self.receivers[receivers.name] = receivers 50 | 51 | def send_msg(self, msg): 52 | """ 53 | wxpy 发送文本消息的基本封装,这里会进行消息 receiver 识别分发 54 | """ 55 | for receiver in msg.receivers: 56 | current_receiver = self.receivers.get(receiver, self.default_receiver) 57 | current_receiver.send_msg(msg) 58 | 59 | 60 | class Message(object): 61 | """ 62 | wechat_sender 消息类,是所有 wechat_sender 发送消息的基本类型 63 | """ 64 | 65 | def __init__(self, content, title=None, time=None, remind=None, interval=None, receivers=None): 66 | """ 67 | :param content: 消息内容 68 | :param title: 消息标题 69 | :param time: 消息时间 70 | :param remind: 消息提醒时间 71 | :param interval: 消息提醒间隔 72 | :param receivers: 消息接收者 73 | """ 74 | self.title = title 75 | self.content = content 76 | self.message_time = time 77 | self.remind_time = None 78 | if time and remind: 79 | self.remind_time = time - remind 80 | self.nc = remind 81 | self.message_interval = interval 82 | self.receivers = [itm for itm in receivers.split(',')] if receivers else ['default'] 83 | 84 | @property 85 | def time(self): 86 | """ 87 | :return: 以字符串 "xxxx-xx-xx xx:xx:xx" 的形式返回消息的时间 88 | """ 89 | return self.message_time.strftime('%Y-%m-%d %H:%M:%S') if self.message_time else None 90 | 91 | @property 92 | def interval(self): 93 | """ 94 | :return: 返回消息提醒间隔的秒数 95 | """ 96 | return '{0}s'.format(self.message_interval.seconds) if self.message_interval else None 97 | 98 | @property 99 | def remind(self): 100 | """ 101 | :return: 以字符串 "xxxx-xx-xx xx:xx:xx" 的形式返回消息的提醒时间 102 | """ 103 | return self.remind_time.strftime('%Y-%m-%d %H:%M:%S') if self.message_time else None 104 | 105 | def render_message(self): 106 | """ 107 | 渲染消息 108 | 109 | :return: 渲染后的消息 110 | """ 111 | message = None 112 | if self.title: 113 | message = '标题:{0}'.format(self.title) 114 | if self.message_time: 115 | message = '{0}\n时间:{1}'.format(message, self.time) 116 | if message: 117 | message = '{0}\n内容:{1}'.format(message, self.content) 118 | else: 119 | message = self.content 120 | return message 121 | 122 | def __repr__(self): 123 | return self.render_message() 124 | 125 | 126 | class Global(object): 127 | """ 128 | wechat_sender 的全局对象类 129 | """ 130 | 131 | def __new__(cls, *args, **kwargs): 132 | if not hasattr(cls, '_instance'): 133 | orig = super(Global, cls) 134 | cls._instance = orig.__new__(cls) 135 | return cls._instance 136 | 137 | def __init__(self, *args, **kwargs): 138 | for k, v in kwargs.items(): 139 | setattr(self, k, v) 140 | 141 | def insert(self, name, value): 142 | setattr(self, name, value) 143 | return True 144 | 145 | def __call__(self, *args, **kwargs): 146 | return self.__dict__ 147 | -------------------------------------------------------------------------------- /wechat_sender/sender.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | 5 | import json 6 | 7 | import datetime 8 | import requests 9 | import logging 10 | 11 | from wechat_sender.utils import STATUS_SUCCESS, DEFAULT_REMIND_TIME 12 | from wechat_sender.compatible import PY2, SYS_ENCODE 13 | from functools import reduce 14 | 15 | 16 | class Sender(object): 17 | """ 18 | sender 对象,任何外部程序向微信发送消息都需要初始化 sender 对象:: 19 | 20 | from wechat_sender import Sender 21 | sender = Sender(token='test', receiver='wechat_name,xxx,xxx') 22 | 23 | # 向 receiver 发送消息 24 | sender.send('Hello From Wechat Sender') 25 | 26 | """ 27 | 28 | def __init__(self, token=None, receivers=None, host='http://localhost', port=10245): 29 | """ 30 | :param token: (选填|str) - 信令,如果不为空请保持和 listen 中的 token 一致 31 | :param receivers: (选填|str) - 接收者,wxpy 的 puid 或 微信名、昵称等,多个发送者请使用半角逗号 ',' 分隔。不填将发送至 default_receiver 32 | :param host: (选填|str) - 远程地址,本地调用不用填写 33 | :param port: (选填|int) - 发送端口,默认 10245 端口,如不为空请保持和 listen 中的 port 一致 34 | """ 35 | self.token = token 36 | if isinstance(receivers, list): 37 | self.receivers = ','.join(receivers) 38 | else: 39 | self.receivers = receivers 40 | self.host = host 41 | self.port = port 42 | self.remote = '{0}:{1}/'.format(self.host, self.port) 43 | self.data = {} 44 | self.timeout = 5 45 | 46 | def _wrap_post_data(self, **kwargs): 47 | self.data = kwargs 48 | if self.token: 49 | self.data['token'] = self.token 50 | if self.receivers: 51 | self.data['receivers'] = self.receivers 52 | return self.data 53 | 54 | def _convert_bytes(self, msg): 55 | if not PY2: 56 | if isinstance(msg, bytes): 57 | return str(msg, encoding=SYS_ENCODE) 58 | return msg 59 | 60 | def send(self, message): 61 | """ 62 | 发送基本文字消息 63 | 64 | :param message: (必填|str) - 需要发送的文本消息 65 | :return: * status:发送状态,True 发送成,False 发送失败 66 | * message:发送失败详情 67 | """ 68 | url = '{0}message'.format(self.remote) 69 | data = self._wrap_post_data(content=message) 70 | res = requests.post(url, data=data, timeout=self.timeout) 71 | if res.status_code == requests.codes.ok: 72 | res_data = json.loads(self._convert_bytes(res.content)) 73 | if res_data.get('status') == STATUS_SUCCESS: 74 | return True, res_data.get('message') 75 | return False, res_data.get('message') 76 | res.raise_for_status() 77 | return False, 'Request or Response Error' 78 | 79 | def delay_send(self, content, time, title='', remind=DEFAULT_REMIND_TIME): 80 | """ 81 | 发送延时消息 82 | 83 | :param content: (必填|str) - 需要发送的消息内容 84 | :param time: (必填|str|datetime) - 发送消息的开始时间,支持 datetime.date、datetime.datetime 格式或者如 '2017-05-21 10:00:00' 的字符串 85 | :param title: (选填|str) - 需要发送的消息标题 86 | :param remind: (选填|int|datetime.timedelta) - 消息提醒时移,默认 1 小时,即早于 time 值 1 小时发送消息提醒, 支持 integer(毫秒) 或 datetime.timedelta 87 | :return: * status:发送状态,True 发送成,False 发送失败 88 | * message:发送失败详情 89 | """ 90 | url = '{0}delay_message'.format(self.remote) 91 | if isinstance(time, (datetime.datetime, datetime.date)): 92 | time = time.strftime('%Y-%m-%d %H:%M:%S') 93 | if isinstance(remind, datetime.timedelta): 94 | remind = int(remind.total_seconds()) 95 | if not isinstance(remind, int): 96 | raise ValueError 97 | data = self._wrap_post_data(title=title, content=content, time=time, remind=remind) 98 | res = requests.post(url, data=data, timeout=self.timeout) 99 | if res.status_code == requests.codes.ok: 100 | res_data = json.loads(self._convert_bytes(res.content)) 101 | if res_data.get('status') == STATUS_SUCCESS: 102 | return True, res_data.get('message') 103 | return False, res_data.get('message') 104 | res.raise_for_status() 105 | return False, 'Request or Response Error' 106 | 107 | def periodic_send(self, content, interval, title=''): 108 | """ 109 | 发送周期消息 110 | 111 | :param content: (必填|str) - 需要发送的消息内容 112 | :param interval: (必填|int|datetime.timedelta) - 发送消息间隔时间,支持 datetime.timedelta 或 integer 表示的秒数 113 | :param title: (选填|str) - 需要发送的消息标题 114 | :return: * status:发送状态,True 发送成,False 发送失败 115 | * message:发送失败详情 116 | """ 117 | url = '{0}periodic_message'.format(self.remote) 118 | if isinstance(interval, datetime.timedelta): 119 | interval = int(interval.total_seconds()) 120 | if not isinstance(interval, int): 121 | raise ValueError 122 | data = self._wrap_post_data(title=title, content=content, interval=interval) 123 | res = requests.post(url, data, timeout=self.timeout) 124 | if res.status_code == requests.codes.ok: 125 | res_data = json.loads(self._convert_bytes(res.content)) 126 | if res_data.get('status') == STATUS_SUCCESS: 127 | return True, res_data.get('message') 128 | return False, res_data.get('message') 129 | res.raise_for_status() 130 | return False, 'Request or Response Error' 131 | 132 | def send_to(self, content, search): 133 | """ 134 | 向指定好友发送消息 135 | 136 | :param content: (必填|str) - 需要发送的消息内容 137 | :param search: (必填|str|dict|list)-搜索对象,同 wxpy.chats.search 使用方法一样。例如,可以使用字符串进行搜索好友或群,或指定具体属性搜索,如 puid=xxx 的字典 138 | :return: * status:发送状态,True 发送成,False 发送失败 139 | * message:发送失败详情 140 | """ 141 | url = '{0}send_to_message'.format(self.remote) 142 | if isinstance(search, dict): 143 | search = json.dumps(search) 144 | elif isinstance(search, list): 145 | search = reduce(lambda x, y: '{0} {1}'.format(x, y), search) 146 | data = self._wrap_post_data(content=content, search=search) 147 | res = requests.post(url, data=data, timeout=self.timeout) 148 | if res.status_code == requests.codes.ok: 149 | res_data = json.loads(self._convert_bytes(res.content)) 150 | if res_data.get('status') == STATUS_SUCCESS: 151 | return True, res_data.get('message') 152 | return False, res_data.get('message') 153 | res.raise_for_status() 154 | return False, 'Request or Response Error' 155 | 156 | 157 | class LoggingSenderHandler(logging.Handler, Sender): 158 | """ 159 | wechat_sender 的 LoggingHandler 对象,可以使用 logging.addHandler() 的方式快速使外部应用支持微信日志输出。在外部应用中:: 160 | 161 | # spider.py 162 | # 假如在一个爬虫脚本,我们想让此脚本的警告信息直接发到微信 163 | 164 | import logging 165 | from wechat_sender import LoggingSenderHandler 166 | 167 | logger = logging.getLogger(__name__) 168 | 169 | # spider code here 170 | def test_spider(): 171 | ... 172 | logger.exception("EXCEPTION: XXX") 173 | 174 | def init_logger(): 175 | sender_logger = LoggingSenderHandler('spider', level=logging.EXCEPTION) 176 | logger.addHandler(sender_logger) 177 | 178 | if __name__ == '__main__': 179 | init_logger() 180 | test_spider() 181 | 182 | """ 183 | 184 | def __init__(self, name=None, token=None, receiver=None, host='http://localhost', port=10245, level=30): 185 | """ 186 | :param name: (选填|str) - 标识日志来源,不填将取应用所在服务器地址为名称 187 | :param token: (选填|str) - 信令,如果不为空请保持和 listen 中的 token 一致 188 | :param receiver: (选填|str) - 接收者,wxpy 的 puid 或 微信名、昵称等,不填将发送至 default_receiver 189 | :param host: (选填|str) - 远程地址,本地调用不用填写 190 | :param port: (选填|int) - 发送端口,默认 10245 端口,如不为空请保持和 listen 中的 port 一致 191 | :param level: (选填|int) - 日志输出等级,默认为 logging.WARNING 192 | """ 193 | super(LoggingSenderHandler, self).__init__(level) 194 | Sender.__init__(self, token, receiver, host, port) 195 | if not name: 196 | import socket 197 | ip = socket.gethostbyname_ex(socket.gethostname())[0] 198 | name = ip 199 | self.name = name 200 | self.setFormatter(logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')) 201 | 202 | def emit(self, record): 203 | self.send('[{0}]\n{1}'.format(self.name, self.format(record))) 204 | -------------------------------------------------------------------------------- /wechat_sender/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | import codecs 4 | 5 | STATUS_SUCCESS = 0 6 | STATUS_PERMISSION_DENIED = 1 7 | STATUS_BOT_EXCEPTION = 2 8 | STATUS_TORNADO_EXCEPTION = 3 9 | STATUS_ERROR = 4 10 | 11 | DEFAULT_REMIND_TIME = 60 * 60 12 | DEFAULT_REPORT_TIME = 60 * 60 * 1000 13 | 14 | DELAY_TASK = 'DELAY_TASK' 15 | PERIODIC_TASK = 'PERIODIC_TASK' 16 | SYSTEM_TASK = 'SYSTEM_TASK' 17 | 18 | MESSAGE_REPORT_COMMAND = '@wsr' 19 | MESSAGE_STATUS_COMMAND = '@wss' 20 | 21 | 22 | def _read_config_list(): 23 | """ 24 | 配置列表读取 25 | """ 26 | with codecs.open('conf.ini', 'w+', encoding='utf-8') as f1: 27 | conf_list = [conf for conf in f1.read().split('\n') if conf != ''] 28 | return conf_list 29 | 30 | 31 | def write_config(name, value): 32 | """ 33 | 配置写入 34 | """ 35 | name = name.lower() 36 | new = True 37 | conf_list = _read_config_list() 38 | for i, conf in enumerate(conf_list): 39 | if conf.startswith(name): 40 | conf_list[i] = '{0}={1}'.format(name, value) 41 | new = False 42 | break 43 | if new: 44 | conf_list.append('{0}={1}'.format(name, value)) 45 | 46 | with codecs.open('conf.ini', 'w+', encoding='utf-8') as f1: 47 | for conf in conf_list: 48 | f1.write(conf + '\n') 49 | return True 50 | 51 | 52 | def read_config(name): 53 | """ 54 | 配置读取 55 | """ 56 | name = name.lower() 57 | conf_list = _read_config_list() 58 | for conf in conf_list: 59 | if conf.startswith(name): 60 | return conf.split('=')[1].split('#')[0].strip() 61 | return None 62 | 63 | 64 | class StatusWrapperMixin(object): 65 | """ 66 | 返回状态码 Mixin 67 | """ 68 | status_code = STATUS_SUCCESS 69 | status_message = '' 70 | 71 | def write(self, chunk): 72 | context = {'status': self.status_code, 73 | 'message': chunk} 74 | super(StatusWrapperMixin, self).write(context) 75 | --------------------------------------------------------------------------------