├── .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 | 
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 |
--------------------------------------------------------------------------------