├── .gitignore ├── LICENSE ├── README.md ├── demo ├── __init__.py ├── ctrl8020.sh ├── manage.py ├── rebuild_db.py ├── runtestserver.sh ├── sample │ ├── __init__.py │ ├── admin.py │ ├── handlers.py │ ├── models.py │ ├── router.py │ ├── urls.py │ └── views.py └── wei_demo │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── res ├── flow.dia ├── flow.jpg ├── home.jpg ├── plugin_test.jpg └── text2text_1.jpg ├── setup.py └── weixin2py ├── __init__.py ├── admin.py ├── format.txt ├── handlers.py ├── models.py ├── plugin ├── __init__.py ├── activity.py ├── data │ └── who_the_spy │ │ └── who_the_spy.data ├── setting.py └── who_the_spy.py ├── routers.py ├── sample_msg.py ├── templates ├── response │ ├── msg_base.xml │ ├── msg_img.xml │ ├── msg_music.xml │ ├── msg_pic_text.xml │ ├── msg_text.xml │ ├── msg_video.xml │ └── msg_voice.xml └── send │ ├── menu_create.json │ ├── msg_music.json │ ├── msg_pic.json │ ├── msg_text.json │ ├── msg_text_pic.json │ ├── msg_video.json │ ├── msg_voice.json │ └── multi_msg_send.json └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # dirs and specified files 2 | env 3 | .idea 4 | .settings 5 | *.db 6 | localsettings.py 7 | 8 | # Python Lib files 9 | *.py[cod] 10 | *.pyc 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Packages 16 | *.egg 17 | *.egg-info 18 | dist 19 | build 20 | eggs 21 | parts 22 | bin 23 | var 24 | sdist 25 | develop-eggs 26 | .installed.cfg 27 | lib 28 | lib64 29 | 30 | # Installer logs 31 | pip-log.txt 32 | 33 | # Unit test / coverage reports 34 | .coverage 35 | .tox 36 | nosetests.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | weixin2py一个简单的微信自动应答平台 294 | Copyright (C) 2013 winkidney 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | weixin2py--微信公众平台web服务器-0.1.3 2 | --------------------------- 3 | 4 | 5 | ##Summary 6 | 7 | * 使用python2.7和django(**Demo仅支持大于1.7版本的Django**)开发的微信公众平台服务端程序,可以自动回复用户发来微信的消息。 8 | * 最初的启发来自“武大助手”,类似的平台,提供消息智能回复功能。 9 | * 完全重构了代码,虽然还有很多缺陷但已经完美了很多:)欢迎提出pull request。 10 | 11 | ##Feature 12 | * 支持可视化的自定义消息回复规则和程序性的消息回复规则 - 使用接口编写即可,通过简单的过程,你也可以集成你的聊天机器人:) 13 | * 文本路由((数据库路由和文件路由) - 使用正则表达式对回复进行匹配。 14 | * 插件支持,你可以用django模板语法定义动态消息回复 15 | * 易于集成 - 单独对django app `weixin2py`,里面有大部分可能用到的工具类和工具函数,需要的时候,新建你的django app,并且在app中使用WeiLib。 16 | * 消息模版 - WeiLil.lib中包含了text_response和pic_text_response函数,传入参数可以在任意view或者handler中使用. 17 | * 缓存session - 为增加诸如“谁是卧底”类的应用提供基础,更改你的缓存后端或者更改缓存接口即可. 18 | 19 | 20 | ##FAQ 21 | * 交流QQ群? - (于是就信手建了一个)416407192 22 | * 调试和测试? - [下载wei-dev二进制包](https://github.com/winkidney/wei-dev/releases)(图形化测试工具) 23 | 24 | ##Change Log 25 | * 2015-05-31 - 为Django1.8升级,修改setup.py中的Django版本要求 26 | * 2015-03-05 - 修改所有代码结构,分离weixin2py和demo应用,更新setup.py,现在可以单独安装weixin2py 27 | * 2014-10-10 - 修改版本号为0.0.1,增加setup.py,修改代码结构和说明文档,修改管理界面项目名称 28 | * 2014-06-10 - 修复小bug,更新文档。 29 | * 2014.05.09 - 2014.05.15 增加路由功能,插件功能。 30 | * 2014.05.08 - 全面重构中 31 | * 2013.xx.xx - first release,多么幼稚的代码 32 | 33 | ##How To 34 | ###INSTALL 35 | + Requires 36 | + django >= 1.5 37 | + python > 2.6 38 | 39 | 你可以使用如下方式安装python依赖: 40 | 41 | ```bash 42 | apt-get install python-pip 43 | pip install django 44 | ``` 45 | 46 | yum系系统类似方法安装依赖即可~~ 47 | 或 48 | 49 | ```bash 50 | cd weixin2py 51 | python setup.py install 52 | ``` 53 | 54 | 接下来,打开你的bash,切换到应用根目录,执行 55 | 56 | ```bash 57 | cd demo 58 | python rebuild_db.py 59 | ``` 60 | 61 | 然后在你的settings.py中编辑TOKEN,改为你自己的TOKEN 62 | 将会自动生成数据库并添加超级用户,用户名admin,密码admin,你可以自行去这个脚本修改默认设定,数据库为了方便起见使用了sqlite 63 | ```bash 64 | sh runserver.sh 65 | ``` 66 | 67 | 运行测试服务器(默认工作在80端口)。 68 | 配置完毕后,登陆你的微信公众平台,设定访问地址为:http://yourhost:port/admin/ 69 | 微信公众平台的接口URL为http://yourhost:port/weichat/ 70 | 也可以使用nginx+*cgi,任何你喜欢的方式。提供了脚本ctrl8020.sh来控制fcig模式的启动和关闭。 71 | 72 | ###Basic Usage 73 | 访问http://youhost:port/admin/ 74 | 登录,添加消息回复规则即可。 75 | 例如 想对用户发来的文本消息进行匹配,并回复一条文本消息,在管理面板中选择“文本>文本消息回复规则”,根据各个字段的提示进行填写即可。 76 | 如下示例图 77 | 用户发来的消息类型 2 回复的消息类型 78 | ####示例:文本2文本 消息回复规则 79 | 80 | ![添加文本->文本消息回复规则](res/home.jpg) 81 | ![添加文本->文本消息回复规则2](res/text2text_1.jpg) 82 | 83 | ####示例:添加使用插件的 文本2文本 消息回复规则 84 | 85 | ![使用包含插件功能的文本2文本消息回复规则](res/plugin_test.jpg) 86 | 87 | 插件消息使用django模板语法进行编写,参见[django模板语法](http://django-14-tkliuxing.readthedocs.org/en/latest/topics/templates.html) 88 | 插件编写参见[插件编写](#插件编写) 89 | 90 | ##流程说明 91 | ![工作流程图](res/flow.jpg) 92 | 93 | 94 | 95 | 96 | ##APIS 97 | 98 | ####[插件](id:插件编写) 99 | 插件目前仅工作在文本回复阶段,在匹配到消息回复之后,会对回复消息进行以此渲染,渲染所用的信息字典就是插件返回的字典。 100 | 每一个插件所返回的字典内容都会被自动合并到一个字典,然后在你的消息回复定义中使用你所返回的字典中的变量即可(当然可以是动态) 101 | ####目录结构 102 | 插件目录位于weixin2py/plugins/ 103 | 目录结构如下 104 | ```bash 105 | weixin2py/plugins/ 106 | |- setting.py - 插件配置文件 107 | |- activity.py - 插件实现 (名字可以任意) 108 | ``` 109 | ####编写 110 | 1.在插件目录下新建一个任意名字的py文件,然后根据如下格式写一个名为processor函数,这个函数会接收用户的消息对象作为参数,你可以根据用户的消息动态定义消息回复。 111 | 112 | ```python 113 | #coding:utf-8 114 | 115 | def processor(recv_msg): 116 | """A processor must return a dict. 117 | If not ,program will throw the returned result. 118 | """ 119 | from_user = recv_msg.from_user 120 | return {'test_plugin': 'only_the test plugin output', 121 | 'from_user': from_user 122 | } 123 | ``` 124 | 125 | 2.启用插件 126 | 打开setting.py,将你的插件导入并编辑plugin_text元组。 127 | 128 | 129 | ```python 130 | import activity 131 | plugin_text = ( activity, 132 | ) 133 | ``` 134 | 135 | 3.在消息回复中使用插件定义的内容 136 | ![使用包含插件功能的文本2文本消息回复规则](res/plugin_test.jpg) 137 | 138 | ###handler 139 | handler是拓展这个应用功能的另一种方式,最初开发使用的是这种方式,在没有数据库的情况下也可以正常运作,缺点是数据一旦写死修改很麻烦,适合用来生成动态内容,例如聊天机器人接口,查询接口之类的. 140 | ####结构 141 | 目前应用内置了两个router,file_router和db_router,执行的优先级是file_router->db_router,handler是file_router才有的结构。 142 | ####编写一个handler 143 | 在任意应用目录新建一个handlers.py(只是约定,可以自定义名称),将handler书写到其中。一个典型的handler如下. 144 | 145 | ```python 146 | #coding:utf-8 147 | 148 | 149 | from weixin2py.utils import text_response 150 | 151 | def default_handler(recv_msg): 152 | #do something 153 | return text_response(recv_msg, "没有匹配操作,返回默认信息") 154 | ``` 155 | 156 | handler 返回一个text_response或者一个pic_text_response(图文消息回复),也可以是你自定义的response,要求必须是一个django的HttpResponse实例。 157 | 158 | ####启用handler 159 | 为了启用handler,你需要增加一个匹配模式,打开应用目录下的sample/router.py文件,示例内容如下 160 | 161 | ```python 162 | #coding:utf-8 163 | 164 | import re 165 | 166 | from sample.handlers import (help_handler,about_handler, 167 | test_handler) 168 | 169 | 170 | """ 171 | 参考信息: 172 | 消息类型:text ,event,image, video, link , location, 173 | """ 174 | router_patterns =[ 175 | # 消息类型 消息文字(非文字类型消息留空) 操作函数 176 | ('text', re.compile('^help$'), help_handler), 177 | #('text', re.compile('^about$'), about_handler), 178 | #('text', re.compile('^test$'), test_handler), 179 | ] 180 | ``` 181 | 182 | 将你的handler导入。然后如注释一样添加模式。 183 | 184 | ####在view中使用handler和router 185 | 示例文件:sample/views.py - 仅展现关键逻辑,详情参考具体文件 186 | 187 | ```python 188 | # coding:utf-8 189 | from django.http import HttpResponse 190 | from django.views.decorators.csrf import csrf_exempt 191 | 192 | from weixin2py.routers import base_router, db_router 193 | from .router import router_patterns 194 | from weixin2py import WeiMsg, check_signature 195 | from weixin2py.handlers import default_handler 196 | 197 | try: 198 | from wei_demo.localsettings import TOKEN 199 | except ImportError: 200 | from wei_demo.settings import TOKEN 201 | 202 | # router must be a list of router instance 203 | routers = [base_router, db_router] 204 | 205 | 206 | @csrf_exempt 207 | def home(request): 208 | if request.method == 'GET': 209 | response = HttpResponse() 210 | if check_signature(request, TOKEN): 211 | response.write(request.GET.get('echostr')) 212 | return response 213 | else: 214 | response.write('不提供直接访问!') 215 | return response 216 | 217 | if request.method == 'POST': 218 | # warning: test demo wil not check signature. 219 | # check_signature(request, TOKEN) 220 | recv_msg = WeiMsg(request.body) 221 | for router in routers: 222 | result = router(recv_msg, router_patterns) 223 | if isinstance(result, HttpResponse): 224 | return result 225 | return default_handler(recv_msg) 226 | ``` 227 | 228 | ###Session 229 | 使用CPickle和缓存接口实现的一个Session类 230 | ```python 231 | class WeiSession(object): 232 | '''微信助手会话类,用来存储用户的会话状态''' 233 | def __init__(self, openid): #使用openid获得session,如果已存在则获得现有session,如果没有则生成的新session,缓存键的名称是openid 234 | 235 | def set_key(self, key , value): #设定一个键值,类似python字典操作,将会自动保存到会话当中 236 | 237 | def get_key(self, key): #取得key,如果不存在则返回空 238 | ``` 239 | 240 | 具体的使用请自由发挥 241 | 242 | ###其他接口/工具类 243 | weixin2py - class: WeiMsg - 从用户发送的消息从获得一个消息实例,自动识别类型并生成相应属性 244 | weixin2py - function:check_signature(request, token) - 从一个request对象和指定token中验证消息是否合法,合法返回True,不合法返回False 245 | weixin2py - function:get_token(appid, secretkey) - 返回一个access_token,用于腾讯的其他接口的必要验证 246 | 247 | 248 | ##To Do List 249 | 1. 写一个谁是卧底的插件(因为懒散暂时搁置) 250 | 2. 更改插件工作流(因为未收集到任何意见暂时搁置) 251 | 252 | [博客](http://blog.winkidney.com/) 253 | 254 | [My-github](http://github.com/winkidney) 255 | 256 | 257 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /demo/ctrl8020.sh: -------------------------------------------------------------------------------- 1 | #--------------- 2 | #!/bin/sh 3 | 4 | if [ $# != 1 ] 5 | then 6 | echo 'Usage: ctrl.sh start|stop|restart ' 7 | elif [ $1 = "start" ] 8 | then 9 | python manage.py runfcgi method=threaded host=127.0.0.1 port=8020 maxchildren=300 pidfile=/tmp/weixinfcgi.pid 10 | elif [ $1 = "stop" ] 11 | then 12 | kill `cat /tmp/weixinfcgi.pid` 13 | echo 'weixin2py killed' 14 | elif [ $1 = "restart" ] 15 | then 16 | kill `cat /tmp/weixinfcgi.pid` 17 | echo 'weixin2py killed\n' 18 | python manage.py runfcgi method=threaded host=127.0.0.1 port=8020 maxchildren=300 pidfile=/tmp/weixinfcgi.pid 19 | echo 'process started!' 20 | fi 21 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | if os.path.isfile("wei_demo/localsettings.py"): 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wei_demo.localsettings") 8 | else: 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wei_demo.settings") 10 | 11 | from django.core.management import execute_from_command_line 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /demo/rebuild_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | import sys 5 | import os 6 | from django.core import management 7 | import django 8 | 9 | 10 | sys.path.append(os.path.realpath(__file__).replace('\\', '/')) 11 | 12 | 13 | if os.path.isfile("wei_demo/localsettings.py"): 14 | os.environ['DJANGO_SETTINGS_MODULE'] = "wei_demo.localsettings" 15 | else: 16 | os.environ['DJANGO_SETTINGS_MODULE'] = "wei_demo.settings" 17 | 18 | 19 | from django.contrib.auth.models import User 20 | 21 | django.setup() 22 | 23 | 24 | def syncdb_with_su(su_name, su_email, su_passwd): 25 | # sync db 26 | management.call_command('syncdb', interactive=False) 27 | print "sync done" 28 | # create super user 29 | user = User.objects.create_superuser(su_name, su_email, su_passwd) 30 | # user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword') 31 | user.save() 32 | print "super user added" 33 | 34 | if __name__ == '__main__': 35 | if os.path.isfile('weixin2py.db'): 36 | os.remove('weixin2py.db') 37 | syncdb_with_su('admin', 'admin@admin.com', 'admin') 38 | -------------------------------------------------------------------------------- /demo/runtestserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python manage.py runserver 0.0.0.0:8000 3 | -------------------------------------------------------------------------------- /demo/sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/demo/sample/__init__.py -------------------------------------------------------------------------------- /demo/sample/admin.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.contrib import admin 3 | from django.contrib.auth.admin import UserAdmin 4 | from django.contrib.auth.models import User 5 | 6 | from .models import WeixinUser 7 | 8 | 9 | class ProfileInline(admin.StackedInline): 10 | model = WeixinUser 11 | max_num = 1 12 | can_delete = False 13 | 14 | 15 | class CustomUserAdmin(UserAdmin): 16 | inlines = [ProfileInline, ] 17 | 18 | admin.site.unregister(User) 19 | admin.site.register(User, CustomUserAdmin) 20 | -------------------------------------------------------------------------------- /demo/sample/handlers.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from weixin2py.utils import PTItem 4 | from weixin2py.utils import text_response, pic_text_response 5 | 6 | 7 | def test_handler(recv_msg, *args, **kwargs): 8 | title = "测试图文消息" 9 | description = "图文消息描述" 10 | pic_url = "//placeimg.com/160/100/any" 11 | url = "http://www.baidu.com" 12 | items = [PTItem(title, description, pic_url, url), ] * 2 13 | return pic_text_response(recv_msg, items) 14 | 15 | 16 | def about_handler(recv_msg, *args, **kwargs): 17 | content = """ 18 | 关于我们 19 | """ 20 | return text_response(recv_msg, content) 21 | 22 | 23 | def subscribe_handler(recv_msg, *args, **kwargs): 24 | content = """ 25 | --键入小写命令-- 26 | """ 27 | return text_response(recv_msg, content) 28 | 29 | 30 | def help_handler(recv_msg, *args, **kwargs): 31 | return subscribe_handler(recv_msg, *args, **kwargs) 32 | -------------------------------------------------------------------------------- /demo/sample/models.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.db import models 3 | from django.contrib.auth.models import User 4 | 5 | # Create your models here. 6 | 7 | 8 | class WeixinUser(models.Model): 9 | 10 | '''WeixinUser profile''' 11 | 12 | auth_user = models.OneToOneField(User, blank=True) # 拓展用户 13 | nickname = models.CharField(max_length=30, blank=True, verbose_name=u'昵称') 14 | openid = models.CharField( 15 | max_length=40, unique=True, verbose_name=u'OpenID') 16 | tel = models.CharField(max_length=40, blank=True, verbose_name=u"电话") 17 | 18 | def __unicode__(self): 19 | return u"%s %s" % (self.id, self.openid) 20 | 21 | 22 | def create_user_profile(sender, instance, created, **kwargs): 23 | """Create the UserProfile when a new User is saved""" 24 | if created: 25 | profile = WeixinUser() 26 | profile.user = instance 27 | profile.save() 28 | 29 | 30 | def create_weiuser(openid, password): 31 | user = User() 32 | user.set_password(password) 33 | user.username = 'UnNamedUser' 34 | user.save() 35 | 36 | profile = WeixinUser() 37 | profile.auth_user = user 38 | profile.openid = openid 39 | profile.save() 40 | -------------------------------------------------------------------------------- /demo/sample/router.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | import re 4 | 5 | from .handlers import (help_handler, about_handler, test_handler) 6 | 7 | 8 | # ---------------- 参考 ----------------- 9 | # 参考信息: 10 | # 消息类型:text ,event,image, video, link , location, 11 | 12 | 13 | router_patterns = [ 14 | # 消息类型 消息文字(非文字类型消息留空) 操作函数 15 | # ('text', re.compile('^help$'), help_handler), 16 | # ('text', re.compile('^about$'), about_handler), 17 | ('text', re.compile('^test$'), test_handler), 18 | ] 19 | -------------------------------------------------------------------------------- /demo/sample/urls.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.conf.urls import patterns, url 3 | 4 | from . import views 5 | 6 | from django.contrib import admin 7 | 8 | urlpatterns = patterns( 9 | '', 10 | # Examples: 11 | # url(r'^$', 'weixin2py.views.home', name='home'), 12 | # url(r'^weixin2py/', include('weixin2py.foo.urls')), 13 | url(r'^$', views.home), 14 | ) 15 | -------------------------------------------------------------------------------- /demo/sample/views.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.http import HttpResponse 3 | from django.views.decorators.csrf import csrf_exempt 4 | 5 | from weixin2py.routers import base_router, db_router 6 | from .router import router_patterns 7 | from weixin2py import WeiMsg, check_signature 8 | from weixin2py.handlers import default_handler 9 | 10 | try: 11 | from wei_demo.localsettings import TOKEN 12 | except ImportError: 13 | from wei_demo.settings import TOKEN 14 | 15 | # router must be a list of router instance 16 | routers = [base_router, db_router] 17 | 18 | 19 | @csrf_exempt 20 | def home(request): 21 | if request.method == 'GET': 22 | response = HttpResponse() 23 | if check_signature(request, TOKEN): 24 | response.write(request.GET.get('echostr')) 25 | return response 26 | else: 27 | response.write('不提供直接访问!') 28 | return response 29 | 30 | if request.method == 'POST': 31 | # warning: test demo wil not check signature. 32 | # check_signature(request, TOKEN) 33 | recv_msg = WeiMsg(request.body) 34 | for router in routers: 35 | result = router(recv_msg, router_patterns) 36 | if isinstance(result, HttpResponse): 37 | return result 38 | return default_handler(recv_msg) 39 | -------------------------------------------------------------------------------- /demo/wei_demo/__init__.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | -------------------------------------------------------------------------------- /demo/wei_demo/settings.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | # localsettings.py - Django localsettings for weixin2py project. 3 | 4 | import os 5 | from weixin2py import weixin2py_template_dir 6 | 7 | PROJECT_ROOT = os.path.join( 8 | os.path.realpath(os.path.dirname(__file__)), os.pardir).replace('\\', '/') 9 | 10 | # Make this unique, and don't share it with anybody. 11 | SECRET_KEY = '_c@$un4*ihb4(&hf&556485qi@)yc-x=rw17h05jhe!h#fg^3r' 12 | 13 | DEBUG = True 14 | 15 | TEMPLATE_DEBUG = DEBUG 16 | 17 | ADMINS = ( 18 | # ('Your Name', 'your_email@example.com'), 19 | ) 20 | 21 | MANAGERS = ADMINS 22 | 23 | DATABASES = { 24 | 'default': { 25 | # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 26 | 'ENGINE': 'django.db.backends.sqlite3', 27 | # Or path to database file if using sqlite3. 28 | 'NAME': os.path.join(PROJECT_ROOT, 'weixin2py.db'), 29 | # The following settings are not used with sqlite3: 30 | 'USER': '', 31 | 'PASSWORD': '', 32 | # Empty for localhost through domain sockets or '127.0.0.1' for 33 | # localhost through TCP. 34 | 'HOST': '', 35 | 'PORT': '', # Set to empty string for default. 36 | } 37 | } 38 | 39 | # Hosts/domain names that are valid for this site; required if DEBUG is False 40 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 41 | ALLOWED_HOSTS = [] 42 | 43 | # Local time zone for this installation. Choices can be found here: 44 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 45 | # although not all choices may be available on all operating systems. 46 | # In a Windows environment this must be set to your system time zone. 47 | TIME_ZONE = 'America/Chicago' 48 | 49 | # Language code for this installation. All choices can be found here: 50 | # http://www.i18nguy.com/unicode/language-identifiers.html 51 | LANGUAGE_CODE = 'zh-hans' 52 | 53 | SITE_ID = 1 54 | 55 | # If you set this to False, Django will make some optimizations so as not 56 | # to load the internationalization machinery. 57 | USE_I18N = True 58 | 59 | # If you set this to False, Django will not format dates, numbers and 60 | # calendars according to the current locale. 61 | USE_L10N = True 62 | 63 | # If you set this to False, Django will not use timezone-aware datetimes. 64 | USE_TZ = True 65 | 66 | # Absolute filesystem path to the directory that will hold user-uploaded files. 67 | # Example: "/var/www/example.com/media/" 68 | MEDIA_ROOT = '' 69 | 70 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 71 | # trailing slash. 72 | # Examples: "http://example.com/media/", "http://media.example.com/" 73 | MEDIA_URL = '' 74 | 75 | # Absolute path to the directory static files should be collected to. 76 | # Don't put anything in this directory yourself; store your static files 77 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 78 | # Example: "/var/www/example.com/static/" 79 | STATIC_ROOT = '' 80 | 81 | # URL prefix for static files. 82 | # Example: "http://example.com/static/", "http://static.example.com/" 83 | STATIC_URL = '/static/' 84 | 85 | # Additional locations of static files 86 | STATICFILES_DIRS = ( 87 | PROJECT_ROOT + '/static/', 88 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 89 | # Always use forward slashes, even on Windows. 90 | # Don't forget to use absolute paths, not relative paths. 91 | ) 92 | 93 | # List of finder classes that know how to find static files in 94 | # various locations. 95 | STATICFILES_FINDERS = ( 96 | 'django.contrib.staticfiles.finders.FileSystemFinder', 97 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 98 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 99 | ) 100 | 101 | 102 | # List of callables that know how to import templates from various sources. 103 | TEMPLATE_LOADERS = ( 104 | 'django.template.loaders.filesystem.Loader', 105 | 'django.template.loaders.app_directories.Loader', 106 | # 'django.template.loaders.eggs.Loader', 107 | ) 108 | 109 | MIDDLEWARE_CLASSES = ( 110 | 'django.middleware.common.CommonMiddleware', 111 | 'django.contrib.sessions.middleware.SessionMiddleware', 112 | 'django.middleware.csrf.CsrfViewMiddleware', 113 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 114 | 'django.contrib.messages.middleware.MessageMiddleware', 115 | # Uncomment the next line for simple clickjacking protection: 116 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 117 | ) 118 | 119 | ROOT_URLCONF = 'wei_demo.urls' 120 | 121 | 122 | # Python dotted path to the WSGI application used by Django's runserver. 123 | WSGI_APPLICATION = 'wei_demo.wsgi.application' 124 | 125 | TEMPLATES = [ 126 | { 127 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 128 | 'DIRS': [weixin2py_template_dir, ], 129 | 'APP_DIRS': True, 130 | 'OPTIONS': { 131 | 'context_processors': [ 132 | 'django.template.context_processors.debug', 133 | 'django.template.context_processors.request', 134 | 'django.contrib.auth.context_processors.auth', 135 | 'django.contrib.messages.context_processors.messages', 136 | ], 137 | }, 138 | }, 139 | ] 140 | 141 | INSTALLED_APPS = ( 142 | 'django.contrib.auth', 143 | 'django.contrib.contenttypes', 144 | 'django.contrib.sessions', 145 | 'django.contrib.sites', 146 | 'django.contrib.messages', 147 | 'django.contrib.staticfiles', 148 | 'weixin2py', 149 | 'sample', 150 | # Uncomment the next line to enable the admin: 151 | 'django.contrib.admin', 152 | # Uncomment the next line to enable admin documentation: 153 | #'django.contrib.admindocs', 154 | ) 155 | # Auth backend setting 156 | AUTH_PROFILE_MODULE = 'djangoadmin.myadmin.UserProfile' 157 | 158 | 159 | # A sample logging configuration. The only tangible logging 160 | # performed by this configuration is to send an email to 161 | # the site admins on every HTTP 500 error when DEBUG=False. 162 | # See http://docs.djangoproject.com/en/dev/topics/logging for 163 | # more details on how to customize your logging configuration. 164 | LOGGING = { 165 | 'version': 1, 166 | 'disable_existing_loggers': False, 167 | 'filters': { 168 | 'require_debug_false': { 169 | '()': 'django.utils.log.RequireDebugFalse' 170 | } 171 | }, 172 | 'handlers': { 173 | 'mail_admins': { 174 | 'level': 'ERROR', 175 | 'filters': ['require_debug_false'], 176 | 'class': 'django.utils.log.AdminEmailHandler' 177 | } 178 | }, 179 | 'loggers': { 180 | 'django.request': { 181 | 'handlers': ['mail_admins'], 182 | 'level': 'ERROR', 183 | 'propagate': True, 184 | }, 185 | } 186 | } 187 | CACHES = { 188 | 'default': { 189 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 190 | 'LOCATION': 'wei_demo-cache' 191 | } 192 | } 193 | 194 | TOKEN = 'kidney' -------------------------------------------------------------------------------- /demo/wei_demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | # Uncomment the next two lines to enable the admin: 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns( 7 | '', 8 | # Examples: 9 | # url(r'^$', 'weixin2py.views.home', name='home'), 10 | # url(r'^weixin2py/', include('weixin2py.foo.urls')), 11 | url(r'^weichat/',include("sample.urls")), 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | url(r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /demo/wei_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for weixin2py project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "weixin2py.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wei_demo.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /res/flow.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/res/flow.dia -------------------------------------------------------------------------------- /res/flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/res/flow.jpg -------------------------------------------------------------------------------- /res/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/res/home.jpg -------------------------------------------------------------------------------- /res/plugin_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/res/plugin_test.jpg -------------------------------------------------------------------------------- /res/text2text_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/res/text2text_1.jpg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | # setup.py - 14-10-3 4 | __author__ = 'winkidney' 5 | 6 | import os 7 | from setuptools import setup, find_packages 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | requires = [ 12 | 'django>=1.8.0', 13 | ] 14 | 15 | setup( 16 | name='weixin2py', 17 | version='0.1.3', 18 | packages=find_packages('.', exclude=("wei_demo", )), 19 | install_requires=requires, 20 | ) -------------------------------------------------------------------------------- /weixin2py/__init__.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import hashlib 3 | import os 4 | import re 5 | import urllib2 6 | 7 | 8 | def weixin2py_template_dir(): 9 | return os.path.abspath( 10 | os.path.dirname(__file__)).replace('\\', '/') 11 | 12 | 13 | # basic info 14 | re_msg_type = re.compile(r"") 15 | re_msg_tuid = re.compile(r"") 16 | re_msg_fuid = re.compile(r"") 17 | re_msg_ctime = re.compile(r"(.*?)") 18 | re_msg_id = re.compile(r"(.*?)") 19 | re_media_id = re.compile(r"") 20 | # text msg 21 | re_text_content = re.compile(r"") 22 | 23 | 24 | # img msg 25 | re_img_url = re.compile(r"") 26 | re_img_id = re.compile(r"") 27 | # location msg 28 | re_locx = re.compile(r"(.*?)") 29 | re_locy = re.compile(r"(.*?)") 30 | re_scale = re.compile(r"(.*?)") 31 | re_label = re.compile(r"") 32 | 33 | # link msg 34 | re_title = re.compile(r"<!\[CDATA\[(.*?)\]\]>") 35 | re_description = re.compile(r"") 36 | re_url = re.compile(r"") 37 | 38 | # event msg 39 | re_event = re.compile(r"") 40 | re_eventkey = re.compile(r"") 41 | 42 | 43 | class WeiMsg(object): 44 | 45 | """输入一个xml文本字符串对象,生成一个object并返回""" 46 | 47 | def get_info(self, regx, msg): 48 | result = re.findall(regx, msg) 49 | if result: 50 | return result[0] 51 | else: 52 | return '' 53 | 54 | def get_text_msg(self, msg): 55 | 56 | self.content = self.get_info(re_text_content, msg) 57 | 58 | def get_img_msg(self, msg): 59 | """图片消息""" 60 | 61 | self.pic_url = self.get_info(re_img_url, msg) 62 | self.media_id = self.get_info(re_media_id, msg) 63 | 64 | def get_location_msg(self, msg): 65 | """地理位置消息""" 66 | 67 | self.location_x = self.get_info(re_locx, msg) 68 | self.location_y = self.get_info(re_locy, msg) 69 | self.scale = self.get_info(re_scale, msg) 70 | self.label = self.get_info(re_label, msg) 71 | 72 | def get_link_msg(self, msg): 73 | """链接消息推送""" 74 | 75 | self.title = self.get_info(re_title, msg) 76 | self.description = self.get_info(re_description, msg) 77 | self.url = self.get_info(re_url, msg) 78 | 79 | def get_event_msg(self, msg): 80 | """事件推送""" 81 | self.event = self.get_info(re_event, msg) 82 | self.event_key = self.get_info(re_eventkey, msg) 83 | 84 | def __init__(self, msg): 85 | """generate a message object 86 | """ 87 | self.to_user_name = self.get_info(re_msg_tuid, msg) 88 | self.from_user_name = self.get_info(re_msg_fuid, msg) 89 | self.create_time = self.get_info(re_msg_ctime, msg) 90 | self.msg_type = self.get_info(re_msg_type, msg) 91 | self.msg_id = self.get_info(re_msg_id, msg) 92 | msgtype = self.msg_type 93 | if msgtype == 'text': 94 | self.get_text_msg(msg) 95 | elif msgtype == 'image': 96 | self.get_img_msg(msg) 97 | elif msgtype == 'location': 98 | self.get_location_msg(msg) 99 | elif msgtype == 'link': 100 | self.get_link_msg(msg) 101 | elif msgtype == 'event': 102 | self.get_event_msg(msg) 103 | 104 | 105 | 106 | # Message for response to user 107 | class BaseReMsg(object): 108 | 109 | """Base returned message for client.""" 110 | 111 | def __init__(self, to_user, from_user, ctime, func_flag=0): 112 | """ 113 | Normal init by (to_user, from_user, ctime, func_flag). 114 | :param to_user: target openid 115 | :type to_user: str, unicode, int 116 | :param from_user: openid 117 | :type from_user: str, unicode, int 118 | :param ctime: the origin msg send time(unix timestamp). 119 | :param func_flag: weichat func_flag. 120 | :return: 121 | """ 122 | self.to_user_name = from_user 123 | self.from_user_name = to_user 124 | self.create_time = int(ctime) + 1 125 | self.function_flag = func_flag 126 | 127 | 128 | class TextMsg(BaseReMsg): 129 | 130 | """文字消息类""" 131 | 132 | def make_msg(self, content): 133 | self.content = content 134 | 135 | 136 | class MusicMsg(BaseReMsg): 137 | 138 | """music message""" 139 | 140 | def make_msg(self, title, description, music_url, hq, media_id=0): 141 | self.title = title 142 | self.description = description 143 | self.music_url = music_url 144 | self.hq_music_url = hq 145 | self.media_id = media_id 146 | 147 | 148 | class ImgMsg(BaseReMsg): 149 | 150 | """Image message""" 151 | 152 | def make_msg(self, media_id): 153 | self.media_id = media_id 154 | 155 | 156 | class PicTextMsg(BaseReMsg): 157 | 158 | """图文消息类""" 159 | 160 | def __init__(self, to_user, from_user, ctime, func_flag=0): 161 | super(PicTextMsg, self).__init__( 162 | to_user, from_user, ctime, func_flag=0) 163 | self.articles = [] 164 | 165 | def make_msg(self, article_count): 166 | 167 | self.article_count = article_count 168 | 169 | def new_item(self, title, description, pic_url, url): 170 | item = {'title': title, 171 | 'description': description, 172 | 'pic_url': pic_url, 173 | 'url': url, } 174 | self.articles.append(item) 175 | 176 | 177 | class PTItem(object): 178 | 179 | def __init__(self, title, description, pic_url, url): 180 | self.title = title 181 | self.description = description 182 | self.pic_url = pic_url 183 | self.url = url 184 | 185 | 186 | class MButton(object): 187 | 188 | """ button class of the weichat meun""" 189 | 190 | def __init__(self, name, **kwargs): 191 | self.type = None 192 | self.key = None 193 | self.url = None 194 | self.sub_buttons = [] 195 | self.name = name 196 | url = kwargs.get('url') 197 | key = kwargs.get('key') 198 | if url or key: 199 | if url: 200 | self.make_view(url) 201 | else: 202 | self.make_click(key) 203 | 204 | def make_click(self, key): 205 | self.type = 'click' 206 | self.key = key 207 | 208 | def make_view(self, url): 209 | self.type = 'view' 210 | self.url = url 211 | 212 | def add_button(self, button): 213 | if isinstance(button, MButton): 214 | self.sub_buttons.append(button) 215 | else: 216 | raise TypeError 217 | 218 | 219 | def check_signature(request, token): 220 | """Verify if the author of received msg is tencent.""" 221 | request_dict = request.GET 222 | if request_dict.get('signature') and request_dict.get('timestamp') \ 223 | and request_dict.get('nonce') and request_dict.get('echostr'): 224 | signature = request_dict.get('signature') 225 | timestamp = request_dict.get('timestamp') 226 | nonce = request_dict.get('nonce') 227 | token = token 228 | tmplist = sorted([token, timestamp, nonce]) 229 | newstr = ''.join(tmplist) 230 | sha1result = hashlib.sha1() 231 | sha1result.update(newstr) 232 | if sha1result.hexdigest() == str(signature): 233 | return True 234 | else: 235 | return False 236 | else: 237 | return False 238 | 239 | 240 | def get_atoken(app_id, app_secret): 241 | """ 242 | Get AccessToken by appid and appsecret. 243 | :param app_id: 244 | :param app_secret: Tencent appsecret 245 | :return: Access token. 246 | :rtype str or unicode 247 | """ 248 | url = """https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%(appid)s&secret=%(appsecret)s""" \ 249 | % {'appid': app_id, 'appsecret': app_secret} 250 | 251 | try: 252 | result = urllib2.urlopen(url, timeout=20).read() 253 | except urllib2.HTTPError, urllib2.URLError: 254 | return None 255 | if result: 256 | if re.findall('"access_token":"(.*?)"', result): 257 | return result[0] 258 | return None 259 | 260 | 261 | -------------------------------------------------------------------------------- /weixin2py/admin.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.contrib import admin 3 | from .models import (DBTextMsg, PatternT2T, DBImgTextMsg, PatternT2PT, 4 | PatternE2PT, PatternE2T) 5 | 6 | 7 | admin.site.register(PatternT2T) 8 | admin.site.register(PatternT2PT) 9 | admin.site.register(PatternE2PT) 10 | admin.site.register(PatternE2T) 11 | admin.site.register(DBTextMsg) 12 | admin.site.register(DBImgTextMsg) 13 | -------------------------------------------------------------------------------- /weixin2py/format.txt: -------------------------------------------------------------------------------- 1 | //文本 2 | 3 | 4 | 5 | 1399455419 6 | 7 | 8 | 1234567890123456 9 | 10 | //0ˆ70Š70ˆ60‹00ˆ30‹40ˆ30„4 11 | 12 | 13 | 14 | 1399455609 15 | 16 | 17 | 1234567890123456 18 | 19 | 20 | //0ˆ10†40‡40…10ˆ30‹40ˆ30„4 21 | 22 | 23 | 24 | 1399455643 25 | 26 | 27 | 1234567890123456 28 | 29 | 30 | //0ˆ20†30‰00‡10ˆ30‹40ˆ30„4 31 | 32 | 33 | 34 | 1399455669 35 | 36 | 255 37 | 255 38 | 20 39 | 40 | 1234567890123456 41 | 42 | 43 | //0†10‰1¡Á0„40‡80‡00†40‹6 44 | 45 | 46 | 47 | 1399455717 48 | 49 | 50 | 51 | 52 | //0‡60„30ˆ30‹40†10‰1¡Á0„4 53 | 54 | 55 | 56 | 1399455735 57 | 58 | 59 | 60 | 61 | //0…50‡90…80„60‡80‡00†40‹6 62 | 63 | 64 | 65 | 1399455761 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /weixin2py/handlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | from .utils import text_response 5 | 6 | 7 | def default_handler(recv_msg): 8 | return text_response(recv_msg, "没有匹配操作,返回默认信息") 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /weixin2py/models.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | # weilib/models.py - database definition file of weilib 3 | # by kidney 2014.05.12 4 | from django.db import models 5 | 6 | MSG_TYPES = ( 7 | ('text', '文本消息'), 8 | ('event', '事件消息'), 9 | ('image', '图片消息'), 10 | ('location', '位置消息'), 11 | ('voice', '语音消息'), 12 | ('video', '视频消息'), 13 | ) 14 | EVENTS = ( 15 | ('subscribe', '关注事件'), 16 | ('unsubscribe', '取消关注事件'), 17 | ('SCAN', '扫描二维码'), 18 | ('LOCATION', '上报地理位置'), 19 | ('CLICK', '自定义菜单事件'), 20 | ('VIEW', '用户点击链接的跳转事件'), 21 | ) 22 | 23 | 24 | class DBTextMsg(models.Model): 25 | 26 | """msg in database""" 27 | class Meta: 28 | verbose_name = u'回复管理(文字消息)' 29 | verbose_name_plural = u'回复管理(文字消息)' 30 | name = models.CharField(blank=True, max_length=50, verbose_name=u"消息名字", 31 | help_text=u"可以为空,仅用来标识消息") 32 | content = models.TextField(blank=False, verbose_name=u"消息内容") 33 | 34 | def __unicode__(self): 35 | return u'%s %s' % (self.id, self.name) 36 | 37 | 38 | class DBImgTextMsg(models.Model): 39 | 40 | """image_text msg in database""" 41 | class Meta: 42 | verbose_name = u'回复管理(图文消息)' 43 | verbose_name_plural = u'回复管理(图文消息)' 44 | name = models.CharField(blank=True, max_length=50, verbose_name=u"消息名称", 45 | help_text=u"可以为空,仅用来标识消息") 46 | title = models.CharField(blank=True, max_length=255, verbose_name=u"消息标题") 47 | description = models.CharField( 48 | blank=True, max_length=255, verbose_name=u"消息描述") 49 | pic_url = models.URLField(blank=False, verbose_name=u"图片地址") 50 | url = models.URLField(blank=False, max_length=255, verbose_name=u"文章地址") 51 | 52 | def __unicode__(self): 53 | return u'%s %s' % (self.id, self.name) 54 | 55 | 56 | class PatternE2T(models.Model): 57 | 58 | """text response pattern to user""" 59 | class Meta: 60 | verbose_name = u'回复规则管理(事件>文本消息)' 61 | verbose_name_plural = u'回复规则管理(事件>文本消息)' 62 | name = models.CharField(blank=True, max_length=50, verbose_name=u"规则命名", 63 | help_text=u"可以为空,仅用来标识规则") 64 | type = models.CharField(max_length=20, 65 | choices=MSG_TYPES, verbose_name=u"收到的消息类型(请保持默认)", 66 | default='event',) 67 | event = models.CharField(max_length=30, 68 | choices=EVENTS, 69 | default='CLICK', verbose_name=u"事件类型", 70 | help_text=u"除非收到的消息类型为“自定义菜单事件或者点击链接跳转事件,否则不要修改本字段”") 71 | event_key = models.CharField(blank=True, max_length=255, 72 | verbose_name=u"event_key或者自定义url", 73 | help_text=u'对于自定义菜单事件和自定义链接跳转事件这个是必填的!') 74 | handler = models.ForeignKey(DBTextMsg, verbose_name=u"回复消息") 75 | 76 | def __unicode__(self): 77 | return u'%s %s' % (self.id, self.name) 78 | 79 | 80 | class PatternE2PT(models.Model): 81 | 82 | """text response pattern to user""" 83 | class Meta: 84 | verbose_name = u'回复规则管理(事件>图文消息回复)' 85 | verbose_name_plural = u'回复规则管理(事件>图文消息回复)' 86 | name = models.CharField(blank=True, max_length=50, verbose_name=u"规则命名", 87 | help_text=u"可以为空,仅用来标识规则") 88 | type = models.CharField(max_length=20, 89 | choices=MSG_TYPES, 90 | default='event', verbose_name=u"用户消息类型(请保持默认)", 91 | help_text=u"除非你清楚这个字段的含义,否则请不要随意更改") 92 | event = models.CharField(max_length=30, 93 | choices=EVENTS, 94 | default='CLICK', verbose_name=u"事件类型") 95 | event_key = models.CharField(blank=True, max_length=255, 96 | verbose_name=u"event_key或者自定义url", 97 | help_text='对于自定义菜单事件和自定义链接跳转事件这个是必填的!') 98 | handler = models.ManyToManyField( 99 | DBImgTextMsg, verbose_name=u"回复消息", help_text=u"最多允许五条,不然会出错") 100 | 101 | def __unicode__(self): 102 | return u'%s %s' % (self.id, self.name) 103 | 104 | 105 | class PatternT2PT(models.Model): 106 | 107 | """image_text response pattern to user""" 108 | class Meta: 109 | verbose_name = u'回复规则管理(文本>图文消息)' 110 | verbose_name_plural = u'回复规则管理(文本>图文消息)' 111 | name = models.CharField(blank=True, max_length=50, verbose_name=u"规则命名", 112 | help_text=u"可以为空,仅用来标识规则") 113 | type = models.CharField(max_length=20, 114 | choices=MSG_TYPES, 115 | default='text', verbose_name=u"用户消息类型(请保持默认)", 116 | help_text=u"除非你清楚这个字段的含义,否则请不要随意更改") 117 | content = models.CharField(max_length=50, blank=True, verbose_name=u"需要匹配的消息", 118 | help_text=u"使用正则表达式") 119 | handler = models.ManyToManyField( 120 | DBImgTextMsg, verbose_name=u"回复消息", help_text=u"最多允许五条,不然会出错") 121 | 122 | def __unicode__(self): 123 | return u'%s %s' % (self.id, self.name) 124 | 125 | 126 | class PatternT2T(models.Model): 127 | 128 | """text response pattern to user""" 129 | class Meta: 130 | verbose_name = u'回复规则管理(文本>文本消息)' 131 | verbose_name_plural = u'回复规则管理(文本>文本消息)' 132 | name = models.CharField(blank=True, max_length=50, verbose_name="规则命名", 133 | help_text=u"可以为空,仅用来标识规则") 134 | type = models.CharField(max_length=20, 135 | choices=MSG_TYPES, 136 | default='text', verbose_name=u"用户消息类型(请保持默认)", 137 | help_text=u"除非你清楚这个字段的含义,否则请不要随意更改") 138 | content = models.CharField(max_length=100, blank=True, verbose_name=u"收到的消息", 139 | help_text=u"使用正则表达式") 140 | handler = models.ForeignKey(DBTextMsg, verbose_name=u"响应的消息内容") 141 | 142 | def __unicode__(self): 143 | return u'%s %s' % (self.id, self.name) 144 | -------------------------------------------------------------------------------- /weixin2py/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkidney/weixin2py/dd0fc951a592da5afc4fe010da4dc7472455db4b/weixin2py/plugin/__init__.py -------------------------------------------------------------------------------- /weixin2py/plugin/activity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | #weilib/plugin/acitvity.py - activity plugin for weilib 4 | #ver 0.1 by winkidney 2014.05.10 5 | 6 | 7 | def processor(recv_msg): 8 | """A processor must return a dict. 9 | If not ,program will throw the returned result. 10 | """ 11 | return {'test_plugin': 'only_the test plugin output'} -------------------------------------------------------------------------------- /weixin2py/plugin/data/who_the_spy/who_the_spy.data: -------------------------------------------------------------------------------- 1 | 王菲,那英 2 | 元芳,展昭 3 | 麻雀,乌鸦 4 | 胖子,肥肉 5 | 眉毛,胡须 6 | 何炅,维嘉 7 | 状元,冠军 8 | 饺子,包子 9 | 端午节,中秋节 10 | 摩托车,电动车 11 | 高跟鞋,增高鞋 12 | 汉堡包,肉夹馍 13 | 小矮人,葫芦娃 14 | 蜘蛛侠,蜘蛛精 15 | 节节高升,票房大卖 16 | 反弹琵琶,乱弹棉花 17 | 玫瑰,月季 18 | 董永,许仙 19 | 若曦,晴川 20 | 谢娜,李湘 21 | 孟非,乐嘉 22 | 牛奶,豆浆 23 | 保安,保镖 24 | 白菜,生菜 25 | 辣椒,芥末 26 | 金庸,古龙 27 | 赵敏,黄蓉 28 | 海豚,海狮 29 | 水盆,水桶 30 | 唇膏,口红 31 | 森马,以纯 32 | 烤肉,涮肉 33 | 气泡,水泡 34 | 纸巾,手帕 35 | 杭州,苏州 36 | 香港,台湾 37 | 首尔,东京 38 | 橙子,橘子 39 | 葡萄,提子 40 | 太监,人妖 41 | 蝴蝶,蜜蜂 42 | 小品,话剧 43 | 裸婚,闪婚 44 | 新年,跨年 45 | 吉他,琵琶 46 | 公交,地铁 47 | 剩女,御姐 48 | 童话,神话 49 | 作家,编剧 50 | 警察,捕快 51 | 结婚,订婚 52 | 奖牌,金牌 53 | 孟飞,乐嘉 54 | 那英,韩红 55 | 面包,蛋糕 56 | 作文,论文 57 | 油条,麻花 58 | 壁纸,贴画 59 | 枕头,抱枕 60 | 手机,座机 61 | 同学,同桌 62 | 婚纱,喜服 63 | 老佛爷,老天爷 64 | 魔术师,魔法师 65 | 鸭舌帽,遮阳帽 66 | 双胞胎,龙凤胎 67 | 情人节,光棍节 68 | 丑小鸭,灰姑娘 69 | 富二代,高富帅 70 | 生活费,零花钱 71 | 麦克风,扩音器 72 | 郭德纲,周立波 73 | 图书馆,图书店 74 | 男朋友,前男友 75 | 洗衣粉,皂角粉 76 | 牛肉干,猪肉脯 77 | 泡泡糖,棒棒糖 78 | 小沈阳,宋小宝 79 | 土豆粉,酸辣粉 80 | 蜘蛛侠,蝙蝠侠 81 | 口香糖,木糖醇 82 | 酸菜鱼,水煮鱼 83 | 小笼包,灌汤包 84 | 薰衣草,满天星 85 | 张韶涵,王心凌 86 | 刘诗诗,刘亦菲 87 | 甄嬛传,红楼梦 88 | 甄子丹,李连杰 89 | 包青天,狄仁杰 90 | 大白兔,金丝猴 91 | 果粒橙,鲜橙多 92 | 沐浴露,沐浴盐 93 | 洗发露,护发素 94 | 自行车,电动车 95 | 班主任,辅导员 96 | 过山车,碰碰车 97 | 铁观音,碧螺春 98 | 十面埋伏,四面楚歌 99 | 成吉思汗,努尔哈赤 100 | 谢娜张杰,邓超孙俪 101 | 福尔摩斯,工藤新一 102 | 贵妃醉酒,黛玉葬花 103 | 流星花园,花样男子 104 | 神雕侠侣,天龙八部 105 | 天天向上,非诚勿扰 106 | 勇往直前,全力以赴 107 | 鱼香肉丝,四喜丸子 108 | 麻婆豆腐,皮蛋豆腐 109 | 语无伦次,词不达意 110 | 鼠目寸光,井底之蛙 111 | 近视眼镜,隐形眼镜 112 | 美人心计,倾世皇妃 113 | 夏家三千金,爱情睡醒了 114 | 降龙十八掌,九阴白骨爪 115 | 红烧牛肉面,香辣牛肉面 116 | 江南style,最炫民族风 117 | 梁山伯与祝英台,罗密欧与朱丽叶 -------------------------------------------------------------------------------- /weixin2py/plugin/setting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | #weilib/plugin/acitvity.py - activity plugin for weilib 4 | #ver 0.1 by winkidney 2014.05.10 5 | from . import activity 6 | 7 | #将在每个response返回之前运行,用于修改response内容。 8 | 9 | 10 | plugin_text = (activity, 11 | ) -------------------------------------------------------------------------------- /weixin2py/plugin/who_the_spy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | #weilib/plugin/who_the_spy.py - 'Who is the spy' plugin for weilib 4 | #ver 0.1 by winkidney 2014.05.10 5 | 6 | from WeiLib.lib import WeiSession 7 | import re 8 | 9 | 10 | def processor(recv_msg): 11 | """A processor must return a dict. 12 | If not ,program will throw the returned result. 13 | """ 14 | 15 | if '谁是卧底' in recv_msg.content: 16 | if re.search('/d'): 17 | session = WeiSession() 18 | if session.get_key('created') == 'yes': 19 | return session.get_key('') 20 | recv_msg.content 21 | return {'test_plugin': 'only_the test plugin output'} -------------------------------------------------------------------------------- /weixin2py/routers.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | import re 4 | from .models import (PatternT2T, PatternT2PT, PatternE2PT, PatternE2T) 5 | from .utils import text_response, pic_text_response, PTItem 6 | 7 | 8 | def base_router(recv_msg, router_patterns): 9 | for type, key, handler in router_patterns: 10 | if recv_msg.msg_type == 'text': 11 | match = re.search(key, recv_msg.content) 12 | if match: 13 | return handler(recv_msg) 14 | elif recv_msg.msg_type == type: 15 | return handler(recv_msg) 16 | 17 | # return default_handler(recv_msg) 18 | 19 | 20 | def new_msg_from_db(pattern): 21 | """Convert PicTextMsg from db to PicTextMsg object for rendering""" 22 | items = [] 23 | for item in pattern.handler.all(): 24 | items.append( 25 | PTItem(item.title, item.description, item.pic_url, item.url)) 26 | return items 27 | 28 | 29 | def db_router(recv_msg, *args): 30 | if recv_msg.msg_type == 'text': 31 | for pattern in PatternT2T.objects.all(): 32 | match = re.search( 33 | pattern.content.encode('utf-8'), recv_msg.content) 34 | if match: 35 | return text_response(recv_msg, pattern.handler.content) 36 | 37 | for pattern in PatternT2PT.objects.all(): 38 | match = re.search( 39 | pattern.content.encode('utf-8'), recv_msg.content) 40 | if match: 41 | return pic_text_response(recv_msg, new_msg_from_db(pattern)) 42 | 43 | if recv_msg.msg_type == 'event': 44 | for pattern in PatternE2T.objects.all(): 45 | 46 | if pattern.event == recv_msg.event: 47 | if recv_msg.event_key: 48 | match = re.search( 49 | pattern.event_key.encode('utf-8'), recv_msg.event_key) 50 | if match: 51 | return text_response(recv_msg, pattern.handler.content) 52 | else: 53 | return text_response(recv_msg, pattern.handler.content) 54 | 55 | for pattern in PatternE2PT.objects.all(): 56 | if pattern.event == recv_msg.event: 57 | if recv_msg.event_key: 58 | match = re.search( 59 | pattern.event_key.encode('utf-8'), recv_msg.event_key) 60 | if match: 61 | return pic_text_response(recv_msg, new_msg_from_db(pattern)) 62 | else: 63 | return pic_text_response(recv_msg, new_msg_from_db(pattern)) 64 | -------------------------------------------------------------------------------- /weixin2py/sample_msg.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | # weilib/sample_msg.py - contains current weichat msg example 3 | # and some other info. 4 | msg_send_kefu = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN" 5 | msg_send_kefu_resp = """ 6 | { 7 | "type":"news", 8 | "media_id":"CsEf3ldqkAYJAU6EJeIkStVDSvffUJ54vqbThMgplD-VJXXof6ctX5fI6-aYyUiQ", 9 | "created_at":1391857799 10 | } 11 | """ 12 | multi_msg_send = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKEN" 13 | 14 | recv_msg_text = """ 15 | 16 | 17 | 1348831860 18 | 19 | 20 | 1234567890123456 21 | """ 22 | 23 | send_msg_text= """ 24 | 25 | 26 | 1399455419 27 | 28 | 29 | 1234567890123456 30 | 31 | """ 32 | recv_msg_image = """ 33 | 34 | 35 | 1348831860 36 | 37 | 38 | 39 | 1234567890123456 40 | 41 | """ 42 | send_msg_image = """ 43 | 44 | 45 | 1348831860 46 | 47 | 48 | 1234567890123456 49 | 50 | """ 51 | recv_msg_voice = """ 52 | 53 | 54 | 1357290913 55 | 56 | 57 | 58 | 1234567890123456 59 | 60 | """ 61 | 62 | recv_msg_video = """ 63 | 64 | 65 | 1357290913 66 | 67 | 68 | 69 | 1234567890123456 70 | 71 | """ 72 | 73 | recv_msg_location = """ 74 | 75 | 76 | 1351776360 77 | 78 | 23.134521 79 | 113.358803 80 | 20 81 | 82 | 1234567890123456 83 | 84 | """ 85 | 86 | recv_msg_link = """ 87 | 88 | 89 | 1351776360 90 | 91 | <![CDATA[公众平台官网链接]]> 92 | 93 | 94 | 1234567890123456 95 | 96 | """ 97 | recv_msg_event = """ 98 | 99 | 100 | 123456789 101 | 102 | 103 | 104 | """ 105 | event_type = {'subscribe':'', 106 | 'unsubscribe': '', 107 | 'CLICK' : '', 108 | } 109 | 110 | recv_msg_scan = """ 111 | 112 | 113 | 123456789 114 | 115 | 116 | 117 | 118 | """ 119 | 120 | msg_scene_subscribe = """ 121 | 122 | 123 | 123456789 124 | 125 | 126 | 127 | 128 | """ 129 | 130 | recv_msg_reloc = """ 131 | 132 | 133 | 123456789 134 | 135 | 136 | 23.137466 137 | 113.352425 138 | 119.385040 139 | 140 | """ 141 | 142 | menu_template = """ 143 | { 144 | "button":[ 145 | { 146 | "type":"click", 147 | "name":"今日歌曲", 148 | "key":"V1001_TODAY_MUSIC" 149 | }, 150 | { 151 | "type":"click", 152 | "name":"歌手简介", 153 | "key":"V1001_TODAY_SINGER" 154 | }, 155 | { 156 | "name":"菜单", 157 | "sub_button":[ 158 | { 159 | "type":"view", 160 | "name":"搜索", 161 | "url":"http://www.soso.com/" 162 | }, 163 | { 164 | "type":"view", 165 | "name":"视频", 166 | "url":"http://v.qq.com/" 167 | }, 168 | { 169 | "type":"click", 170 | "name":"赞一下我们", 171 | "key":"V1001_GOOD" 172 | }] 173 | }] 174 | } 175 | """ -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_base.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ msg.create_time }} 5 | {% block ext %} 6 | 7 | 8 | 0 9 | {% endblock %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_img.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | {% block ext %} 3 | 4 | 5 | 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_music.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | {% block ext %} 3 | 4 | 5 | <![CDATA[{{ msg.title }}]]> 6 | 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_pic_text.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | {% block ext %} 3 | 4 | {{ msg.article_count }} 5 | 6 | {%for item in msg.articles %} 7 | 8 | <![CDATA[{{ item.title }}]]> 9 | 10 | 11 | 12 | 13 | {% endfor %} 14 | 15 | 1 16 | {% endblock %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_text.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | 3 | {% block ext %} 4 | 5 | 6 | 0 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_video.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | {% block ext %} 3 | 4 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /weixin2py/templates/response/msg_voice.xml: -------------------------------------------------------------------------------- 1 | {% extends "response/msg_base.xml" %} 2 | {% block ext %} 3 | 4 | 5 | 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /weixin2py/templates/send/menu_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "button":[ 3 | {% for menu in menu_list %} 4 | { 5 | "name":"{{ menu.name }}", 6 | {% if not menu.sub_buttons %} 7 | "type":"{{ menu.type }}", 8 | {% if menu.type == 'view' %} 9 | "url":"{{ menu.url }}", 10 | {% endif %} 11 | {% if menu.type == 'click' %} 12 | "key":"{{ menu.key }}", 13 | {% endif %} 14 | {% else %} 15 | "sub_button":[ 16 | {% for button in menu.sub_buttons %} 17 | { 18 | "name":"{{ button.name }}", 19 | {% if button.type == 'view' %} 20 | "url":"{{ button.url }}", 21 | {% endif %} 22 | {% if button.type == 'click' %} 23 | "key":"{{ button.key }}", 24 | {% endif %} 25 | "type":"{{ button.type }}" 26 | }{% if not forloop.last %},{% endif %} 27 | {% endfor %} 28 | ] 29 | {% endif %} 30 | }{% if not forloop.last %},{% endif %} 31 | {% endfor %} 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_music.json: -------------------------------------------------------------------------------- 1 | { 2 | "touser":"OPENID", 3 | "msgtype":"music", 4 | "music": 5 | { 6 | "title":"MUSIC_TITLE", 7 | "description":"MUSIC_DESCRIPTION", 8 | "musicurl":"MUSIC_URL", 9 | "hqmusicurl":"HQ_MUSIC_URL", 10 | "thumb_media_id":"THUMB_MEDIA_ID" 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_pic.json: -------------------------------------------------------------------------------- 1 | { 2 | "touser":"OPENID", 3 | "msgtype":"image", 4 | "image": 5 | { 6 | "media_id":"MEDIA_ID" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_text.json: -------------------------------------------------------------------------------- 1 | { 2 | "touser":"OPENID", 3 | "msgtype":"text", 4 | "text": 5 | { 6 | "content":"Hello World" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_text_pic.json: -------------------------------------------------------------------------------- 1 | { 2 | "touser":"OPENID", 3 | "msgtype":"news", 4 | "news":{ 5 | "articles": [ 6 | { 7 | "title":"Happy Day", 8 | "description":"Is Really A Happy Day", 9 | "url":"URL", 10 | "picurl":"PIC_URL" 11 | }, 12 | { 13 | "title":"Happy Day", 14 | "description":"Is Really A Happy Day", 15 | "url":"URL", 16 | "picurl":"PIC_URL" 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_video.json: -------------------------------------------------------------------------------- 1 | { 2 | "touser":"OPENID", 3 | "msgtype":"video", 4 | "video": 5 | { 6 | "media_id":"MEDIA_ID", 7 | "title":"TITLE", 8 | "description":"DESCRIPTION" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /weixin2py/templates/send/msg_voice.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /weixin2py/templates/send/multi_msg_send.json: -------------------------------------------------------------------------------- 1 | { 2 | "articles": [ 3 | { 4 | "thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p", 5 | "author":"xxx", 6 | "title":"Happy Day", 7 | "content_source_url":"www.qq.com", 8 | "content":"content", 9 | "digest":"digest" 10 | }, 11 | { 12 | "thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p", 13 | "author":"xxx", 14 | "title":"Happy Day", 15 | "content_source_url":"www.qq.com", 16 | "content":"content", 17 | "digest":"digest" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /weixin2py/utils.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import re 3 | import urllib2 4 | from django.shortcuts import render_to_response 5 | from django.template.loader import render_to_string 6 | from django.template import Context, Template 7 | from django.core.cache import cache 8 | from . import TextMsg, ImgMsg, PicTextMsg, PTItem 9 | 10 | try: 11 | import cPickle as pickle 12 | except ImportError: 13 | import pickle 14 | 15 | from .plugin.setting import plugin_text as PLUGINS 16 | 17 | DEFAULT_TIMEOUT = 15 * 60 18 | 19 | 20 | class WeiSession(object): 21 | 22 | """ Helper Class to store info by session ID(Default: OpenID), 23 | Because of its usage of pickle, some type of data can't be stored correctly. 24 | """ 25 | 26 | def __init__(self, session_id): 27 | if not isinstance(session_id, (str, unicode, int)): 28 | raise TypeError("Argument openid [%s] must be a str/unicode/int object!") 29 | self.session_id = session_id 30 | self._get_session() 31 | 32 | def _get_session(self): 33 | session = cache.get(self.session_id) 34 | if not session: 35 | self.session = {} 36 | else: 37 | self.session = pickle.loads(session) 38 | 39 | def _save(self): 40 | session_storage = pickle.dumps(self.session) 41 | cache.set(self.session_id, session_storage, DEFAULT_TIMEOUT) 42 | 43 | def set_key(self, key, value): 44 | self.session[key] = value 45 | self._save() 46 | 47 | def get_key(self, key): 48 | return self.session.get(key) 49 | 50 | 51 | def create_menu(access_token, menu_list): 52 | """ 53 | Create WeiChat menu in WeiChat Client. 54 | :param access_token: access_token str 55 | :param menu_list: 56 | :return: 57 | """ 58 | url = """ https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s""" \ 59 | % access_token 60 | data = render_to_string('send/menu_create.json', {'menu_list': menu_list}) 61 | 62 | encoded_data = data.encode('utf-8') 63 | try: 64 | result = urllib2.urlopen(url, encoded_data, 20).read() 65 | except urllib2.URLError, urllib2.HTTPError: 66 | return False 67 | if result: 68 | if re.findall('ok', result): 69 | return True 70 | else: 71 | return False 72 | 73 | 74 | def render_from_string(string, data): 75 | t = Template(string) 76 | return t.render(Context(data)) 77 | 78 | 79 | def text_response(recv_msg, content): 80 | msg = TextMsg( 81 | recv_msg.to_user_name, recv_msg.from_user_name, recv_msg.create_time) 82 | plugin_dict = {} 83 | for plugin in PLUGINS: 84 | result = plugin.processor(recv_msg) 85 | if isinstance(result, dict): 86 | plugin_dict.update(result) 87 | msg.make_msg(render_from_string(content, plugin_dict)) 88 | return render_to_response('response/msg_text.xml', 89 | {'msg': msg, } 90 | ) 91 | 92 | 93 | def image_response(recv_msg, media_id): 94 | msg = ImgMsg( 95 | recv_msg.to_user_name, recv_msg.from_user_name, recv_msg.create_time) 96 | msg.make_msg(media_id) 97 | return render_to_response('response/msg_text.xml', 98 | {'msg': msg, } 99 | ) 100 | 101 | 102 | def pic_text_response(recv_msg, msg_item): 103 | msg = PicTextMsg(recv_msg.to_user_name, recv_msg.from_user_name, recv_msg.create_time) 104 | if isinstance(msg_item, PTItem): 105 | article_count = 1 106 | msg.new_item( 107 | msg_item.title, msg_item.description, msg_item.pic_url, msg_item.url) 108 | elif isinstance(msg_item, list): 109 | article_count = len(msg_item) 110 | for item in msg_item: 111 | msg.new_item(item.title, item.description, item.pic_url, item.url) 112 | else: 113 | raise ValueError("msg_item must be instance of list or PTItem.") 114 | msg.make_msg(article_count) 115 | return render_to_response('response/msg_pic_text.xml', 116 | {'msg': msg, } 117 | ) 118 | --------------------------------------------------------------------------------