├── .gitignore ├── LICENSE ├── Pipfile ├── README.md ├── app.py ├── manage.py ├── momo ├── __init__.py ├── app.py ├── helper.py ├── media.py ├── models │ ├── __init__.py │ ├── account.py │ ├── bill.py │ └── wx_response.py ├── note.py ├── note_imgs │ ├── SourceHanSansSC-Regular.otf │ ├── note_body.png │ ├── note_body_660.png │ ├── note_footer.png │ ├── note_footer_660.png │ ├── note_header.png │ └── note_header_660.png ├── settings.py ├── tuling_trainer.py └── views │ ├── __init__.py │ ├── hello.py │ └── mweixin.py ├── requirements.txt ├── start.sh └── supervisord.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | *.py[cod] 3 | *.swp 4 | 5 | .DS_Store 6 | 7 | # build 8 | .gitconfig 9 | .qiniu_pythonsdk_hostscache.json 10 | .installed.cfg 11 | .ropeproject/ 12 | ./momo/ropeproject/ 13 | ./__pycache__/ 14 | ./momo/__pycache__/ 15 | .idea/ 16 | bin/ 17 | parts/ 18 | eggs/ 19 | build/ 20 | dist/ 21 | access.log* 22 | error.log* 23 | newrelic.ini 24 | 25 | local_settings.py 26 | 27 | # test 28 | test_example.py 29 | test_settings.py 30 | 31 | Pipfile.lock 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "http://mirrors.tencentyun.com/pypi/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | six = "*" 10 | lxml = "*" 11 | qiniu = "*" 12 | xmltodict = "*" 13 | jsonschema = "*" 14 | pycrypto = "*" 15 | chatterbot = "*" 16 | python-weixin = "*" 17 | "cos-python-sdk-v5" = "*" 18 | Sanic = "*" 19 | PyYAML = "*" 20 | Pillow = "*" 21 | uvloop = "*" 22 | 23 | [requires] 24 | python_version = "3.7" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # momo 2 | 微信记账助手 3 | 4 | 5 | # 测试使用请关注微信公号(hiiiapril)测试 6 | 7 | ![](http://media.gusibi.mobi/Hy8XHexmzppNKuekLuGxWy8LjdGrQAzZA3mH_e9xltoiYgTFWdvlpZwGWxZESrbK) 8 | 9 | ## 实现功能 10 | 11 | * 支持 PM2.5 查询(例如:pm25 北京) 12 | * 微信聊天机器人 13 | * 支持训练 14 | * 特定用户发送图片返回七牛地址 15 | * 支持记账 16 | * 支持用户名查询 17 | 18 | ## TODO 19 | 20 | * 对记账结果统计 21 | 22 | ## 安装& 使用 23 | 24 | ### 获取代码 25 | 26 | 27 | ``` 28 | git clone git@github.com:gusibi/momo.git 29 | git co -b chatterbot 30 | git pull origin chatterbot 31 | 32 | ``` 33 | 34 | ### 安装依赖 35 | 36 | ``` 37 | pip install -r requirements.txt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com 38 | ``` 39 | 40 | ### 运行 41 | 42 | ``` 43 | python manage.py 44 | ``` 45 | 46 | ## spuervisord 启动命令 47 | 48 | ``` 49 | supervisord -c supervisor.conf 通过配置文件启动supervisor 50 | supervisorctl -c supervisor.conf status 察看supervisor的状态 51 | supervisorctl -c supervisor.conf reload 重新载入 配置文件 52 | supervisorctl -c supervisor.conf start [all]|[appname] 启动指定/所有 supervisor管理的程序进程 53 | supervisorctl -c supervisor.conf stop [all]|[appname] 关闭指定/所有 supervisor管理的程序进程 54 | ``` 55 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | import uvloop 5 | from sanic import Sanic 6 | 7 | from momo.settings import Config 8 | 9 | 10 | blueprints = [] 11 | 12 | 13 | def create_app(register_bp=True, test=False): 14 | app = Sanic(__name__) 15 | if test: 16 | app.config['TESTING'] = True 17 | app.config.from_object(Config) 18 | register_blueprints(app) 19 | return app 20 | 21 | 22 | def register_extensions(app): 23 | pass 24 | 25 | 26 | def register_blueprints(app): 27 | from momo.views.hello import blueprint as hello_bp 28 | from momo.views.mweixin import blueprint as wx_bp 29 | app.register_blueprint(hello_bp) 30 | app.register_blueprint(wx_bp) 31 | 32 | 33 | def register_jinja_funcs(app): 34 | # funcs = dict() 35 | return app 36 | 37 | app = create_app() 38 | asyncio.set_event_loop(uvloop.new_event_loop()) 39 | server = app.create_server(host="0.0.0.0", port=8888, debug=True) 40 | loop = asyncio.get_event_loop() 41 | task = asyncio.ensure_future(server) 42 | try: 43 | loop.run_forever() 44 | except: 45 | loop.stop() 46 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import asyncio 4 | from signal import signal, SIGINT 5 | 6 | import uvloop 7 | 8 | from momo.app import create_app 9 | 10 | app = create_app() 11 | 12 | 13 | def run(): 14 | asyncio.set_event_loop(uvloop.new_event_loop()) 15 | server = app.create_server(host="0.0.0.0", port=8888) 16 | loop = asyncio.get_event_loop() 17 | asyncio.ensure_future(server) 18 | signal(SIGINT, lambda s, f: loop.stop()) 19 | try: 20 | loop.run_forever() 21 | except: 22 | loop.stop() 23 | 24 | 25 | if __name__ == '__main__': 26 | run() 27 | -------------------------------------------------------------------------------- /momo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/__init__.py -------------------------------------------------------------------------------- /momo/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sanic import Sanic 3 | 4 | from momo.settings import Config 5 | 6 | 7 | blueprints = [] 8 | 9 | 10 | def create_app(register_bp=True, test=False): 11 | app = Sanic(__name__) 12 | if test: 13 | app.config['TESTING'] = True 14 | app.config.from_object(Config) 15 | register_blueprints(app) 16 | return app 17 | 18 | 19 | def register_extensions(app): 20 | pass 21 | 22 | 23 | def register_blueprints(app): 24 | from momo.views.hello import blueprint as hello_bp 25 | from momo.views.mweixin import blueprint as wx_bp 26 | app.register_blueprint(hello_bp) 27 | app.register_blueprint(wx_bp) 28 | 29 | 30 | def register_jinja_funcs(app): 31 | return app 32 | -------------------------------------------------------------------------------- /momo/helper.py: -------------------------------------------------------------------------------- 1 | # -*-coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | """ 5 | File: client.py 6 | Author: goodspeed 7 | Email: cacique1103@gmail.com 8 | Github: https://github.com/zongxiao 9 | Date: 2015-02-11 10 | Description: Weixin helpers 11 | """ 12 | 13 | import sys 14 | import time 15 | import logging 16 | import datetime 17 | 18 | try: 19 | import cPickle as pickle 20 | except ImportError: 21 | import pickle 22 | from functools import wraps 23 | from hashlib import sha1 24 | from decimal import Decimal 25 | 26 | from chatterbot import ChatBot 27 | from chatterbot.trainers import ListTrainer 28 | from chatterbot.response_selection import get_random_response 29 | 30 | import six 31 | 32 | from momo.settings import Config 33 | 34 | PY2 = sys.version_info[0] == 2 35 | 36 | _always_safe = (b'abcdefghijklmnopqrstuvwxyz' 37 | b'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-+') 38 | 39 | 40 | error_dict = { 41 | 'AppID 参数错误': { 42 | 'errcode': 40013, 43 | 'errmsg': 'invalid appid' 44 | } 45 | } 46 | 47 | 48 | if PY2: 49 | text_type = unicode 50 | iteritems = lambda d, *args, **kwargs: d.iteritems(*args, **kwargs) 51 | 52 | def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): 53 | if x is None or isinstance(x, str): 54 | return x 55 | return x.encode(charset, errors) 56 | else: 57 | text_type = str 58 | iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) 59 | 60 | def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): 61 | if x is None or isinstance(x, str): 62 | return x 63 | return x.decode(charset, errors) 64 | 65 | 66 | """ 67 | The md5 and sha modules are deprecated since Python 2.5, replaced by the 68 | hashlib module containing both hash algorithms. Here, we provide a common 69 | interface to the md5 and sha constructors, preferring the hashlib module when 70 | available. 71 | """ 72 | 73 | try: 74 | import hashlib 75 | md5_constructor = hashlib.md5 76 | md5_hmac = md5_constructor 77 | sha_constructor = hashlib.sha1 78 | sha_hmac = sha_constructor 79 | except ImportError: 80 | import md5 81 | md5_constructor = md5.new 82 | md5_hmac = md5 83 | import sha 84 | sha_constructor = sha.new 85 | sha_hmac = sha 86 | 87 | 88 | momo_chat = ChatBot( 89 | 'Momo', 90 | storage_adapter='chatterbot.storage.MongoDatabaseAdapter', 91 | # response_selection_method=get_random_response, 92 | logic_adapters=[ 93 | "chatterbot.logic.BestMatch", 94 | "chatterbot.logic.MathematicalEvaluation", 95 | # "chatterbot.logic.TimeLogicAdapter", 96 | ], 97 | input_adapter='chatterbot.input.VariableInputTypeAdapter', 98 | output_adapter='chatterbot.output.OutputAdapter', 99 | database_uri=Config.MONGO_MASTER_URL, 100 | database='chatterbot', 101 | read_only=True 102 | ) 103 | 104 | 105 | def get_momo_answer(content): 106 | try: 107 | response = momo_chat.get_response(content) 108 | except: 109 | return content 110 | if isinstance(response, str): 111 | return response 112 | return response.text 113 | 114 | 115 | def set_momo_answer(conversation): 116 | momo_chat.set_trainer(ListTrainer) 117 | momo_chat.train(conversation) 118 | 119 | 120 | log_format = '>' * 10 + "%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s" 121 | logging.basicConfig(format=log_format) 122 | logger = logging.getLogger() 123 | logger.setLevel(logging.DEBUG) 124 | 125 | 126 | def timeit(fn): 127 | 128 | @wraps(fn) 129 | def real_fn(*args, **kwargs): 130 | _start = time.time() 131 | result = fn(*args, **kwargs) 132 | _end = time.time() 133 | _last = _end - _start 134 | logger.debug('End timeit for %s in %s seconds.' % 135 | (fn.__name__, _last)) 136 | return result 137 | 138 | return real_fn 139 | 140 | 141 | try: 142 | from line_profiler import LineProfiler 143 | except: 144 | class LineProfiler(): 145 | def __call__(self, func): 146 | return func 147 | 148 | def print_stats(self): 149 | pass 150 | 151 | 152 | ln_profile = LineProfiler() 153 | 154 | 155 | def lprofile(fn): 156 | ''' 157 | 用line_profiler输出代码行时间统计信息 158 | ''' 159 | fn = ln_profile(fn) 160 | 161 | @wraps(fn) 162 | def _fn(*args, **kwargs): 163 | result = fn(*args, **kwargs) 164 | print('>' * 10) 165 | ln_profile.print_stats() 166 | return result 167 | return _fn 168 | 169 | 170 | class Promise(object): 171 | """ 172 | This is just a base class for the proxy class created in 173 | the closure of the lazy function. It can be used to recognize 174 | promises in code. 175 | """ 176 | pass 177 | 178 | 179 | class _UnicodeDecodeError(UnicodeDecodeError): 180 | def __init__(self, obj, *args): 181 | self.obj = obj 182 | UnicodeDecodeError.__init__(self, *args) 183 | 184 | def __str__(self): 185 | original = UnicodeDecodeError.__str__(self) 186 | return '%s. You passed in %r (%s)' % (original, self.obj, 187 | type(self.obj)) 188 | 189 | 190 | def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'): 191 | """ 192 | Returns a text object representing 's' -- unicode on Python 2 and str on 193 | Python 3. Treats bytestrings using the 'encoding' codec. 194 | If strings_only is True, don't convert (some) non-string-like objects. 195 | """ 196 | if isinstance(s, Promise): 197 | # The input is the result of a gettext_lazy() call. 198 | return s 199 | return force_text(s, encoding, strings_only, errors) 200 | 201 | 202 | _PROTECTED_TYPES = six.integer_types + (type(None), float, Decimal, 203 | datetime.datetime, datetime.date, 204 | datetime.time) 205 | 206 | 207 | def is_protected_type(obj): 208 | """Determine if the object instance is of a protected type. 209 | Objects of protected types are preserved as-is when passed to 210 | force_text(strings_only=True). 211 | """ 212 | return isinstance(obj, _PROTECTED_TYPES) 213 | 214 | 215 | def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): 216 | """ 217 | Similar to smart_text, except that lazy instances are resolved to 218 | strings, rather than kept as lazy objects. 219 | If strings_only is True, don't convert (some) non-string-like objects. 220 | """ 221 | # Handle the common case first for performance reasons. 222 | if issubclass(type(s), six.text_type): 223 | return s 224 | if strings_only and is_protected_type(s): 225 | return s 226 | try: 227 | if not issubclass(type(s), six.string_types): 228 | if six.PY3: 229 | if isinstance(s, bytes): 230 | s = six.text_type(s, encoding, errors) 231 | else: 232 | s = six.text_type(s) 233 | elif hasattr(s, '__unicode__'): 234 | s = six.text_type(s) 235 | else: 236 | s = six.text_type(bytes(s), encoding, errors) 237 | else: 238 | # Note: We use .decode() here, instead of six.text_type(s, encoding, 239 | # errors), so that if s is a SafeBytes, it ends up being a 240 | # SafeText at the end. 241 | s = s.decode(encoding, errors) 242 | except UnicodeDecodeError as e: 243 | if not isinstance(s, Exception): 244 | raise _UnicodeDecodeError(s, *e.args) 245 | else: 246 | # If we get to here, the caller has passed in an Exception 247 | # subclass populated with non-ASCII bytestring data without a 248 | # working unicode method. Try to handle this without raising a 249 | # further exception by individually forcing the exception args 250 | # to unicode. 251 | s = ' '.join(force_text(arg, encoding, strings_only, errors) 252 | for arg in s) 253 | return s 254 | 255 | 256 | def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): 257 | """ 258 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 259 | If strings_only is True, don't convert (some) non-string-like objects. 260 | """ 261 | if isinstance(s, Promise): 262 | # The input is the result of a gettext_lazy() call. 263 | return s 264 | return force_bytes(s, encoding, strings_only, errors) 265 | 266 | 267 | def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): 268 | """ 269 | Similar to smart_bytes, except that lazy instances are resolved to 270 | strings, rather than kept as lazy objects. 271 | If strings_only is True, don't convert (some) non-string-like objects. 272 | """ 273 | # Handle the common case first for performance reasons. 274 | if isinstance(s, bytes): 275 | if encoding == 'utf-8': 276 | return s 277 | else: 278 | return s.decode('utf-8', errors).encode(encoding, errors) 279 | if strings_only and is_protected_type(s): 280 | return s 281 | if isinstance(s, Promise): 282 | return six.text_type(s).encode(encoding, errors) 283 | if not isinstance(s, six.string_types): 284 | try: 285 | if six.PY3: 286 | return six.text_type(s).encode(encoding) 287 | else: 288 | return bytes(s) 289 | except UnicodeEncodeError: 290 | if isinstance(s, Exception): 291 | # An Exception subclass containing non-ASCII data that doesn't 292 | # know how to print itself properly. We shouldn't raise a 293 | # further exception. 294 | return b' '.join(force_bytes(arg, encoding, 295 | strings_only, errors) 296 | for arg in s) 297 | return six.text_type(s).encode(encoding, errors) 298 | else: 299 | return s.encode(encoding, errors) 300 | 301 | if six.PY3: 302 | smart_str = smart_text 303 | force_str = force_text 304 | else: 305 | smart_str = smart_bytes 306 | force_str = force_bytes 307 | # backwards compatibility for Python 2 308 | smart_unicode = smart_text 309 | force_unicode = force_text 310 | 311 | smart_str.__doc__ = """ 312 | Apply smart_text in Python 3 and smart_bytes in Python 2. 313 | This is suitable for writing to sys.stdout (for instance). 314 | """ 315 | 316 | force_str.__doc__ = """ 317 | Apply force_text in Python 3 and force_bytes in Python 2. 318 | """ 319 | 320 | 321 | def genarate_js_signature(params): 322 | keys = params.keys() 323 | keys.sort() 324 | params_str = b'' 325 | for key in keys: 326 | params_str += b'%s=%s&' % (smart_str(key), smart_str(params[key])) 327 | params_str = params_str[:-1] 328 | return sha1(params_str).hexdigest() 329 | 330 | 331 | def genarate_signature(params): 332 | sorted_params = sorted([v for k, v in params.items()]) 333 | params_str = ''.join(sorted_params) 334 | return sha1(params_str).hexdigest() 335 | 336 | 337 | def get_encoding(html=None, headers=None): 338 | try: 339 | import chardet 340 | if html: 341 | encoding = chardet.detect(html).get('encoding') 342 | return encoding 343 | except ImportError: 344 | pass 345 | if headers: 346 | content_type = headers.get('content-type') 347 | try: 348 | encoding = content_type.split(' ')[1].split('=')[1] 349 | return encoding 350 | except IndexError: 351 | pass 352 | 353 | 354 | def iter_multi_items(mapping): 355 | """ 356 | Iterates over the items of a mapping yielding keys and values 357 | without dropping any from more complex structures. 358 | """ 359 | if isinstance(mapping, dict): 360 | for key, value in iteritems(mapping): 361 | if isinstance(value, (tuple, list)): 362 | for value in value: 363 | yield key, value 364 | else: 365 | yield key, value 366 | else: 367 | for item in mapping: 368 | yield item 369 | 370 | 371 | def url_quote(string, charset='utf-8', errors='strict', safe='/:', unsafe=''): 372 | """ 373 | URL encode a single string with a given encoding. 374 | 375 | :param s: the string to quote. 376 | :param charset: the charset to be used. 377 | :param safe: an optional sequence of safe characters. 378 | :param unsafe: an optional sequence of unsafe characters. 379 | 380 | .. versionadded:: 0.9.2 381 | The `unsafe` parameter was added. 382 | """ 383 | if not isinstance(string, (text_type, bytes, bytearray)): 384 | string = text_type(string) 385 | if isinstance(string, text_type): 386 | string = string.encode(charset, errors) 387 | if isinstance(safe, text_type): 388 | safe = safe.encode(charset, errors) 389 | if isinstance(unsafe, text_type): 390 | unsafe = unsafe.encode(charset, errors) 391 | safe = frozenset(bytearray(safe) + _always_safe) - frozenset(bytearray(unsafe)) 392 | rv = bytearray() 393 | for char in bytearray(string): 394 | if char in safe: 395 | rv.append(char) 396 | else: 397 | rv.extend(('%%%02X' % char).encode('ascii')) 398 | return to_native(bytes(rv)) 399 | 400 | 401 | def url_quote_plus(string, charset='utf-8', errors='strict', safe=''): 402 | return url_quote(string, charset, errors, safe + ' ', '+').replace(' ', '+') 403 | 404 | 405 | def _url_encode_impl(obj, charset, encode_keys, sort, key): 406 | iterable = iter_multi_items(obj) 407 | if sort: 408 | iterable = sorted(iterable, key=key) 409 | for key, value in iterable: 410 | if value is None: 411 | continue 412 | if not isinstance(key, bytes): 413 | key = text_type(key).encode(charset) 414 | if not isinstance(value, bytes): 415 | value = text_type(value).encode(charset) 416 | yield url_quote_plus(key) + '=' + url_quote_plus(value) 417 | 418 | 419 | def url_encode(obj, charset='utf-8', encode_keys=False, sort=False, key=None, 420 | separator=b'&'): 421 | separator = to_native(separator, 'ascii') 422 | return separator.join(_url_encode_impl(obj, charset, encode_keys, sort, key)) 423 | 424 | 425 | def validate_xml(xml): 426 | """ 427 | 使用lxml.etree.parse 检测xml是否符合语法规范 428 | """ 429 | from lxml import etree 430 | try: 431 | return etree.parse(xml) 432 | except etree.XMLSyntaxError: 433 | return False 434 | 435 | 436 | def cache_for(duration): 437 | 438 | def deco(func): 439 | @wraps(func) 440 | def fn(*args, **kwargs): 441 | all_args = [] 442 | all_args.append(args) 443 | key = pickle.dumps((all_args, kwargs)) 444 | value, expire = func.__dict__.get(key, (None, None)) 445 | now = int(time.time()) 446 | if value is not None and expire > now: 447 | return value 448 | value = func(*args, **kwargs) 449 | func.__dict__[key] = (value, int(time.time()) + duration) 450 | return value 451 | return fn 452 | 453 | return deco 454 | 455 | 456 | @cache_for(60 * 60) 457 | def get_weixinmp_token(appid, app_secret, is_refresh=False): 458 | from weixin import WeixinMpAPI 459 | from weixin.oauth2 import (ConnectTimeoutError, 460 | ConnectionError, 461 | OAuth2AuthExchangeError) 462 | try: 463 | api = WeixinMpAPI( 464 | appid=appid, app_secret=app_secret, 465 | grant_type='client_credential') 466 | token = api.client_credential_for_access_token().get('access_token') 467 | return token, None 468 | except (OAuth2AuthExchangeError, ConnectTimeoutError, 469 | ConnectionError) as ex: 470 | return None, ex 471 | 472 | 473 | @timeit 474 | def get_weixinmp_media_id(access_token, filepath): 475 | import requests 476 | upload_url = 'https://api.weixin.qq.com/cgi-bin/media/upload' 477 | payload_img = { 478 | 'access_token': access_token, 479 | 'type': 'image' 480 | } 481 | data = {'media': open(filepath, 'rb')} 482 | req = requests.post(url=upload_url, params=payload_img, files=data) 483 | if req.status_code == 200: 484 | info = req.json() 485 | media_id = info.get('media_id', '') 486 | return media_id 487 | return '' 488 | -------------------------------------------------------------------------------- /momo/media.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | try: 5 | from string import ascii_letters as letters, digits 6 | except ImportError: 7 | from string import letters, digits 8 | import random 9 | from hashlib import md5 10 | import urllib 11 | 12 | from qiniu import Auth, BucketManager, put_file 13 | from qcloud_cos import (CosConfig, CosS3Client, 14 | CosServiceError, CosClientError) 15 | 16 | from momo.helper import smart_str 17 | from momo.settings import Config 18 | 19 | 20 | def generate_nonce_str(length=32): 21 | return ''.join(random.SystemRandom().choice( 22 | letters + digits) for _ in range(length)) 23 | 24 | 25 | class QiniuUriGen(): 26 | 27 | __version__ = '1.0' 28 | 29 | def __init__(self, access_key=None, secret_key=None, 30 | time_key=None, host=None): 31 | self.access_key = access_key 32 | self.secret_key = secret_key 33 | self.time_key = time_key 34 | self.host = host 35 | 36 | def url_encode(self, s): 37 | if not s: 38 | return '' 39 | return urllib.quote(smart_str(s), safe="/") 40 | 41 | def t16(self, t): 42 | return hex(t)[2:].lower() 43 | 44 | def to_deadline(self, expires): 45 | return int(time.time()) + int(expires) 46 | 47 | def summd5(self, str): 48 | m = md5() 49 | m.update(str) 50 | return m.hexdigest() 51 | 52 | def sign(self, path, t): 53 | if not path: 54 | return 55 | key = self.time_key 56 | a = key + self.url_encode(path) + t 57 | sign_s = self.summd5(a).lower() 58 | sign_part = "sign=" + sign_s + "&t=" + t 59 | return sign_part 60 | 61 | def sign_download_url(self, path, expires=1800): 62 | deadline = self.to_deadline(expires) 63 | sign_part = self.sign('/%s' % path, self.t16(deadline)) 64 | return '%s/%s?' % (self.host, path) + sign_part 65 | 66 | 67 | def qiniu_sign_url(hash_key, expires=1800): 68 | config = Config.QINIU_AUDIOS_CONFIG 69 | url = QiniuUriGen(**config).sign_download_url(hash_key, expires) 70 | return url 71 | 72 | 73 | def media_fetch_to_qiniu(media_url, media_id): 74 | '''抓取url的资源存储在库''' 75 | auth = qiniu_auth() 76 | bucket = BucketManager(auth) 77 | bucket_name = Config.QINIU_BUCKET 78 | ret, info = bucket.fetch(media_url, bucket_name, media_id) 79 | if info.status_code == 200: 80 | return True, media_id 81 | return False, None 82 | 83 | 84 | def qiniu_auth(): 85 | access_key = str(Config.QINIU_ACCESS_TOKEN) 86 | secret_key = str(Config.QINIU_SECRET_TOKEN) 87 | auth = Auth(access_key, secret_key) 88 | return auth 89 | 90 | 91 | def get_qiniu_token(key=None, bucket_name=None): 92 | q = qiniu_auth() 93 | bucket_name = bucket_name or Config.QINIU_BUCKET 94 | token = q.upload_token(bucket_name, key, 3600) 95 | return token 96 | 97 | 98 | def upload_file_to_qiniu(file_path, key=None, **kwargs): 99 | bucket_name = kwargs.pop('bucket_name', None) 100 | token = get_qiniu_token(key, bucket_name=bucket_name) 101 | ret, info = put_file(token, key, file_path, **kwargs) 102 | return ret, info 103 | 104 | 105 | def get_cos_client(secret_id=None, secret_key=None, 106 | region=None, token=''): 107 | # 设置用户属性, 包括secret_id, secret_key, region 108 | secret_id = secret_id or Config.QCOS_SECRET_ID 109 | secret_key = secret_key or Config.QCOS_SECRET_KEY 110 | region = region or Config.QCOS_REGION 111 | token = '' # 使用临时秘钥需要传入Token,默认为空,可不填 112 | config = CosConfig(Region=region, Secret_id=secret_id, 113 | Secret_key=secret_key, Token=token) # 获取配置对象 114 | client = CosS3Client(config) 115 | return client 116 | 117 | 118 | def upload_file_to_qcos(filepath, file_name, appid=None, bucket='note'): 119 | # 本地路径 简单上传 120 | appid = appid or Config.QCOS_APPID 121 | bucket = '%s-%s' % (bucket, appid) 122 | client = get_cos_client() 123 | response = client.put_object_from_local_file( 124 | Bucket=bucket, 125 | LocalFilePath=filepath, 126 | Key=file_name, 127 | ) 128 | return response 129 | -------------------------------------------------------------------------------- /momo/models/__init__.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | import copy 3 | 4 | from six import with_metaclass 5 | from pymongo import MongoClient 6 | from pymongo import ReturnDocument 7 | from momo.settings import Config 8 | 9 | pyclient = MongoClient(Config.MONGO_MASTER_URL) 10 | 11 | 12 | class ModelMetaclass(type): 13 | """ 14 | Metaclass of the Model. 15 | """ 16 | __collection__ = None 17 | 18 | def __init__(cls, name, bases, attrs): 19 | super(ModelMetaclass, cls).__init__(name, bases, attrs) 20 | cls.db = pyclient['momo_bill'] 21 | if cls.__collection__: 22 | cls.collection = cls.db[cls.__collection__] 23 | 24 | 25 | class Model(with_metaclass(ModelMetaclass, object)): 26 | 27 | ''' 28 | Model 29 | ''' 30 | 31 | __collection__ = 'model_base' 32 | 33 | @classmethod 34 | def get(cls, _id=None, **kwargs): 35 | if _id: 36 | doc = cls.collection.find_one({'_id': _id}) 37 | else: 38 | doc = cls.collection.find_one(kwargs) 39 | return doc 40 | 41 | @classmethod 42 | def find(cls, filter=None, projection=None, skip=0, limit=20, **kwargs): 43 | docs = cls.collection.find(filter=filter, 44 | projection=projection, 45 | skip=skip, limit=limit, 46 | **kwargs) 47 | return docs 48 | 49 | @classmethod 50 | def insert(cls, **kwargs): 51 | doc = cls.collection.insert_one(kwargs) 52 | return doc 53 | 54 | @classmethod 55 | def update_or_insert(cls, fields=None, **kwargs): 56 | ''' 57 | :param fields: list filter fields 58 | :param kwargs: update fields 59 | :return: 60 | ''' 61 | if fields: 62 | filters = {field: kwargs[field] for field in fields if kwargs.get(field)} 63 | doc = cls.collection.find_one_and_update( 64 | filters, kwargs, upsert=True) 65 | # filters, kwargs, return_document=ReturnDocument.AFTER, upsert=True) 66 | else: 67 | doc = cls.collection.insert_one(kwargs) 68 | return doc 69 | 70 | 71 | @classmethod 72 | def bulk_inserts(cls, *params): 73 | ''' 74 | :param params: document list 75 | :return: 76 | ''' 77 | results = cls.collection.insert_many(params) 78 | return results 79 | 80 | @classmethod 81 | def update_one(cls, filter, **kwargs): 82 | result = cls.collection.update_one(filter, **kwargs) 83 | return result 84 | 85 | @classmethod 86 | def update_many(cls, filter, **kwargs): 87 | results = cls.collection.update_many(filter, **kwargs) 88 | return results 89 | 90 | @classmethod 91 | def delete_one(cls, **filter): 92 | cls.collection.delete_one(filter) 93 | 94 | @classmethod 95 | def delete_many(cls, **filter): 96 | cls.collection.delete_many(filter) 97 | -------------------------------------------------------------------------------- /momo/models/account.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from momo.models import Model 5 | 6 | 7 | class Account(Model): 8 | 9 | ''' 10 | :param _id: '用户ID', 11 | :param nickname: '用户昵称', 12 | :param username: '用户名 用于登录', 13 | :param avatar: '头像', 14 | :param password: '密码', 15 | :param created_time: '创建时间', 16 | ''' 17 | __collection__ = 'account' 18 | __default_fields__ = { 19 | 'created_time': datetime.utcnow() 20 | } 21 | 22 | 23 | class AccountWorkflow(Model): 24 | 25 | __collection__ = 'account_workflow' 26 | 27 | ''' 28 | _id: id 29 | uid: uid 30 | workflow: workflow name 31 | next: workflow next 32 | ''' 33 | 34 | __default_fields__ = { 35 | 'created_time': datetime.utcnow() 36 | } 37 | 38 | 39 | 40 | class UserWorkFlow(object): 41 | 42 | name = 'user_setting' 43 | 44 | actions = { 45 | 'name_query': { 46 | 'new_uid': { 47 | 'value': '竟然没有设置用户名,来设置一下!输入"就不"取消设置!', 48 | 'next': 'input_username', 49 | }, 50 | 'old_uid': { 51 | 'value': '您的用户名为:%s', 52 | 'next': 'done' 53 | } 54 | }, 55 | 'input_username': { 56 | 'value': '设置成功,您的用户名为:%s,忘记用户名可以输入"用户名"查询', 57 | 'next': 'done' 58 | }, 59 | 'give_up': { 60 | 'value': '好吧你说了算!', 61 | 'next': 'give_up' 62 | } 63 | } 64 | 65 | def __init__(self, uid, word, wxkw=None, aw=None): 66 | self.uid = uid 67 | self.word = word 68 | self.wxkw = wxkw # weixin key word instance 69 | self.aw = aw 70 | 71 | def process_name_query(self): 72 | account = Account.get(_id=self.uid) or {} 73 | username = account.get('username') 74 | action = self.actions['name_query'] 75 | if not account or not username: 76 | status = 'new_uid' 77 | value = action[status]['value'] 78 | else: 79 | status = 'old_uid' 80 | value = action[status]['value'] % username 81 | return {'next': action[status]['next']}, value 82 | 83 | def process_input_username(self): 84 | username = self.word 85 | account = Account.get(username=username) 86 | if account: 87 | return {'next': 'input_username'}, '用户名已被使用,请重新输入' 88 | Account.update_or_insert(fields=['_id'], 89 | _id=self.uid, 90 | username=username, 91 | created_time=datetime.utcnow()) 92 | action = self.actions['input_username'] 93 | value = action['value'] % username 94 | return {'next': action['next']}, value 95 | 96 | def process_give_up(self): 97 | action = self.actions['give_up'] 98 | aw = AccountWorkflow.get(uid=self.uid) 99 | if not aw: 100 | return {}, None 101 | return {'next': action['next']}, action['value'] 102 | 103 | def process_workflow(self, action): 104 | function_name = 'process_{action}'.format(action=action) 105 | function = getattr(self, function_name) 106 | params, value = function() 107 | params['uid'] = self.uid 108 | next = params.get('next') 109 | if next in ['done', 'give_up']: 110 | AccountWorkflow.delete(uid=self.uid) 111 | else: 112 | print(params) 113 | if self.uid: 114 | params['workflow'] = self.name 115 | AccountWorkflow.update_or_insert(fields=['uid'], **params) 116 | return value 117 | 118 | def get_result(self): 119 | if self.wxkw: 120 | action = self.wxkw['data'].get('action', 'name_query') 121 | value = self.process_workflow(action) 122 | return value 123 | elif self.aw: 124 | value = self.process_workflow(self.aw['next']) 125 | return value 126 | else: 127 | return -------------------------------------------------------------------------------- /momo/models/bill.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from momo.models import Model 5 | 6 | 7 | class Bill(Model): 8 | 9 | ''' 10 | 记账 11 | :param _id: 账单ID 12 | :param uid: 用户ID 13 | :param money: 金额 精确到分 14 | :param tag: 标签 15 | :param remark: 备注 16 | :param created_time: 创建时间 17 | ''' 18 | 19 | __collection__ = 'bill' 20 | __default_fields__ = { 21 | 'created_time': datetime.utcnow() 22 | } 23 | 24 | def update(self, *args, **kwargs): 25 | pass 26 | 27 | def delete(self, *args, **kwargs): 28 | pass 29 | 30 | 31 | 32 | class Tag(Model): 33 | 34 | ''' 35 | :param _id: id 36 | :param name: 标签名 37 | :param icon: 图标 38 | :param created_time: 创建时间 39 | ''' 40 | 41 | __collection__ = 'tag' 42 | 43 | __default_fields__ = { 44 | 'created_time': datetime.utcnow() 45 | } 46 | 47 | -------------------------------------------------------------------------------- /momo/models/wx_response.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from momo.models import Model 5 | from momo.models.bill import Bill, Tag 6 | from momo.models.account import Account, AccountWorkflow as AW, UserWorkFlow 7 | 8 | 9 | class WXKeyWord(Model): 10 | 11 | ''' 12 | _id: '关键字ID', 13 | word: '关键字', 14 | data: { 15 | 'workflow': '工作流', 16 | 'action': '工作流动作', 17 | 'value': '返回值', 18 | 'type': '返回值类型 url|pic|text', 19 | }, 20 | 'created_time': '创建时间', 21 | ''' 22 | 23 | __collection__ = 'wx_keyword' 24 | 25 | def update(self, *args, **kwargs): 26 | pass 27 | 28 | def delete(self, *args, **kwargs): 29 | pass 30 | 31 | 32 | class KWResponse: 33 | 34 | def __init__(self, uid, word): 35 | self.uid = uid 36 | self.word = word # key word 37 | 38 | def _get_data(self): 39 | kw = WXKeyWord.get(word=self.word) 40 | aw = AW.get(uid=self.uid) or {} 41 | if not (kw or aw): 42 | return 43 | workflow_key = kw.get('data', {}).get('workflow') if kw else None 44 | if workflow_key == BillWorkFlow.name or aw.get('workflow') == BillWorkFlow.name: 45 | value = BillWorkFlow(self.uid, self.word, wxkw=kw, aw=aw).get_result() 46 | elif workflow_key == UserWorkFlow.name or aw.get('workflow') == UserWorkFlow.name: 47 | value = UserWorkFlow(self.uid, self.word, wxkw=kw, aw=aw).get_result() 48 | else: 49 | return 50 | return value 51 | 52 | def get_response(self): 53 | value = self._get_data() 54 | if not value: 55 | return 56 | return value 57 | 58 | 59 | class BillWorkFlow(object): 60 | 61 | actions = { 62 | 'active': { 63 | 'new_uid': { 64 | 'value': '欢迎使用魔魔记账,你是第一次使用,请设置用户名!', 65 | 'next': 'input_username', 66 | }, 67 | 'old_uid': { 68 | 'value': '输入金额', 69 | 'next': 'input_amount', 70 | } 71 | }, 72 | 'input_username': { 73 | 'value': '注册成功,忘记用户名可以输入"用户名"查询\n请输入金额', 74 | 'next': 'input_amount', 75 | }, 76 | 'input_amount':{ 77 | 'value': '请选择分类: \n%s\n重新输入金额请输入"修改"', 78 | 'next': 'input_tag', 79 | }, 80 | 'input_tag': { 81 | 'value': '记账完成,可以再次输入"记账"记录下一笔', 82 | }, 83 | 'again': { 84 | 'value': '请重新输入金额', 85 | 'next': 'input_amount', 86 | }, 87 | 'cancel': { 88 | 'value': '已取消记账, 可以重新输入"记账"开始记账。', 89 | 'next': 'clear', 90 | } 91 | } 92 | name = 'keep_accounts' 93 | 94 | def __init__(self, uid, word, wxkw=None, aw=None): 95 | self.uid = uid 96 | self.word = word 97 | self.wxkw = wxkw # weixin key word instance 98 | self.aw = aw 99 | 100 | def _get_all_tags(self): 101 | tags = Tag.find(limit=100) 102 | tags_name = [t['name'] for t in tags] 103 | names = '\n'.join(tags_name) 104 | return names 105 | 106 | def process_active(self): 107 | account = Account.get(_id=self.uid) 108 | action = self.actions['active'] 109 | if not account: 110 | account = Account.insert(_id=self.uid) 111 | status = 'new_uid' 112 | else: 113 | status = 'old_uid' 114 | return {'next': action[status]['next']}, action[status]['value'] 115 | 116 | def process_input_username(self): 117 | username = self.word 118 | account = Account.get(username=username) 119 | if account: 120 | return {'next': 'input_username'}, '用户名已被使用,请重新输入' 121 | Account.update_or_insert(fields=['_id'], 122 | _id=self.uid, 123 | username=username, 124 | created_time=datetime.utcnow()) 125 | action = self.actions['input_username'] 126 | return {'next': action['next']}, action['value'] 127 | 128 | def process_input_amount(self): 129 | action = self.actions['input_amount'] 130 | try: 131 | amount = float(self.word) 132 | except (ValueError, TypeError): 133 | return {'next': 'input_amount'}, '输入的金额不正确,请重新输入' 134 | data = { 135 | 'uid': self.uid, 136 | 'money': amount 137 | } 138 | msg = action['value'] % self._get_all_tags() 139 | return {'next': action['next'], 'data': data}, msg 140 | 141 | def process_again(self): 142 | action = self.actions['again'] 143 | aw = AW.get(uid=self.uid) 144 | if not aw: 145 | return {}, None 146 | data = { 147 | 'uid': self.uid, 148 | } 149 | return {'next': action['next']}, action['value'] 150 | 151 | def process_cancel(self): 152 | action = self.actions['cancel'] 153 | aw = AW.get(uid=self.uid) 154 | if not aw: 155 | return {}, None 156 | return {'next': action['next']}, action['value'] 157 | 158 | def process_input_tag(self): 159 | action = self.actions['input_tag'] 160 | aw = AW.get(uid=self.uid) 161 | if not aw: 162 | return {}, None 163 | data = aw['data'] 164 | tag = Tag.get(name=self.word) 165 | if not tag: 166 | names = self._get_all_tags() 167 | msg = '暂不允许此分类,分类列表如下: \n%s\n请重新输入' % names 168 | return {'data': data, 'next': 'input_tag'}, msg 169 | data['tag'] = self.word 170 | return {'data': data, 'next': 'done'}, action['value'] 171 | 172 | def process_workflow(self, action): 173 | function_name = 'process_{action}'.format(action=action) 174 | function = getattr(self, function_name) 175 | params, value = function() 176 | params['uid'] = self.uid 177 | next = params.get('next') 178 | if next == 'done': 179 | data = params['data'] 180 | data['created_time'] = datetime.utcnow() 181 | Bill.insert(**data) 182 | AW.delete(uid=self.uid) 183 | elif next == 'clear': 184 | AW.delete(uid=self.uid) 185 | else: 186 | print(params) 187 | if self.uid: 188 | params['workflow'] = self.name 189 | AW.update_or_insert(fields=['uid'], **params) 190 | return value 191 | 192 | def get_result(self): 193 | print(self.wxkw) 194 | if self.wxkw: 195 | action = self.wxkw['data'].get('action', 'active') 196 | print('action', action) 197 | value = self.process_workflow(action) 198 | return value 199 | elif self.aw: 200 | value = self.process_workflow(self.aw['next']) 201 | return value 202 | else: 203 | return 204 | -------------------------------------------------------------------------------- /momo/note.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | 3 | """ 4 | 1. 将文字写入到 note_body.png 5 | 2. 将 note_header.png 拼接到 note_body.png 上边 6 | 3. 将 note_footer.png 拼接到 note_body.png 后边 7 | """ 8 | from os import path 9 | 10 | from PIL import Image, ImageDraw, ImageFont 11 | 12 | from momo.settings import Config 13 | from momo.helper import timeit 14 | 15 | otf = Config.NOTE_OTF 16 | font = ImageFont.truetype(otf, 24) 17 | 18 | 19 | class Note: 20 | 21 | def __init__(self, text, filename, header=None, body=None, footer=None, 22 | header_height=None, footer_height=None, 23 | body_wh=None, note_width=None, line_padding=10): 24 | self.text = text 25 | self.header = header 26 | self.body = body 27 | self.footer = footer 28 | self.note_width = note_width 29 | self.line_padding = line_padding # 行高 padding 30 | self.header_height = header_height 31 | self.footer_height = footer_height 32 | self.body_width, self.body_height = body_wh 33 | self.paragraphs, self.note_height, self.line_height = self.split_text() 34 | self.filename = '/tmp/%s' % filename 35 | self.background_img = None 36 | 37 | def get_paragraph(self, text): 38 | txt = Image.new('RGBA', (100, 100), (255, 255, 255, 0)) 39 | # get a drawing context 40 | draw = ImageDraw.Draw(txt) 41 | paragraph, sum_width = '', 0 42 | line_numbers, line_height = 1, 0 43 | for char in text: 44 | w, h = draw.textsize(char, font) 45 | sum_width += w 46 | if sum_width > self.note_width: 47 | line_numbers += 1 48 | sum_width = 0 49 | paragraph += '\n' 50 | paragraph += char 51 | line_height = max(h, line_height) 52 | if not paragraph.endswith('\n'): 53 | paragraph += '\n' 54 | return paragraph, line_height, line_numbers 55 | 56 | def split_text(self): 57 | # 将文本按规定宽度分组 58 | max_line_height, total_lines = 0, 0 59 | paragraphs = [] 60 | for t in self.text.split('\n'): 61 | paragraph, line_height, line_numbers = self.get_paragraph(t) 62 | max_line_height = max(line_height, max_line_height) 63 | total_lines += line_numbers 64 | paragraphs.append((paragraph, line_numbers)) 65 | line_height = max_line_height + self.line_padding # 行高多一点 66 | total_height = total_lines * line_height 67 | return paragraphs, total_height, line_height 68 | 69 | # @timeit 70 | def draw_text(self): 71 | background_img = self.make_backgroud() 72 | note_img = Image.open(background_img).convert("RGBA") 73 | draw = ImageDraw.Draw(note_img) 74 | # 文字开始位置 75 | x, y = 80, 100 76 | for paragraph, line_numbers in self.paragraphs: 77 | for line in paragraph.split('\n')[:-1]: 78 | draw.text((x, y), line, fill=(110, 99, 87), font=font) 79 | y += self.line_height 80 | # draw.text((x, y), paragraph, fill=(110, 99, 87), font=font) 81 | # y += self.line_height * line_numbers 82 | note_img.save(self.filename, "png", quality=1, optimize=True) 83 | return self.filename 84 | 85 | def get_images(self): 86 | numbers = self.note_height // self.body_height + 1 87 | images = [(self.header, self.header_height)] 88 | images.extend([(self.body, self.body_height)] * numbers) 89 | images.append((self.footer, self.footer_height)) 90 | return images 91 | 92 | def make_backgroud(self): 93 | # 将图片拼接到一起 94 | images = self.get_images() 95 | total_height = sum([height for _, height in images]) 96 | # 最终拼接完成后的图片 97 | backgroud = Image.new('RGB', (self.body_width, total_height)) 98 | left, right = 0, 0 99 | background_img = '/tmp/%s_backgroud.png' % total_height 100 | # 判断背景图是否存在 101 | if path.exists(background_img): 102 | return background_img 103 | for image_file, height in images: 104 | image = Image.open(image_file) 105 | # (0, left, self.body_width, right+height) 106 | # 分别为 左上角坐标 0, left 107 | # 右下角坐标 self.body_width, right+height 108 | backgroud.paste(image, (0, left, self.body_width, right+height)) 109 | left += height # 从上往下拼接,左上角的纵坐标递增 110 | right += height # 左下角的纵坐标也递增 111 | backgroud.save(background_img, quality=85) 112 | self.background_img = background_img 113 | return background_img 114 | 115 | 116 | note_img_config = { 117 | 'header': Config.NOTE_HEADER_IMG, 118 | 'body': Config.NOTE_BODY_IMG, 119 | 'footer': Config.NOTE_FOOTER_IMG, 120 | 'header_height': Config.NOTE_HEADER_HEIGHT, 121 | 'footer_height': Config.NOTE_FOOTER_HEIGHT, 122 | 'body_wh': (Config.NOTE_WIDTH, Config.NOTE_BODY_HEIGHT), 123 | 'note_width': Config.NOTE_TEXT_WIDTH, 124 | } 125 | print(note_img_config) 126 | -------------------------------------------------------------------------------- /momo/note_imgs/SourceHanSansSC-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/SourceHanSansSC-Regular.otf -------------------------------------------------------------------------------- /momo/note_imgs/note_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_body.png -------------------------------------------------------------------------------- /momo/note_imgs/note_body_660.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_body_660.png -------------------------------------------------------------------------------- /momo/note_imgs/note_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_footer.png -------------------------------------------------------------------------------- /momo/note_imgs/note_footer_660.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_footer_660.png -------------------------------------------------------------------------------- /momo/note_imgs/note_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_header.png -------------------------------------------------------------------------------- /momo/note_imgs/note_header_660.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/note_imgs/note_header_660.png -------------------------------------------------------------------------------- /momo/settings.py: -------------------------------------------------------------------------------- 1 | from os import environ, path 2 | from six.moves.urllib.parse import urlparse 3 | 4 | 5 | class Config(object): 6 | 7 | DEBUG = False 8 | TESTING = False 9 | 10 | SECRET_KEY = 'MmPNFrWjQZ3Z9yKZ8PMFQttgHphaq8AZ' 11 | 12 | MONGO_MASTER_HOST = environ.get('MONGO_PORT_27017_TCP_ADDR', '127.0.0.1') 13 | MONGO_MASTER_PORT = environ.get('MONGO_PORT_27017_TCP_PORT', '27017') 14 | MONGO_DATABASE = environ.get('MONGO_DATABASE', 'momo_bill') 15 | MONGO_MASTER_URL = 'mongodb://%s:%s' % (MONGO_MASTER_HOST, 16 | MONGO_MASTER_PORT) 17 | 18 | APP_TRANSPORT = environ.get('APP_TRANSPORT', 'http') 19 | APP_DOMAIN = environ.get('APP_DOMAIN', 'http://gusibi.com') 20 | API_DOMAIN = environ.get('API_DOMAIN', 'http://gusibi.com') 21 | DOMAIN = '%s://%s' % (APP_TRANSPORT, urlparse(APP_DOMAIN).netloc) 22 | 23 | # 微信 公众账号信息 24 | WEIXINMP_APPID = environ.get('WEIXINMP_APPID', 'appid') 25 | WEIXINMP_APP_SECRET = environ.get('WEIXINMP_APP_SECRET', '') 26 | WEIXINMP_TOKEN = environ.get('WEIXINMP_TOKEN', 'token') 27 | WEIXINMP_ENCODINGAESKEY = environ.get( 28 | 'WEIXINMP_ENCODINGAESKEY', '') 29 | 30 | PM25_TOKEN = environ.get('PM25_TOKEN', 'pm25_token') 31 | XMR_ID = environ.get('XMR_ID', '') 32 | 33 | QINIU_ACCESS_TOKEN = environ.get('QINIU_ACCESS_TOKEN', '') 34 | QINIU_SECRET_TOKEN = environ.get('QINIU_SECRET_TOKEN', '') 35 | QINIU_UPLOAD_URL = 'http://up.qiniu.com/' 36 | QINIU_DOMAIN = environ.get('QINIU_DOMAIN', 'media.gusibi.mobi') 37 | QINIU_DOMAINS = [QINIU_DOMAIN, 'omuo4kh1k.bkt.clouddn.com'] 38 | QINIU_HOST = "http://%s" % QINIU_DOMAIN 39 | QINIU_NOTIFY_URL = '%s/qiniu/pfop/notify' % DOMAIN 40 | QINIU_BUCKET = environ.get('QINIU_BUCKET', 'blog') 41 | 42 | QINIU_AUDIOS_TIME_KEY = environ.get('QINIU_AUDIOS_TIME_KEY', '') 43 | QINIU_AUDIOS_HOST = environ.get('QINIU_AUDIOS_HOST', 44 | 'http://omuo4kh1k.bkt.clouddn.com') 45 | 46 | QINIU_AUDIOS_CONFIG = { 47 | 'access_key': QINIU_ACCESS_TOKEN, 48 | 'secret_key': QINIU_SECRET_TOKEN, 49 | 'time_key': QINIU_AUDIOS_TIME_KEY, 50 | 'host': QINIU_AUDIOS_HOST 51 | } 52 | 53 | QCOS_HOST = environ.get('QCOS_HOST', 'http://note.gusibi.mobi') 54 | QCOS_SECRET_ID = environ.get('QCOS_SECRET_ID', '') 55 | QCOS_SECRET_KEY = environ.get('QCOS_SECRET_KEY', '') 56 | QCOS_REGION = environ.get('QCOS_REGION', 'ap-beijing') 57 | QCOS_APPID = environ.get('QCOS_APPID', '') 58 | QCOS_BUCKET = environ.get('QCOS_BUCKET', 'note') 59 | 60 | NOTE_OTF = path.normpath(path.join( 61 | path.dirname(__file__), 'note_imgs/SourceHanSansSC-Regular.otf')) 62 | NOTE_SIZE = 660 63 | if NOTE_SIZE == 990: 64 | NOTE_HEADER_FILE = 'note_header.png' 65 | NOTE_FOOTER_FILE = 'note_footer.png' 66 | NOTE_BODY_FILE = 'note_body.png' 67 | NOTE_WIDTH = 990 68 | NOTE_TEXT_WIDTH = 760 69 | NOTE_BODY_HEIGHT = 309 70 | NOTE_HEADER_HEIGHT = 133 71 | NOTE_FOOTER_HEIGHT = 218 72 | else: 73 | NOTE_HEADER_FILE = 'note_header_660.png' 74 | NOTE_FOOTER_FILE = 'note_footer_660.png' 75 | NOTE_BODY_FILE = 'note_body_660.png' 76 | NOTE_WIDTH = 660 77 | NOTE_TEXT_WIDTH = 460 78 | NOTE_BODY_HEIGHT = 206 79 | NOTE_HEADER_HEIGHT = 89 80 | NOTE_FOOTER_HEIGHT = 145 81 | 82 | NOTE_HEADER_IMG = path.normpath(path.join( 83 | path.dirname(__file__), 'note_imgs/%s' % NOTE_HEADER_FILE)) 84 | NOTE_BODY_IMG = path.normpath(path.join( 85 | path.dirname(__file__), 'note_imgs/%s' % NOTE_BODY_FILE)) 86 | NOTE_FOOTER_IMG = path.normpath(path.join( 87 | path.dirname(__file__), 'note_imgs/%s' % NOTE_FOOTER_FILE)) 88 | -------------------------------------------------------------------------------- /momo/tuling_trainer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from time import sleep 3 | 4 | from chatterbot import ChatBot 5 | from chatterbot.trainers import ListTrainer 6 | 7 | import requests 8 | 9 | API_URL = "http://www.tuling123.com/openapi/api" 10 | API_KEY0 = "" # 机器人1 的key 11 | API_KEY1 = "" # 机器人2 的key 12 | 13 | momo = ChatBot( 14 | 'Momo', 15 | storage_adapter='chatterbot.storage.MongoDatabaseAdapter', 16 | logic_adapters=[ 17 | "chatterbot.logic.BestMatch", 18 | "chatterbot.logic.MathematicalEvaluation", 19 | "chatterbot.logic.TimeLogicAdapter", 20 | ], 21 | input_adapter='chatterbot.input.VariableInputTypeAdapter', 22 | output_adapter='chatterbot.output.OutputAdapter', 23 | database='chatterbot', 24 | read_only=True 25 | ) 26 | 27 | 28 | def ask(question, key, name): 29 | params = { 30 | "key": key, 31 | "userid": name, 32 | "info": question, 33 | } 34 | res = requests.post(API_URL, json=params) 35 | result = res.json() 36 | answer = result.get('text') 37 | return answer 38 | 39 | 40 | def A(bsay): 41 | print('B:', bsay) 42 | answer = ask(bsay, API_KEY0, 'momo123') 43 | print('A:', answer) 44 | return answer 45 | 46 | 47 | def B(asay): 48 | print('A:', asay) 49 | answer = ask(asay, API_KEY1, 'momo456') 50 | print('B', answer) 51 | return answer 52 | 53 | 54 | def tariner(asay): 55 | momo.set_trainer(ListTrainer) 56 | while True: 57 | conv = [] 58 | conv.append(asay) 59 | bsay = B(asay) 60 | conv.append(bsay) 61 | momo.train(conv) 62 | print(conv) 63 | conv = [] 64 | conv.append(bsay) 65 | asay = A(bsay) 66 | conv.append(asay) 67 | momo.train(conv) 68 | print(conv) 69 | sleep(5) # 控制频率 70 | 71 | 72 | def main(asay): 73 | tariner(asay) 74 | 75 | 76 | if __name__ == '__main__': 77 | main(*sys.argv[1:]) 78 | -------------------------------------------------------------------------------- /momo/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gusibi/momo/a665e2501d05abbf05679c3834f2ba1d4b4afb7e/momo/views/__init__.py -------------------------------------------------------------------------------- /momo/views/hello.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from sanic import Sanic, Blueprint 5 | from sanic.views import HTTPMethodView 6 | from sanic.response import text 7 | 8 | from weixin.helper import smart_unicode 9 | from weixin.pay import WeixinPay 10 | 11 | from momo.models.bill import Tag 12 | from momo.helper import get_momo_answer 13 | from momo.models.wx_response import KWResponse as KWR 14 | 15 | 16 | blueprint = Blueprint('index', url_prefix='/') 17 | 18 | 19 | class Index(HTTPMethodView): 20 | 21 | async def get(self, request): 22 | return text('hello momo!') 23 | 24 | 25 | class KWResponse(HTTPMethodView): 26 | 27 | async def get(self, request): 28 | args = request.raw_args 29 | uid = args.get('uid') 30 | word = args.get('word') 31 | kwr = KWR(uid, word) 32 | value = kwr.get_response() 33 | return text(value or 'please input') 34 | 35 | 36 | class Tags(HTTPMethodView): 37 | 38 | def get(self, request): 39 | args = request.raw_args 40 | _tag = args.get('tag') 41 | tag = Tag.insert(name=_tag, created_time=datetime.utcnow()) 42 | return text(_tag) 43 | 44 | 45 | class ChatBot(HTTPMethodView): 46 | 47 | async def get(self, request): 48 | ask = request.args.get('ask') 49 | if ask: 50 | answer = get_momo_answer(ask) 51 | return text(answer) 52 | return text('你说啥?') 53 | 54 | 55 | blueprint.add_route(Index.as_view(), '/') 56 | # blueprint.add_route(Tags.as_view(), '/add_tag') 57 | # blueprint.add_route(ChatBot.as_view(), '/momo') 58 | # blueprint.add_route(KWResponse.as_view(), '/kwr') 59 | -------------------------------------------------------------------------------- /momo/views/mweixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from six import StringIO 5 | 6 | import re 7 | import time 8 | import xmltodict 9 | # from chatterbot.trainers import ListTrainer 10 | 11 | import requests 12 | from sanic import Blueprint 13 | from sanic.views import HTTPMethodView 14 | from sanic.response import text 15 | from sanic.exceptions import ServerError 16 | 17 | from weixin import WeixinMpAPI 18 | from weixin.reply import TextReply, ImageReply 19 | from weixin.response import WXResponse as _WXResponse 20 | from weixin.lib.WXBizMsgCrypt import WXBizMsgCrypt 21 | 22 | from momo.settings import Config 23 | from momo.media import media_fetch_to_qiniu, upload_file_to_qcos 24 | from momo.helper import (validate_xml, smart_str, 25 | get_momo_answer, set_momo_answer, 26 | get_weixinmp_token, get_weixinmp_media_id) 27 | from momo.models.wx_response import KWResponse as KWR 28 | 29 | 30 | blueprint = Blueprint('weixin', url_prefix='/weixin') 31 | 32 | appid = smart_str(Config.WEIXINMP_APPID) 33 | secret = smart_str(Config.WEIXINMP_APP_SECRET) 34 | token = smart_str(Config.WEIXINMP_TOKEN) 35 | encoding_aeskey = smart_str(Config.WEIXINMP_ENCODINGAESKEY) 36 | 37 | PM25_BASE_URL = 'http://api.waqi.info' 38 | PM25_TOKEN = Config.PM25_TOKEN 39 | 40 | AUTO_REPLY_CONTENT = """ 41 | Hi,朋友! 42 | 43 | 这是我妈四月的公号,我是魔魔,我可以陪你聊天呦! 44 | 45 | 你可以输入"pm25 城市名" 查询实时 pm 指数! 46 | 47 | 也可以试试"菜单"、"note"、"并发"、"协程"、"设计模式" 等关键字吼! 48 | 49 | 历史记录 50 | """ 51 | 52 | CUSTOMER_SERVICE_TEMPLATE = ''' 53 | 54 | 55 | 56 | {create_time} 57 | 58 | 59 | ''' 60 | 61 | momo_learn = re.compile(r'^momoya:"(?P\S*)"<"(?P\S*)"') 62 | pm25 = re.compile(r'^pm25 (?P\S*)') 63 | xmr_url = 'https://supportxmr.com/api/miner/%s/stats' % Config.XMR_ID 64 | xmr_stats_tmp = ''' 65 | Hash Rate(24 Avg): {hash}H/s ({lastHash}H/s) 66 | Total Hashes: {totalHashes} 67 | Valid Shares: {validShares} 68 | Total Due: {amtDue} XMR 69 | Total Paid: {amtPaid} XMR 70 | ''' 71 | 72 | 73 | def get_response(url, format='json'): 74 | resp = requests.get(url) 75 | if resp.status_code != 200: 76 | return '发生了错误' 77 | if format == 'json': 78 | results = resp.json() 79 | return results 80 | 81 | 82 | def get_pm25(city): 83 | url = "{}/search/?token={token}&keyword={city}".format(PM25_BASE_URL, token=PM25_TOKEN, city=city) 84 | results = get_response(url) 85 | if not isinstance(results, dict): 86 | return results 87 | data = results.get('data') 88 | if len(data) == 0: 89 | return '没有搜到结果' 90 | text = '\n'.join(['PM2.5: {pm25} {name}'.format( 91 | name=info.get('station').get('name', '').split(';')[0], 92 | pm25=info.get('aqi')) for info in data]) 93 | return text 94 | 95 | 96 | def format_xmr_stats(data): 97 | data['lastHash'] = data.get('lastHash', 0) // (10**7) 98 | data['amtDue'] = data.get('amtDue', 0.0) / (10**12) 99 | return data 100 | 101 | 102 | def get_xmr_stats(): 103 | results = get_response(xmr_url) 104 | if not isinstance(results, dict): 105 | return results 106 | data = format_xmr_stats(results) 107 | text = xmr_stats_tmp.format(**data) 108 | return text 109 | 110 | 111 | class ReplyContent(object): 112 | 113 | _source = 'value' 114 | 115 | def __init__(self, event, keyword, content=None, momo=True): 116 | self.momo = momo 117 | self.event = event 118 | self.content = content 119 | self.keyword = keyword 120 | if self.event == 'scan': 121 | pass 122 | 123 | @property 124 | def value(self): 125 | if self.momo: 126 | answer = get_momo_answer(self.content) 127 | return answer 128 | return '' 129 | 130 | def set(self, conversation): 131 | if self.momo: 132 | set_momo_answer(conversation) 133 | return '魔魔学会了!' 134 | 135 | 136 | class Article(object): 137 | 138 | def __init__(self, Title=None, Description=None, PicUrl=None, Url=None): 139 | self.title = Title or '' 140 | self.description = Description or '' 141 | self.picurl = PicUrl or '' 142 | self.url = Url or '' 143 | 144 | 145 | class WXResponse(_WXResponse): 146 | 147 | auto_reply_content = AUTO_REPLY_CONTENT 148 | 149 | def _subscribe_event_handler(self): 150 | self.reply_params['content'] = self.auto_reply_content 151 | self.reply = TextReply(**self.reply_params).render() 152 | 153 | def _unsubscribe_event_handler(self): 154 | pass 155 | 156 | def _image_msg_handler(self): 157 | media_id = self.data['MediaId'] 158 | picurl = None 159 | if not picurl: 160 | picurl = self.data['PicUrl'] 161 | is_succeed, media_key = media_fetch_to_qiniu(picurl, media_id) 162 | qiniu_url = '{host}/{key}'.format(host=Config.QINIU_HOST, key=media_key) 163 | self.reply_params['content'] = qiniu_url 164 | self.reply = TextReply(**self.reply_params).render() 165 | 166 | def _text_msg_handler(self): 167 | # 文字消息处理逻辑 168 | event_key = 'text' 169 | content = self.data.get('Content') 170 | pm25_match = pm25.match(content) 171 | learn_match = momo_learn.match(content) 172 | if learn_match: 173 | # 教魔魔说话第一优先级 174 | conversation = learn_match.groups() 175 | reply_content = ReplyContent('text', event_key) 176 | response = reply_content.set(conversation) 177 | self.reply_params['content'] = response 178 | elif pm25_match: 179 | # pm2.5 查询第二优先级 180 | city = pm25_match.groupdict().get('city') 181 | reply_content = ReplyContent('text', event_key) 182 | text = get_pm25(city) 183 | self.reply_params['content'] = text 184 | elif content.startswith('note '): 185 | if content.startswith('note -u '): 186 | note = content[8:] 187 | else: 188 | note = content[5:] 189 | reply_content = ReplyContent('text', event_key) 190 | to_user = self.reply_params['to_user'] 191 | from momo.note import Note, note_img_config 192 | filename = '%s_%s.png' % (to_user, int(time.time())) 193 | note_file = Note(note, filename, **note_img_config).draw_text() 194 | if content.startswith('note -u '): 195 | upload_file_to_qcos(note_file, filename) 196 | qiniu_url = '{host}/{key}'.format(host=Config.QCOS_HOST, 197 | key=filename) 198 | self.reply_params['content'] = qiniu_url 199 | else: 200 | access_token, _ = get_weixinmp_token(appid, secret) 201 | media_id = get_weixinmp_media_id(access_token, note_file) 202 | self.reply_params['media_id'] = media_id 203 | self.reply = ImageReply(**self.reply_params).render() 204 | return 205 | elif content == 'xmr_stats': 206 | text = get_xmr_stats() 207 | self.reply_params['content'] = text 208 | else: 209 | # 再查询有没有特殊自动回复消息workflow 210 | to_user = self.reply_params['to_user'] 211 | kwr = KWR(to_user, content) 212 | value = kwr.get_response() 213 | if not value: 214 | reply_content = ReplyContent('text', event_key, content) 215 | value = reply_content.value 216 | if value.startswith('The current time is'): 217 | value = content 218 | self.reply_params['content'] = value 219 | self.reply = TextReply(**self.reply_params).render() 220 | 221 | def _click_event_handler(self): 222 | # 点击菜单事件的逻辑 223 | pass 224 | 225 | 226 | class WXRequestView(HTTPMethodView): 227 | 228 | def _get_args(self, request): 229 | params = request.raw_args 230 | if not params: 231 | raise ServerError("invalid params", status_code=400) 232 | args = { 233 | 'mp_token': Config.WEIXINMP_TOKEN, 234 | 'signature': params.get('signature'), 235 | 'timestamp': params.get('timestamp'), 236 | 'echostr': params.get('echostr'), 237 | 'nonce': params.get('nonce'), 238 | } 239 | return args 240 | 241 | def get(self, request): 242 | args = self._get_args(request) 243 | weixin = WeixinMpAPI(**args) 244 | if weixin.validate_signature(): 245 | return text(args.get('echostr') or 'fail') 246 | return text('fail') 247 | 248 | def _get_xml(self, data): 249 | post_str = smart_str(data) 250 | # 验证xml 格式是否正确 251 | validate_xml(StringIO(post_str)) 252 | return post_str 253 | 254 | def _decrypt_xml(self, params, crypt, xml_str): 255 | nonce = params.get('nonce') 256 | msg_sign = params.get('msg_signature') 257 | timestamp = params.get('timestamp') 258 | ret, decryp_xml = crypt.DecryptMsg(xml_str, msg_sign, 259 | timestamp, nonce) 260 | return decryp_xml, nonce 261 | 262 | def _encryp_xml(self, crypt, to_xml, nonce): 263 | to_xml = smart_str(to_xml) 264 | ret, encrypt_xml = crypt.EncryptMsg(to_xml, nonce) 265 | return encrypt_xml 266 | 267 | def post(self, request): 268 | args = self._get_args(request) 269 | weixin = WeixinMpAPI(**args) 270 | if not weixin.validate_signature(): 271 | raise AttributeError("Invalid weixin signature") 272 | xml_str = self._get_xml(request.body) 273 | crypt = WXBizMsgCrypt(token, encoding_aeskey, appid) 274 | decryp_xml, nonce = self._decrypt_xml(request.raw_args, crypt, xml_str) 275 | xml_dict = xmltodict.parse(decryp_xml) 276 | xml = WXResponse(xml_dict)() or 'success' 277 | encryp_xml = self._encryp_xml(crypt, xml, nonce) 278 | return text(encryp_xml or xml) 279 | 280 | 281 | blueprint.add_route(WXRequestView.as_view(), '/request') 282 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | lxml 3 | qiniu 4 | Sanic 5 | xmltodict 6 | jsonschema 7 | pyyaml 8 | pillow 9 | pycrypto 10 | chatterbot 11 | python-weixin 12 | cos-python-sdk-v5 13 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | supervisord -c supervisord.conf 2 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock ; path to your socket file 3 | 4 | ; [inet_http_server] ; HTTP 服务器,提供 web 管理界面 5 | ; port=127.0.0.1:9001 ; Web 管理后台运行的 IP 和端口,如果开放到公网,需要注意安全性 6 | 7 | [supervisord] 8 | logfile=/tmp/supervisord.log ; supervisord log file 9 | logfile_maxbytes=50MB ; maximum size of logfile before rotation 10 | logfile_backups=10 ; number of backed up logfiles 11 | loglevel=info ; info, debug, warn, trace 12 | pidfile=/tmp/supervisord.pid ; pidfile location 13 | nodaemon=true ; run supervisord as a daemon 14 | minfds=1024 ; number of startup file descriptors 15 | minprocs=200 ; number of process descriptors 16 | 17 | [rpcinterface:supervisor] 18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 19 | 20 | [supervisorctl] 21 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 22 | 23 | [program:momo] 24 | command=/bin/sh -c 'NEW_RELIC_CONFIG_FILE=newrelic.ini newrelic-admin run-program python app.py' 25 | autostart=true 26 | autorestart=true 27 | directory=./ 28 | ;启动的等待时候,我想是为了重启能杀掉原来进程预留的时间 29 | startsecs=10 30 | ;进程发送停止信号等待os返回SIGCHILD的时间 31 | stopwaitsecs=10 32 | ;低优先级的会首先启动最后关闭 33 | priority=998 34 | ;以下2句是为了保证杀掉进程和其子进程而不会只杀死其控制的程序主进程而留下子进程变为孤立进程的问题 35 | stopsignal=QUIT 36 | stopasgroup=true 37 | --------------------------------------------------------------------------------