├── .gitignore
├── LICENSE
├── README.md
├── config
└── .gitignore
├── doc
├── arch.md
├── robot.dot
├── robot.dot.svg
├── webwx.dot.svg
├── webwxApp2aeaf2.js
└── webwxApp2c32b4.js
├── examples
└── web.js
├── index.js
├── lib
├── global.js
├── logger
│ └── logger.js
├── reply
│ ├── dialog.js
│ └── reply.js
├── robot.js
├── util.js
└── webwx.js
├── package.json
└── screenshots
├── 0.1.3.png
├── 0.1.4-2.gif
└── 0.1.4.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | # vim temp file
30 | *.swp
31 | *.swo
32 | # gedit temp file
33 | *~
34 | # data dir
35 | data/
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 组装中的机器人
2 | =================
3 |
4 | 注意: 这只是个组装中的机器人,作者还在探索。详细协议分析见[web 微信与基于node的微信机器人实现](http://reverland.org/javascript/2016/01/15/webchat-user-bot/)。
5 |
6 | > ``我认为,保持计算机科学的趣味举足轻重。这一学科在起步时让人乐不可支。当然,那些付钱的客户们时常会觉得被我们敷衍了。之后,我们开始信以为真。我们开始觉得,自己真的像是对成功地、无差错地、完美地使用这些机器义不容辞。我不以为然。我认为我们的责任是去拓展这一领域,将其发展到新的方向,并在私底下保持趣味。我希望计算机科学领域绝不要丧失其趣味意识。最重要的是,我希望我们不要变成传道士,不要认为你是兜售圣经的人,世界上这种人已经太多了。你所知道的有关计算的东西,其他人也都能学到。绝不要认为成功计算的钥匙只掌握在你的手里。我认为并希望,你所掌握的是智慧:那种当你第一次站在这一机器面前时就能看到它的本质的能力,这样你才能将它推向前进。''
7 | >
8 | > Alan J. Perlis (生于1922年4月1日,卒于1990年2月7日)
9 | > 从SICP摘抄
10 |
11 | 作者仅仅为了:
12 |
13 | 1. 自己想要一个能进行信息收发的某国内顶级IM机器人。
14 | 2. 熟悉Node的http/https request 等模块,学习HTTP基本知识。
15 | 3. 学着Promise怎么使用,如果可以Stream如何玩,这么比较好的抽象整个流程
16 | 4. 学习使用浏览器调试工具,https代理等等。甚至透明代理,iptable这种东西。。
17 | 5. 抽象
18 |
19 | 最重要的是:
20 |
21 | 5. 好奇
22 | 6. 聊以自娱
23 |
24 | 所以,这是一堆混乱不堪的东西,希望各位老师教我做人。
25 |
26 | ## 概览
27 |
28 | 基本上是这样,长连接一旦断开(服务器返回(服务器会在超时前返回)或者网络问题),继续长连接:
29 |
30 | 登录->长连接->长连接...(无尽的长连接来保持服务器能及时推送新信息)
31 |
32 | 根据长连接返回信息,如果出现服务器如果需要更新
33 |
34 | webwxsync->filter->transducer
35 |
36 | 像js这种异步程序,当你长连接保持时并不会阻塞其他操作的执行。异步大法好!
37 |
38 |
39 | ## 依赖
40 |
41 | 如果不使用linux分支,需要imagemagick:
42 |
43 | sudo apt-get install imagemagick
44 |
45 | 终端支持unicode字体:
46 |
47 | sudo apt-get install ttf-ancient-fonts
48 |
49 | ## 使用须知
50 |
51 | 不好意思,Mac下需要你自己折腾让imagemagick能支持管道流数据。
52 | 据我所知,brew 默认安装的imagemagick并不行
53 |
54 | 请为了学习和娱乐适量使用,因此造成的任何损失、影响,都由使用者自行承担,与作者无关。源代码遵循GPL v2。
55 |
56 | 使用方式
57 |
58 | git clone https://github.com/HalfdogStudio/wechat-user-bot.git wechat-user-bot && cd wechat-user-bot
59 | npm install
60 | CURRENT_CASH=1314 node index.js
61 |
62 | 调试请求:
63 |
64 | CURRENT_CASH=1314 DEBUG=info node index.js
65 |
66 | 扫描二维码确认登录。
67 |
68 | 目前是个聊天和记录机器人,对话引擎默认为重复(echo),可指定其它引擎。
69 |
70 | ## 捐赠
71 |
72 | [如果您觉得这些东西对您有用,请支持自由软件基金会](https://my.fsf.org/donate/?pk_campaign=2015-2016-fundraiser-banner-gnu&pk_kwd=donate)
73 |
74 | ## 截图
75 |
76 | 登录
77 |
78 | 
79 |
80 | 运行
81 |
82 | 
83 |
84 | ## 友情链接
85 |
86 | > Wechaty - Wechat for Bot. Powered by WebDriver & Node.js
87 |
88 | https://github.com/zixia/wechaty
89 |
90 | ## ChangeLog
91 |
92 | ### 2016.4.24
93 |
94 | - 实验性的多媒体文件上传接口实现
95 | - 默认记账机器人
96 |
97 | ### 2016.3.9
98 |
99 | - 表情信息记录
100 | - 重写media api
101 | - 重构、清理和日志
102 |
103 | ### 2016.2.11
104 |
105 | - 重新组织代码
106 | - 分离请求和逻辑处理
107 | - 图像信息记录
108 |
109 | ### 2016.2.9
110 |
111 | - 更新web示例
112 | - 分离display
113 |
114 | ### 2016.1.31
115 |
116 | - 将消息过滤和处理分离到入口程序中
117 | - 滥用高阶函数特性
118 |
119 | ### 2016.1.26
120 |
121 | - 分离逻辑与重构
122 |
123 | ### 2016.1.20
124 |
125 | - 更加健壮的对话引擎
126 | - 非好友用户名解析与缓存
127 |
128 | ### 2016.1.19
129 |
130 | - 群成员信息缓存
131 | - 消息过滤机制
132 | - 过滤特殊用户
133 |
134 | ### 2016.1.18
135 |
136 | - 重构消息处理逻辑
137 |
138 | ### 2016.1.17
139 |
140 | - 群组发信人用户名解析
141 | - 智能关闭二维码图像窗口
142 |
143 | ### 2016.1.15
144 |
145 | - 清理协议相关程序
146 |
147 | ### 2016.1.14
148 |
149 | - 实现登录长连接 #8
150 | - 分离替换api
151 |
152 | ### 2016.1.13
153 |
154 | - 重新实现长连接,修复多条消息会重新出现的问题 #5
155 | - 分离回复逻辑 #2
156 | - 捕获服务器断开消息自动退出 #1
157 |
158 | ### 2016.1.12
159 |
160 | - 修复遗漏消息的问题
161 | - 接入图灵机器人实现聊天机器人
162 | - 清理代码
163 | - 完成用request替换所有原生模块
164 |
165 | ### 2016.1.11
166 |
167 | - 完成基本的回复机器人功能。
168 |
--------------------------------------------------------------------------------
/config/.gitignore:
--------------------------------------------------------------------------------
1 | # all file in data should be ignored
2 | *
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/doc/arch.md:
--------------------------------------------------------------------------------
1 | ## 架构
2 |
3 | 总体如下
4 |
5 | ```bash
6 | ..[reverland@reverland-R478-R429] - [~/wx/wechat-user-bot] - [四 2月 18, 04:35]
7 | ..[$] <( (git)-[master]-)> tree -I "node_modules|data" .
8 | .
9 | ├── config
10 | │ └── apikeys.js
11 | ├── index.js
12 | ├── lib
13 | │ ├── cache.js
14 | │ ├── global.js
15 | │ ├── logger
16 | │ │ └── logger.js
17 | │ ├── msghandle.js
18 | │ ├── reply
19 | │ │ ├── dialog.js
20 | │ │ └── reply.js
21 | │ ├── robot.js
22 | │ └── webwx.js
23 | ```
24 |
25 | ### index.js: 入口文件
26 |
27 | 用串联的Promise构建起整个程序,wxSession在其中传递。
28 |
29 | ### apikeys.js: api文件
30 |
31 | 如果使用图灵机器人,需要自行申请图灵机器人的API,保存到`apikeys.js`文件内:
32 |
33 | module.exports.turingRobotApiKey = '你申请的key';
34 |
35 | 也可以在`dialog.js`里实现自己的对话系统,请参照源码。
36 |
37 | ### logger/logger.js: 消息记录
38 |
39 | ```javascript
40 | function wechatLogger(wxSession) {
41 | return o=>{
42 | // 对每一条MsgAddList对象o
43 | return o;
44 | }
45 | }
46 | ```
47 |
48 | ### reply/reply.js: 消息回复
49 |
50 | 传递给msghandle的transducer,接受wxSeesion,返回一个接受一个参数的函数。
51 |
52 | ```javascript
53 | function generateReply(wxSession) {
54 | return o=>{
55 | // o: 每个addMsgList中对象经过一些列transducer消息处理后的对象
56 | // 根据o回复消息
57 | return something; // 传递给下一个transducer的对象
58 | }
59 | }
60 |
61 | ### reply/dialog.js: 对话引擎
62 |
63 | 每个对话引擎实现为一个函数`dialog`:
64 |
65 | ```javascript
66 | function dialog(content, userid) {
67 | // 处理content
68 | // ...
69 | return Promise.resolve(newContent);
70 | }
71 | ```
72 | ### msghandle.js: 消息处理
73 |
74 | 接受filter列表和transducer列表
75 | 返回接受addMsgList和wxSession的函数
76 |
77 | 内置某些filter和Promise化 transducer。
78 |
79 | ```javascript
80 | function handleMsg(filters, transducers) {
81 | return (addMsgList, wxSession) => {
82 | var replys = addMsgList
83 | .filter(o=>(o.ToUserName === wxSession.username)) // 过滤不是给我的信息
84 | .filter(o=>(SPECIAL_USERS.indexOf(o.FromUserName) < 0)); // 不是特殊用户
85 |
86 | filters.forEach(f=> {
87 | replys = replys.filter(f(wxSession));
88 | });
89 |
90 | transducers.push((wxSession)=>(o)=>Promise.resolve(o)); // 默认transducers,Promise化reply
91 |
92 | transducers.forEach(f=> {
93 | replys = replys.map(f(wxSession));
94 | });
95 |
96 | replys.map(r=>r.catch(console.error)); // 错误捕获
97 | }
98 | }
99 |
100 | ### cache.js: 更新联系人信息缓存
101 |
102 | ### global.js: 变量声明
103 |
104 |
105 | ### robot.js: 定义机器人
106 |
107 | ### webwx.js: web微信基础函数
108 |
--------------------------------------------------------------------------------
/doc/robot.dot:
--------------------------------------------------------------------------------
1 | # sa@linuxer.me
2 | # LiuYuyang
3 |
4 |
5 | graph robot {
6 | rankdir=BT;
7 | bgcolor="#f6f6f6";
8 | color="#b5e77d";
9 |
10 | # 样式
11 | node [style=filled,color="black", fillcolor="#85d1df", shape=box];
12 | edge [color="darkblue", fontcolor="darkblue"];
13 | style="filled";
14 | fillcolor="#b5e77d";
15 |
16 | webwx[label="微信机器人"];
17 | dialog[label="对话引擎"];
18 | global[label="变量声明"];
19 | logger[label="消息记录"];
20 | reply[label="消息回复"];
21 | cache[label="信息缓存"];
22 | apikeys[label="api配置"];
23 | main[label="入口程序"];
24 |
25 | {logger, reply, cache, global} -- webwx;
26 | webwx -- main;
27 | {apikeys, dialog} -- reply;
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/doc/robot.dot.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
82 |
--------------------------------------------------------------------------------
/doc/webwx.dot.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
298 |
--------------------------------------------------------------------------------
/examples/web.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var getUUID = require('../webwx.js').getUUID;
4 | var checkAndParseUUID = require('../webwx.js').checkAndParseUUID;
5 | var showQRImage = require('../webwx.js').showQRImage;
6 | var checkLogin = require('../webwx.js').checkLogin;
7 | var parseRedirectUrl = require('../webwx.js').parseRedirectUrl;
8 | var login = require('../webwx.js').login;
9 | var getbaseRequest = require('../webwx.js').getbaseRequest;
10 | var webwxinit = require('../webwx.js').webwxinit;
11 |
12 | var wechatLogger = require('../logger.js').wechatLogger;
13 | var generateReplys = require('../reply.js').generateReplys;
14 |
15 | var getContact = require('../webwx.js').getContact;
16 | var robot = require('../webwx.js').robot;
17 |
18 | var http = require('http');
19 |
20 | http.createServer((req, res)=>{
21 | var display = res;
22 | display.kill = res.end;
23 | display.stdin = res;
24 |
25 | getUUID
26 | .then(checkAndParseUUID)
27 | .then(showQRImage(display))
28 | .then(checkLogin)
29 | .then(parseRedirectUrl)
30 | .then(login)
31 | .then(getbaseRequest)
32 | .then(webwxinit)
33 | .then(getContact)
34 | .then(robot(
35 | [(obj)=>o=>true],
36 | [wechatLogger, generateReplys]
37 | ))
38 | .catch((e)=>{
39 | console.error(e);
40 | process.exit(1);
41 | });
42 | }).listen(8000)
43 |
44 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var getUUID = require('./lib/webwx.js').getUUID;
4 | var checkAndParseUUID = require('./lib/webwx.js').checkAndParseUUID;
5 | var showQRImage = require('./lib/webwx.js').showQRImage;
6 | var checkLogin = require('./lib/webwx.js').checkLogin;
7 | var parseRedirectUrl = require('./lib/webwx.js').parseRedirectUrl;
8 | var login = require('./lib/webwx.js').login;
9 | var getbaseRequest = require('./lib/webwx.js').getbaseRequest;
10 | var webwxinit = require('./lib/webwx.js').webwxinit;
11 |
12 | var wechatLogger = require('./lib/logger/logger.js').wechatLogger;
13 | var generateReply = require('./lib/reply/reply.js').generateReply;
14 |
15 | var webwxgetcontact = require('./lib/webwx.js').webwxgetcontact;
16 | var robot = require('./lib/robot.js').robot;
17 |
18 | // display, which is a stream
19 | var child_process = require('child_process');
20 | var display = child_process.spawn('display');
21 |
22 | getUUID
23 | .then(checkAndParseUUID)
24 | .then(showQRImage(display))
25 | .then(checkLogin)
26 | .then(parseRedirectUrl)
27 | .then(login)
28 | .then(getbaseRequest)
29 | .then(webwxinit)
30 | .then(webwxgetcontact)
31 | .then(robot(
32 | [(wxSession)=>o=>true],
33 | [wechatLogger, generateReply]
34 | // [],
35 | // [wechatLogger]
36 | ))
37 | .catch((e)=>{
38 | console.error(e);
39 | process.exit(1);
40 | });
41 |
42 |
--------------------------------------------------------------------------------
/lib/global.js:
--------------------------------------------------------------------------------
1 | const MSGTYPE_TEXT = 1;
2 | const MSGTYPE_IMAGE = 3;
3 | const MSGTYPE_VOICE = 34;
4 | const MSGTYPE_VIDEO = 43;
5 | const MSGTYPE_MICROVIDEO = 62;
6 | const MSGTYPE_EMOTICON = 47;
7 | const MSGTYPE_APP = 49;
8 | const SPECIAL_USERS = 'newsapp,fmessage,filehelper,weibo,qqmail,fmessage,tmessage,qmessage,qqsync,floatbottle,lbsapp,shakeapp,medianote,qqfriend,readerapp,blogapp,facebookapp,masssendapp,meishiapp,feedsapp,voip,blogappweixin,weixin,brandsessionholder,weixinreminder,officialaccounts,notification_messages,wxitil,userexperience_alarm,notification_messages';
9 | const MINE_EXT = {
10 | 'image/png': 'png',
11 | 'image/jpeg': 'jpeg',
12 | 'image/gif': 'gif',
13 | 'video/mp4': 'mp4',
14 | 'audio/mp3': 'mp3',
15 | }
16 |
17 | module.exports.MSGTYPE_TEXT = MSGTYPE_TEXT;
18 | module.exports.MSGTYPE_IMAGE = MSGTYPE_IMAGE;
19 | module.exports.MSGTYPE_VOICE = MSGTYPE_VOICE;
20 | module.exports.MSGTYPE_VIDEO = MSGTYPE_VIDEO;
21 | module.exports.MSGTYPE_MICROVIDEO = MSGTYPE_MICROVIDEO;
22 | module.exports.MSGTYPE_EMOTICON = MSGTYPE_EMOTICON;
23 | module.exports.MSGTYPE_APP = MSGTYPE_APP;
24 |
25 | module.exports.SPECIAL_USERS = SPECIAL_USERS;
26 |
27 | module.exports.MINE_EXT = MINE_EXT;
28 |
--------------------------------------------------------------------------------
/lib/logger/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | var inspect = require('util').inspect
3 | var request = require('request');
4 | var path = require('path');
5 | var fs = require('fs');
6 | var webwxbatchgetcontact = require('../webwx.js').webwxbatchgetcontact;
7 | var webwxgetmsgimg = require('../webwx.js').webwxgetmsgimg;
8 | var webwxgetvoice = require('../webwx.js').webwxgetvoice;
9 | var webwxgetvideo = require('../webwx.js').webwxgetvideo;
10 | var webwxgetemoticon = require('../webwx.js').webwxgetemoticon;
11 | var webwxgetmedia = require('../webwx.js').webwxgetmedia;
12 |
13 | var MSGTYPE_TEXT = require('../global.js').MSGTYPE_TEXT;
14 | var MSGTYPE_IMAGE = require('../global.js').MSGTYPE_IMAGE;
15 | var MSGTYPE_VOICE = require('../global.js').MSGTYPE_VOICE;
16 | var MSGTYPE_VIDEO = require('../global.js').MSGTYPE_VIDEO;
17 | var MSGTYPE_MICROVIDEO = require('../global.js').MSGTYPE_MICROVIDEO;
18 | var MSGTYPE_EMOTICON = require('../global.js').MSGTYPE_EMOTICON;
19 | var MSGTYPE_APP = require('../global.js').MSGTYPE_APP;
20 |
21 | var convertEmoji = require('../util.js').convertEmoji;
22 |
23 | /* 目录检查 */
24 | (function checkDir(dirs) {
25 | dirs.forEach(d=>{
26 | var dirPath = path.join(process.cwd(), d);
27 | if (!fs.existsSync(dirPath)) {
28 | fs.mkdirSync(dirPath);
29 | }
30 | })
31 | })([
32 | 'data/',
33 | 'data/pic',
34 | 'data/voice',
35 | 'data/video',
36 | 'data/msglog',
37 | 'data/emoticon',
38 | 'data/file'
39 | ]);
40 |
41 | var winston = require('winston');
42 |
43 | var logger = new (winston.Logger)({
44 | level: 'info',
45 | transports: [
46 | new (winston.transports.Console)(),
47 | new (winston.transports.File)( { filename: path.join(process.cwd(), `data/msglog/${Date.now()}.log`) } )
48 | ]
49 | });
50 |
51 | /*
52 | * logger函数,
53 | * @param: 会话对象
54 | */
55 |
56 | function wechatLogger(wxSession) {
57 | return o=>{
58 | // 对每一条MsgAddList对象o
59 | switch (o.MsgType) {
60 | case MSGTYPE_TEXT:
61 | logTextMessage(o, wxSession)
62 | break;
63 | case MSGTYPE_IMAGE:
64 | logMultimediaMessage(o, wxSession, webwxgetmsgimg, 'data/pic');
65 | break;
66 | case MSGTYPE_VOICE:
67 | logMultimediaMessage(o, wxSession, webwxgetvoice, 'data/voice');
68 | break;
69 | case MSGTYPE_VIDEO:
70 | case MSGTYPE_MICROVIDEO:
71 | logMultimediaMessage(o, wxSession, webwxgetvideo, 'data/video');
72 | break;
73 | case MSGTYPE_EMOTICON:
74 | logEmoticonMessage(o, wxSession, webwxgetemoticon, 'data/emoticon')
75 | break;
76 | case MSGTYPE_APP:
77 | if (o.AppMsgType == 6) {
78 | logMultimediaMessage(o, wxSession, webwxgetmedia, 'data/file')
79 | break;
80 | }
81 | default:
82 | logNotImplementMsg(o, wxSession, "wechatLogger");
83 | }
84 | return o;
85 | }
86 | }
87 |
88 | /*
89 | * 表情
90 | */
91 |
92 | function logEmoticonMessage(o, wxSession, apiFunc, dirPath) {
93 | var emoticonPath = path.join(process.cwd(), dirPath, o.MsgId);
94 | apiFunc(o, wxSession, emoticonPath)
95 | .then((emoticonPath) => {
96 | if (o.FromUserName.startsWith("@@")) {
97 | logGroupEmoticonMsg(o, wxSession, emoticonPath);
98 | } else {
99 | logPrivateEmoticonMsg(o, wxSession, emoticonPath);
100 | }
101 | }).catch((e)=>{
102 | logger.error(`[logEmoticonMessage]${e} ${inspect(o)}`);
103 | })
104 | }
105 |
106 | function logPrivateEmoticonMsg(o, wxSession, emoticonPath) {
107 | handlePrivate(o.FromUserName, 'file://' + emoticonPath, wxSession)
108 | .then(logger.info, logger.error);
109 | }
110 |
111 | function logGroupEmoticonMsg(o, wxSession, emoticonPath) {
112 | var result = /^(@[^:]+):
/mg.exec(o.Content);
113 | if (result) {
114 | var fromUserName = result[1];
115 | }
116 | handleGroup(o.FromUserName, `${fromUserName}:
file://${emoticonPath}`, wxSession)
117 | .then(logger.info, logger.error);
118 | }
119 |
120 |
121 | /*
122 | * 多媒体记录
123 | */
124 |
125 | function logMultimediaMessage(o, wxSession, apiFunc, dirPath) {
126 | var multimediaPath = path.join(process.cwd(), dirPath, o.MsgId);
127 | apiFunc(o, wxSession, multimediaPath)
128 | .then((multimediaPath) => {
129 | if (o.FromUserName.startsWith("@@")) {
130 | logGroupMultimediaMsg(o, wxSession, multimediaPath);
131 | } else {
132 | logPrivateMultimediaMsg(o, wxSession, multimediaPath);
133 | }
134 | })
135 | }
136 |
137 | function logPrivateMultimediaMsg(o, wxSession, multimediaPath) {
138 | handlePrivate(o.FromUserName, 'file://' + multimediaPath, wxSession)
139 | .then(logger.info, logger.error);
140 | }
141 |
142 | function logGroupMultimediaMsg(o, wxSession, multimediaPath) {
143 | var result = /^(@[^:]+):
/mg.exec(o.Content);
144 | if (result) {
145 | var fromUserName = result[1];
146 | }
147 | handleGroup(o.FromUserName, `${fromUserName}:
file://${multimediaPath}`, wxSession)
148 | .then(logger.info, logger.error);
149 | }
150 |
151 | /*
152 | * 文本记录
153 | */
154 |
155 | function logTextMessage(o, wxSession) {
156 | //debug("in webwxsync someone call me:" + inspect(o));
157 | // 查询用户名昵称
158 | if (o.FromUserName.startsWith("@@")) {
159 | logGroupTextMsg(o, wxSession);
160 | } else {
161 | logPrivateTextMsg(o, wxSession);
162 | }
163 | }
164 |
165 | function logPrivateTextMsg(o, wxSession) {
166 | handlePrivate(o.FromUserName, o.Content, wxSession)
167 | .then(logger.info, logger.error);
168 | }
169 |
170 | function logGroupTextMsg(o, wxSession) {
171 | handleGroup(o.FromUserName, o.Content, wxSession)
172 | .then(logger.info, logger.error);
173 | }
174 |
175 | /*
176 | * 群组或用户信息处理
177 | */
178 |
179 | function handlePrivate(username, replyContent, wxSession) {
180 | return new Promise((resolve, reject)=>{
181 | if (wxSession.memberList.findIndex(m=>m['UserName']==username) < 0) {
182 | // memberList中不存在
183 | var contactP = webwxbatchgetcontact(username, wxSession);
184 | } else {
185 | var contactP = Promise.resolve(wxSession);
186 | }
187 |
188 | contactP.then(_logPrivateTextMsg).catch(reject);
189 |
190 | function _logPrivateTextMsg(wxSession) {
191 | var m = wxSession.memberList.find(m=>m.UserName==username);
192 | resolve(convertEmoji(`[${m.NickName}说]${replyContent.replace(/
<]*\/?>/g, "")}`));
193 | }
194 | });
195 | }
196 |
197 |
198 | function handleGroup(groupUserName, replyContent, wxSession) {
199 | return new Promise((resolve, reject)=>{
200 | // debug("groupUserName:" + groupUserName);
201 | // debug("replyContent: " + replyContent);
202 | var result = /^(@[^:]+):
/mg.exec(replyContent);
203 | if (result) {
204 | var fromUserName = result[1];
205 | }
206 | // 查看是否缓存中有
207 | if (!(groupUserName in wxSession.groupContact)) {
208 | var contactP = webwxbatchgetcontact(groupUserName, wxSession)
209 | } else {
210 | var contactP = Promise.resolve(wxSession);
211 | }
212 |
213 | contactP.then(_logGroupTextMsg);
214 | // 记录群消息函数
215 | function _logGroupTextMsg(wxSession) {
216 | var groupRealName = wxSession.groupContact[groupUserName]['nickName'];
217 | var m = wxSession.groupContact[groupUserName]['memberList'].find(m=>m.UserName==fromUserName)
218 | resolve(convertEmoji(`[${groupRealName}]${m.NickName}${replyContent.replace(fromUserName, '').replace(/
<]*\/?>/g, "")}`));
219 | }
220 |
221 | });
222 | }
223 |
224 | /*
225 | * 未实现
226 | */
227 |
228 | function logNotImplementMsg(o, wxSession, context) {
229 | logger.error(`[${context}]未实现消息类型:${o.MsgType}`);
230 | }
231 |
232 | module.exports.wechatLogger = wechatLogger;
233 |
--------------------------------------------------------------------------------
/lib/reply/dialog.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | var request = require('request');
3 | var inspect = require('util').inspect;
4 | var apikeys = require('../../config/apikeys.js');
5 | var current = process.env.CURRENT_CASH ? parseFloat(process.env.CURRENT_CASH) : 0;
6 |
7 | function thesis(content) {
8 | return Promise.resolve("hello world");
9 | }
10 |
11 | function echo(content) {
12 | return Promise.resolve(content);
13 | }
14 |
15 | function turingRobot(content, userid) {
16 | content = content.replace(/^[^:]+:
/m, "");
17 | return new Promise((resolve, reject)=> {
18 | var url = `http://www.tuling123.com/openapi/api`
19 | request.get(
20 | url,
21 | {
22 | qs: {
23 | key: apikeys.turingRobotApiKey,
24 | info: content,
25 | userid: userid.slice(0, 32),
26 | },
27 | json: true,
28 | },
29 | (error, response, body)=>{
30 | if (error || !body) {
31 | reject(error?error:"turing robot return no body");
32 | }
33 | //debug("in turing machine: " + inspect(body))
34 | try {
35 | body.text = body.text.replace(/<\s*br\s*\/?\s*>/g, '\n');
36 | if (body.code == 100000) {
37 | resolve(body.text);
38 | } else if (body.code == 200000) {
39 | resolve(body.text + ": " + body.url);
40 | } else if (body.code == 302000) {
41 | resolve(body.list.map(n=>n.article + ": " + n.detailurl).join('\n'));
42 | } else if (body.code == 308000) {
43 | resolve(body.text + '\n' + body.list.map(n=>n.name + ": " + n.info + "<" + n.detailurl + ">").join('\n'));
44 | } else {
45 | reject(body.code + body.text);
46 | }
47 | } catch(e) {
48 | reject(e);
49 | }
50 | });
51 | });
52 | }
53 |
54 | function turingBaiduRobot(content, userid) {
55 | content = content.replace(/^[^:]+:
/m, "");
56 | return new Promise((resolve, reject)=> {
57 | var url = `http://apis.baidu.com/turing/turing/turing`
58 | request.get(
59 | url,
60 | {
61 | headers: {
62 | 'apikey': apikeys.turingBaiduRobotApiKey,
63 | },
64 | qs: {
65 | key: apikeys.turingBaiduRobotKey,
66 | info: content,
67 | userid: userid.slice(0, 32),
68 | },
69 | json: true,
70 | },
71 | (error, response, body)=>{
72 | if (error) {
73 | reject(error);
74 | }
75 | //debug("in turing machine: " + inspect(body))
76 | resolve(body.text);
77 | });
78 | });
79 | }
80 |
81 | function baiduDirect(content) {
82 | // FIXME: not work
83 | var mode;
84 | var re = /^([\u4E00-\u9FD5]+)从([\u4E00-\u9FD5])+到([\u4E00-\u9FD5]+)$/mg
85 | var result = re.exec(content);
86 | if (!result) {
87 | notFound();
88 | }
89 | switch (result[1]) {
90 | case "公交":
91 | mode = 'transit';
92 | break;
93 | case "步行":
94 | mode = 'walking';
95 | break;
96 | case "开车":
97 | mode = 'driving';
98 | break;
99 | default:
100 | notFound();
101 | }
102 | var origin = result[2];
103 | var destination = result[3];
104 |
105 | var p = new Promise((resolve, reject)=>{
106 | var param = {
107 | origin: origin,
108 | destination: destination,
109 | mode: 'transit',
110 | region: '北京',
111 | origin_region: '北京',
112 | destination_region: '北京',
113 | output: 'json',
114 | ak: apikeys.baiduDirectApiKey,
115 | };
116 |
117 | request('http://api.map.baidu.com/direction/v1',
118 | {
119 | qs: param,
120 | json: true,
121 | },
122 | (error, response, body)=>{
123 | if (error) {
124 | reject(error);
125 | }
126 | console.log(inspect(body));
127 | if (body.status != 0) {
128 | resolve(notFound());
129 | }
130 | if (body.type == 2) {
131 | resolve(body.routes[0].scheme.steps)
132 | } else if (body.type == 1) {
133 | resolve(notFound());
134 | }
135 | })
136 | })
137 | return p;
138 |
139 | function notFound() {
140 | return Promise.resolve("没有查询到相应的路线")
141 | }
142 | }
143 |
144 | function magic(content, userid) {
145 | // TODO: a magic trigger engine
146 | var result;
147 | var map = new Map();
148 | // map顺序其实是固定的,参见hacks.mozilla.org
149 | map.set(/好不好/g, '吼啊!');
150 | map.set(/那么早就说要([\u4E00-\u9FD5]+),会不会给人感觉是([\u4E00-\u9FD5]+)/g, '任何事,也要按照基本法!按照$1法来进行。刚才你问我,我可以回答一句「无可奉告」,但是你们又不高兴,我怎么办?我讲的意思不是要$2。你问我支持不支持,我说支持。我就明确告诉你这一点。我感觉你们文化界还需要学习,你们毕竟还是 too young ,你明白这意思吧?我告诉你们我是身经百战了,见得多了!');
151 | map.set(/(支持)|(支不支持)|(支持不支持)/g, '当然啦!');
152 | map.set(/(exciting)|(激动)|(感动)/g, '亦可赛艇!');
153 | map.set(/(谈笑风生)|(高到)|(不知哪里)/g, '北邮的邱神你知道么,比你高到不知哪里去了,我跟他谈笑风生,所以说你还需要学习一个。');
154 | map.set(/学习(一个)?/g, '所以说你们还是要提高自己的知识水平,识得唔识得啊?我为你们感到拙计呀……');
155 | map.set(/([\u4E00-\u9FD5]来[\u4E00-\u9FD5]去)/g, '你们有一个好,全世界跑到什么地方,你们比北邮的学生找工作跑得都快,但是$1这个水平呀,都 too simple , sometimes naive !懂了没有?');
156 | map.set(/(simple)|naive/ig, '你们啊!too simple , sometimes naive !懂了没有?');
157 | map.set(/(热情)|(不说话)/g, '但是我想我见到你们这样热情,一句话不说也不好。');
158 | map.set(/(发大财)|(发财)/g, '中国有一句话叫「闷声发大财」,我就什么话也不说,这是最好的。');
159 | map.set(/(负责)|(责任)/g, '在宣传上将来如果你们报道上有偏差,你们要负责任。');
160 | map.set(/(人生)|(经验)/g, '我有必要告诉你们一些人生的经验……');
161 | map.set(/大新闻/g, '你们不要想喜欢弄个大新闻,说现在已经定了,把我批判一番。');
162 | map.set(/naive/ig, '你们啊,naive!');
163 | map.set(/(angry)|很生气/ig, 'I am angry!你们这样子是不行的!我今天算是得罪了你们一下。');
164 | for (let reg of map) {
165 | if (result = reg[0].exec(content)) {
166 | return Promise.resolve(result[0].replace(reg[0], reg[1]));
167 | }
168 | }
169 | return Promise.resolve(turingRobot(content, userid));
170 | }
171 |
172 | function jizhangRobot(content) {
173 | // remove sender info in group chat session
174 | content = content.replace(/^[^:]+:
/m, "").trim();
175 | var match = /^(\+|\-)\s+(\d*\.\d+|\d+)\s+(.*)$/g.exec(content);
176 | if (match) {
177 | switch (match[1]) {
178 | case "+":
179 | current = current + parseFloat(match[2]);
180 | break;
181 | case "-":
182 | current = current - parseFloat(match[2]);
183 | break;
184 | default:
185 | current = current;
186 | }
187 | return Promise.resolve(`* ${current} 剩余社费`)
188 | } else {
189 | return Promise.reject(`格式错误`)
190 | }
191 |
192 | }
193 |
194 | module.exports.turingRobot = turingRobot;
195 | module.exports.echo = echo;
196 | module.exports.thesis = thesis;
197 | module.exports.baiduDirect = baiduDirect;
198 | module.exports.jizhangRobot = jizhangRobot;
199 |
--------------------------------------------------------------------------------
/lib/reply/reply.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var MSGTYPE_TEXT = require('../global.js').MSGTYPE_TEXT;
4 | var reply = require('./dialog.js').jizhangRobot;
5 | var webwxsendmsg = require('../webwx.js').webwxsendmsg;
6 | var webwxbatchgetcontact = require('../webwx.js').webwxbatchgetcontact;
7 | var convertEmoji = require('../util.js').convertEmoji;
8 |
9 | function generateReply(wxSession) {
10 | return o=>{
11 | var reply;
12 | switch (o.MsgType) {
13 | case MSGTYPE_TEXT:
14 | reply = generateTextMessage(o, wxSession);
15 | break;
16 | default:
17 | generateNotImplementMsg(o, wxSession, "generateReply");
18 | }
19 | return reply;
20 | }
21 | }
22 |
23 | function generateTextMessage(o, wxSession) {
24 |
25 | if (o.FromUserName.startsWith("@@") && (o.Content.includes("@" + wxSession.nickname))) {
26 | // FIXME: 用户名解析
27 | o.Content = o.Content.replace(/@[^:]+:
/g, '');
28 | // FIXME: at 我, 在Username NickName和群的displayName里
29 | // FIXME: 正则escape
30 | //o.Content = o.Content.replace(new RegExp('@' + wxSession.nickname), '喂, ');
31 | o.Content = o.Content.replace(new RegExp('@' + wxSession.nickname), '');
32 | } else if (o.FromUserName.startsWith("@@")) {
33 | // 查看是否缓存中有
34 | var groupUserName = o.FromUserName;
35 | if (!(groupUserName in wxSession.groupContact)) {
36 | var contactP = webwxbatchgetcontact(groupUserName, wxSession)
37 | } else {
38 | var contactP = Promise.resolve(wxSession);
39 | }
40 | contactP.then((wxSession) => {
41 | var groupRealName = wxSession.groupContact[o.FromUserName]['nickName'];
42 | if (groupRealName == "半条汪财政部") {
43 | o.Content = o.Content.replace(/@[^:]+:
/g, '');
44 | _sendContent(o);
45 | }
46 | })
47 | } else {
48 | // 其他群信息则不回复
49 | return;
50 | }
51 |
52 | function _sendContent(o) {
53 | // 回复
54 | var username = o.FromUserName; // 闭包,防止串号,血泪教训
55 | var replyPromise = reply(o.Content, o.FromUserName);
56 | // add then
57 | replyPromise.then((text)=>{
58 | webwxsendmsg(text, username, wxSession);
59 | }).catch((e) => {
60 | console.log(e)
61 | })
62 | }
63 |
64 | return o; // transducer if you like, however I won't
65 | }
66 |
67 | function generateNotImplementMsg(o, wxSession, context) {
68 | console.error("[" + context + "]未实现回复生成类型: " + o.MsgType);
69 | }
70 |
71 | module.exports.generateReply = generateReply;
72 |
--------------------------------------------------------------------------------
/lib/robot.js:
--------------------------------------------------------------------------------
1 | var synccheck = require('./webwx.js').synccheck;
2 | var webwxsync = require('./webwx.js').webwxsync;
3 | var handleMsg = require('./util.js').handleMsg;
4 |
5 | function robot(filters, transducers) {
6 | return (wxSession) => {
7 | synccheck(wxSession)
8 | .then(webwxsync(handleMsg(filters, transducers)))
9 | .then(robot(filters, transducers))
10 | .catch(console.error);
11 | }
12 | }
13 |
14 | module.exports.robot = robot;
15 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var webwxsendmsg = require('./webwx.js').webwxsendmsg;
4 | var SPECIAL_USERS = require('./global.js').SPECIAL_USERS;
5 |
6 |
7 | /**
8 | * 缓存通讯录
9 | * @param {Object} modContactList - modContactList对象
10 | * @param {Object} wxSession - 微信会话
11 | */
12 | function cacheContact(modContactList, wxSession) {
13 | modContactList.forEach(o=>{
14 | if (o.UserName.startsWith('@@')) { // 群组直接替换了
15 | // console.log('群缓存更新', o.NickName)
16 | wxSession.groupContact[o.UserName] = {
17 | nickName: o.NickName,
18 | memberList: o.MemberList,
19 | }
20 | } else { // 用户
21 | // 如果不在缓存中
22 | var index = wxSession.memberList.findIndex(user=> user['UserName'] == o.UserName);
23 | if (index < 0) {
24 | // console.log('用户缓存推入', o.NickName)
25 | wxSession.memberList.push(o);
26 | } else {
27 | // console.log('用户缓存替换', o.NickName)
28 | wxSession.memberList[index] = o;
29 | }
30 | }
31 | });
32 | }
33 |
34 |
35 | /*
36 | * 消息处理
37 | * @param {Array} filter - 过滤
38 | * @param {Array} transducers - 并行处理
39 | * @return {Function} - 接受addMsgList和wxSession的函数
40 | */
41 | function handleMsg(filters, transducers) {
42 | return (addMsgList, wxSession) => {
43 | var replys = addMsgList
44 | .filter(o=>(o.ToUserName === wxSession.username)) // 过滤不是给我的信息
45 | .filter(o=>(SPECIAL_USERS.indexOf(o.FromUserName) < 0)); // 不是特殊用户
46 |
47 | filters.forEach(f=> {
48 | replys = replys.filter(f(wxSession));
49 | });
50 |
51 | transducers.push((wxSession)=>(o)=>Promise.resolve(o)); // 默认transducers,Promise化reply
52 |
53 | transducers.forEach(f=> {
54 | replys = replys.map(f(wxSession));
55 | });
56 |
57 | replys.map(r=>r.catch(console.error)); // 错误捕获
58 | }
59 | }
60 |
61 | /*
62 | * emoji处理
63 | * @param {String} - 待转换emoji文本
64 | * @return {String} - 处理后的文本
65 | * FIXME: 检查该函数
66 | */
67 | function convertEmoji(s) {
68 | return s.replace(/<\/span>/g, (a, b) => {
69 | try {
70 | let s = null
71 | if (b.length == 4 || b.length == 5) {
72 | s = ['0x' + b]
73 | } else if (b.length == 8) {
74 | s = ['0x' + b.slice(0, 4), '0x' + b.slice(4, 8)]
75 | } else if (b.length == 10) {
76 | s = ['0x' + b.slice(0, 5), '0x' + b.slice(5, 10)]
77 | } else {
78 | throw new Error('unknown emoji characters')
79 | }
80 | return String.fromCodePoint.apply(null, s)
81 | } catch (err) {
82 | error(b, err)
83 | }
84 | })
85 | }
86 |
87 | module.exports.cacheContact = cacheContact;
88 | module.exports.handleMsg = handleMsg;
89 | module.exports.convertEmoji = convertEmoji;
90 |
--------------------------------------------------------------------------------
/lib/webwx.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var winston = require('winston');
3 | var LEVEL = process.env.DEBUG || 'error'
4 |
5 | var logger = new (winston.Logger)({
6 | level: LEVEL,
7 | transports: [
8 | new (winston.transports.Console)(),
9 | ]
10 | });
11 |
12 | var verbose = (text)=>logger.log('verbose', text);
13 | var info = (text)=>logger.log('info', text);
14 | var error = (text)=>logger.log('error', text);
15 |
16 | var inspect = require('util').inspect;
17 | var request = require('request');
18 |
19 | var querystring = require('querystring');
20 | var fs = require('fs');
21 |
22 | var cacheContact = require('./util.js').cacheContact;
23 | var MINE_EXT = require('./global.js').MINE_EXT;
24 |
25 | /** uuid promise */
26 | var getUUID = new Promise((resolve, reject)=>{
27 | var param = {
28 | appid: 'wx782c26e4c19acffb',
29 | fun: 'new',
30 | redirect_uri: 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage',
31 | lang: 'en_US',
32 | _: Date.now()
33 | }
34 |
35 | var uri = '/jslogin';
36 |
37 | //verbose(uri);
38 |
39 | var options = {
40 | uri: uri,
41 | baseUrl: 'https://login.weixin.qq.com',
42 | method: 'GET',
43 | qs: param,
44 | };
45 |
46 | info('getuuid')
47 | var req = request(options, (error, response, body)=>{
48 | verbose(body);
49 | if (error) {
50 | //verbose(error);
51 | return reject(error);
52 | }
53 | resolve(body);
54 | });
55 | });
56 |
57 | /**
58 | * 获取UUID
59 | * @param {string} body - 要解析的body
60 | * @return {Boolean} 标识是否成功获取uuid
61 | */
62 | function checkAndParseUUID(body) {
63 | var result = /window.QRLogin.code = (\d+); window.QRLogin.uuid = "([^"]+)";/.exec(body);
64 | //verbose("checkAndParseUUID");
65 | if (!result || result[1] != '200') {
66 | return false;
67 | }
68 | return result[2];
69 | }
70 |
71 | /**
72 | * 展示二维码
73 | * @param {Object} display - display Stream 对象
74 | * @return {Promise} session对象
75 | */
76 | function showQRImage(display) {
77 | return (uuid) => {
78 | console.log("请扫描二维码并确认登录,关闭二维码窗口继续...");
79 | var QRUrl = 'https://login.weixin.qq.com/qrcode/' + uuid;
80 |
81 | var checkLoginPromise = new Promise((resolve, reject)=> {
82 | display.on('exit', wxSessionStop);
83 | info("GET " + QRUrl)
84 | var req = request(QRUrl, {agentOptions: {keepAlive: true}} )
85 | //req.on('response', ()=>{
86 | resolve({
87 | uuid: uuid,
88 | display: display, // 将display传递下去
89 | tip: 1, //标识
90 | });
91 | //})
92 | req.pipe(display.stdin);
93 | req.on('error', (err)=>{
94 | return reject(err);
95 | })
96 | });
97 |
98 | return checkLoginPromise;
99 | // 登录
100 | }
101 | }
102 |
103 | /**
104 | * 检查扫描二维码状况
105 | * @param {Object} wxSession - 微信会话
106 | * @return {Promise} wxSession对象
107 | */
108 | function checkLogin(wxSession) {
109 | var timestamp = ~Date.now();
110 | var uuid = wxSession.uuid;
111 | var display = wxSession.display;
112 | // 检查登录和跳转
113 | return new Promise((resolve, reject)=> {
114 | var checkUrl = `https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&tip=${wxSession.tip}&uuid=${uuid}&r=${timestamp}`
115 | info('GET ' + checkUrl)
116 | request(checkUrl,
117 | {agentOptions: {keepAlive: true}},
118 | (error, response, body)=>{
119 | verbose(body);
120 | if (error) {
121 | return reject(error);
122 | }
123 | verbose("in checkLogin: " + body);
124 | if (/window\.code=200/.test(body)) {
125 | console.log("登录微信...");
126 | // 删除退出子进程杀掉主进程的回调
127 | display.removeListener('exit', wxSessionStop)
128 | display.kill();
129 | resolve(body);
130 | } else if(/window\.code=201/.test(body)){
131 | wxSession.tip = 0; // 第一次之后tip都为0,不然下一个请求不是长连接
132 | // NOTE: 在这里我试了一会儿
133 | // 关键是对promise的理解。
134 | // !! 总结!!
135 | console.log("已扫描,请点击确认登录");
136 | resolve(checkLogin(wxSession));
137 | } else if(/window\.code=408/.test(body)){
138 | resolve(checkLogin(wxSession));
139 | } else {
140 | console.log("验证码超时...")
141 | display.kill();
142 | wxSessionStop(1);
143 | }
144 | });
145 | });
146 | }
147 |
148 | /**
149 | * 解析登录地址
150 | * @param {String} body - 返回体
151 | * @return {String} 登录地址
152 | */
153 | function parseRedirectUrl(text) {
154 | var result = /window\.redirect_uri="([^"]+)";/.exec(text);
155 | // verbose("parse redirect_uri: " + inspect(result));
156 | if (!result) {
157 | console.log("无重定向地址");
158 | wxSessionStop(1);
159 | }
160 | return result[1]
161 | }
162 |
163 | /**
164 | * 登录
165 | * @param {String} redirectUrl - 登录地址
166 | * @return {Promise} 返回体Promise
167 | */
168 | function login(redirectUrl) {
169 | verbose("redirectUrl in login:" + redirectUrl);
170 | var wxSession = Object.create(null);
171 | wxSession.wxJar = request.jar();
172 | return new Promise((resolve, reject)=> {
173 | info('GET ' + redirectUrl);
174 | request.get({
175 | url: redirectUrl,
176 | jar: wxSession.wxJar,
177 | followRedirect: false,
178 | agentOptions: {
179 | keepAlive: true
180 | },
181 | headers: {
182 | 'Host': 'wx.qq.com'
183 | }
184 | },
185 | (error, response, body)=>{
186 | verbose(response.statusCode)
187 | // server set cookie here,之后的操作需要cookie
188 | if (error) {
189 | return reject(error);
190 | }
191 | wxSession.loginBody = body;
192 | resolve(wxSession);
193 | })
194 | });
195 | }
196 |
197 | /**
198 | * 获取baseRequest函数
199 | * @param {String} wxSession - 登录时返回体
200 | * @return {Object} 包含BaseRequest和pass_ticket对象
201 | */
202 | function getbaseRequest(wxSession) {
203 | var text = wxSession.loginBody;
204 | verbose("getbaseRequest: " + text)
205 | var skey = new RegExp('([^<]+)');
206 | var wxsid = new RegExp('([^<]+)');
207 | var wxuin = new RegExp('([^<]+)');
208 | var pass_ticket = new RegExp('([^<]+)');
209 | // dirty hack
210 | var skey = skey.exec(text);
211 | var wxsid = wxsid.exec(text);
212 | var wxuin = wxuin.exec(text);
213 | var pass_ticket = pass_ticket.exec(text);
214 |
215 | wxSession.BaseRequest = {
216 | Skey: skey[1],
217 | Sid: wxsid[1],
218 | Uin: wxuin[1],
219 | };
220 | wxSession.pass_ticket = pass_ticket[1];
221 |
222 | return wxSession;
223 | }
224 |
225 | /**
226 | * webwxinit
227 | * @param {Object} wxSession - 微信会话
228 | * @return {Promise} 代表微信会话的Promise
229 | */
230 | function webwxinit(wxSession) {
231 | console.log("登录成功,初始化");
232 | // 参见uproxy_wechat,使用面向对象的方式实现变量传递
233 | wxSession.groupContact = Object.create(null);
234 | return new Promise((resolve, reject)=> {
235 | //verbose("in webwxinit wxSession:\n" + inspect(wxSession));
236 | var postData = {BaseRequest: wxSession.BaseRequest};
237 | //verbose("in webwxinit postData: " + postData);
238 | var timestamp = Date.now();
239 | var options = {
240 | baseUrl: 'https://wx.qq.com',
241 | uri: `/cgi-bin/mmwebwx-bin/webwxinit?lang=en_US&pass_ticket=${wxSession.pass_ticket}`,
242 | method: 'POST',
243 | body: postData,
244 | json: true,
245 | agentOptions: {
246 | keepAlive: true
247 | },
248 | headers: {
249 | 'Accept': '*/*',
250 | 'Connection': 'keep-alive',
251 | 'Host': 'wx.qq.com'
252 | },
253 | jar: wxSession.wxJar,
254 | }
255 | info(options.method + " " + options.baseUrl + options.uri);
256 | var req = request(options, (error, response, body) => {
257 | verbose(body);
258 | if (error) {
259 | return reject(error);
260 | }
261 | //verbose("In webwxinit body: " + inspect(body));
262 | // fs.writeFile('init.json', JSON.stringify(body));
263 | wxSession.username = body['User']['UserName'];
264 | wxSession.nickname = body['User']['NickName'];
265 | wxSession.SyncKey = body['SyncKey'];
266 | resolve(wxSession);
267 | })
268 | });
269 | }
270 |
271 |
272 | /**
273 | * @param {Object} wxSession - 微信会话
274 | * @return {Promise} 代表微信会话的Promise
275 | */
276 | function webwxgetcontact(wxSession) {
277 | console.log("初始化成功,获取联系人...")
278 | return new Promise((resolve, reject)=> {
279 | var skey = wxSession.BaseRequest.Skey;
280 | var pass_ticket = wxSession.pass_ticket;
281 | // var jsonFile = fs.createWriteStream('contact.json');
282 | var timestamp = Date.now();
283 | var options = {
284 | baseUrl: 'https://wx.qq.com',
285 | uri: `/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=en_US&pass_ticket=${pass_ticket}&skey=${skey}&seq=0&r=${timestamp}`,
286 | method: 'GET',
287 | json: true,
288 | agentOptions: {
289 | keepAlive: true
290 | },
291 | headers: {
292 | 'Host': 'wx.qq.com'
293 | },
294 | jar: wxSession.wxJar,
295 | }
296 | info(options.method + options.baseUrl + options.uri);
297 | request(options, (error, response, body)=>{
298 | if (error) {
299 | return reject(error);
300 | }
301 | if (!body || body.BaseResponse.Ret !== 0) {
302 | verbose('webwxgetcontact no body: ' + inspect(body));
303 | resolve(wxSession);
304 | return;
305 | }
306 | verbose(body)
307 | // fs.writeFile('contact.json', JSON.stringify(body));
308 | wxSession.memberList = body.MemberList;
309 | //wxSession.toUser = memberList.filter(m=>(m.NickName == "核心活动都是玩玩玩吃吃吃的北邮GC"))[0]['UserName'];
310 | console.log("联系人获取完毕...");
311 | console.log("<--OK-->");
312 | resolve(wxSession);
313 | });
314 | })
315 | }
316 |
317 | /**
318 | * @param {String} msg - 准备发送的消息
319 | * @param {String} toUser - 用户username
320 | * @param {Object} wxSession - 微信会话
321 | * @return {Promise} 代表微信会话的Promise
322 | */
323 | function webwxsendmsg(msg, toUser, wxSession) {
324 | var msgId = (Date.now() + Math.random().toFixed(3)).replace('.', '');
325 | var BaseRequest = wxSession.BaseRequest;
326 | var pass_ticket = wxSession.pass_ticket;
327 | var postData = {
328 | BaseRequest: BaseRequest,
329 | Msg: {
330 | "Type": 1,
331 | "Content": msg,
332 | "FromUserName": wxSession.username,
333 | "ToUserName": toUser,
334 | "LocalID": msgId,
335 | "ClientMsgId": msgId}
336 | };
337 | var options = {
338 | baseUrl: 'https://wx.qq.com',
339 | uri: `/cgi-bin/mmwebwx-bin/webwxsendmsg?lang=en_US&pass_ticket=${pass_ticket}`,
340 | method: 'POST',
341 | jar: wxSession.wxJar,
342 | json: true,
343 | body: postData,
344 | };
345 |
346 | info("webwxsendmsg:" + options.method + " " + options.baseUrl + options.uri)
347 | request(options, (error, response, body)=>{
348 | verbose(body);
349 | if (!error) {
350 | console.log("发送-> ", msg);
351 | }
352 | });
353 | }
354 |
355 | /**
356 | * @param {Object} wxSession - 微信会话
357 | * @return {Promise} 代表微信会话的Promise
358 | */
359 | function synccheck(wxSession) {
360 | //https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=1452482036596&skey=%40crypt_3bb2969_2e63a3568c783f0d4a9afbab8ba9f0d2&sid=be%2FeK3jB4eicuZct&uin=2684027137&deviceid=e203172097127147&synckey=1_638107724%7C2_638108703%7C3_638108650%7C1000_1452474264&_=1452482035266
361 | return new Promise((resolve, reject)=>{
362 | // FIXME: 在这里检查合适吗?每次synccheck开始的时候
363 | if (wxSession.socketFail >= 6) {
364 | reject(new Error('wxSession socket fail TOO MUCH(maybe network lost)'));
365 | wxSessionStop(3);
366 | return;
367 | }
368 | // 重置wxSession.webwxsync, 默认不需要webwxsync
369 | wxSession.webwxsync = false;
370 | var timestamp = Date.now();
371 | var skey = wxSession.BaseRequest.Skey;
372 | var sid = wxSession.BaseRequest.Sid;
373 | var uin = wxSession.BaseRequest.Uin;
374 | var deviceid = 'e' + ('' + Math.random().toFixed(15)).substring(2, 17)
375 | var synckey = wxSession.SyncKey.List.map(o=>o.Key + '_' + o.Val).join('|');
376 | var options = {
377 | baseUrl: 'https://webpush.wx.qq.com',
378 | uri: '/cgi-bin/mmwebwx-bin/synccheck',
379 | method: 'GET',
380 | qs: {
381 | r: timestamp,
382 | skey: skey,
383 | sid: sid,
384 | uin: uin,
385 | deviceid: deviceid,
386 | synckey: synckey,
387 | },
388 | forever: true,
389 | headers: {
390 | 'Host': 'webpush.wx.qq.com',
391 | 'Referer': 'https://wx.qq.com/',
392 | 'Connection': 'keep-alive'
393 | },
394 | jar: wxSession.wxJar,
395 | timeout: 35000, // 源码这么写的
396 | }
397 |
398 | info(options.method + " " + options.baseUrl + options.uri)
399 | verbose(options.qs)
400 | request(options, (error, response, body)=>{
401 |
402 | verbose(body)
403 | wxSession.webwxsync = false;
404 |
405 | if (error || !(/retcode:"0"/.test(body)) ){ // 有时候synccheck失败仅仅返回空而没有失败?
406 | wxSession.socketFail = (wxSession.socketFail || 0) + 1;
407 | resolve(wxSession);
408 | return;
409 | }
410 | // 正常
411 | wxSession.socketFail = 0;
412 |
413 | verbose(body);
414 |
415 | if (body == 'window.synccheck={retcode:"1101",selector:"0"}') {
416 | console.log("服务器断开连接,退出程序")
417 | reject(new Error('wxSessionStop'))
418 | } else if (body !== 'window.synccheck={retcode:"0",selector:"0"}') {
419 | wxSession.webwxsync = true; // 标识有没有新消息,要不要websync
420 | } else {
421 | wxSession.webwxsync = false; // 还是写出来清晰一些
422 | }
423 | resolve(wxSession);
424 | });
425 | });
426 | }
427 |
428 | /**
429 | * @param {Object} wxSession - 微信会话
430 | * @return {Function} 接受wxSession的函数该函数返回包含wxSession的Promise
431 | */
432 | function webwxsync(handleMsg) {
433 | return (wxSession)=>{
434 | // https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xWam498tVKzNaHLt&skey=@crypt_3bb2969_a8ec83465d303fb83bf7ddcf512c081d&lang=en_US&pass_ticket=YIBmwsusvnbs8l7Z4wtRdBXtslA8JjyHxsy0Fsf3PN8NTiP3fzhjB9rOE%252Fzu6Nur
435 | if (!wxSession.webwxsync) {
436 | return Promise.resolve(wxSession);
437 | }
438 | return new Promise((resolve, reject) => {
439 | //verbose('wxSession in webwxsync:\n' + inspect(wxSession));
440 | var BaseRequest = wxSession.BaseRequest;
441 | var pass_ticket = wxSession.pass_ticket;
442 | var rr = ~Date.now();
443 | var postData = {
444 | BaseRequest: wxSession.BaseRequest,
445 | SyncKey: wxSession.SyncKey
446 | };
447 | var options = {
448 | baseUrl: 'https://wx.qq.com',
449 | uri: `/cgi-bin/mmwebwx-bin/webwxsync?sid=${wxSession.BaseRequest.Sid}&skey=${wxSession.BaseRequest.Skey}&lang=en_US&pass_ticket=${pass_ticket}&rr=${rr}`,
450 | method: 'POST',
451 | body: postData,
452 | json: true,
453 | headers: {
454 | 'Host': 'wx.qq.com'
455 | },
456 | jar: wxSession.wxJar,
457 | timeout: 15e3, // 不设定又会hang
458 | }
459 |
460 | info(options.method + " " + options.baseUrl + options.uri)
461 | request(options, (error, response, body)=>{
462 | verbose(body);
463 | // 经常出现socket hang up或者timeout的网络问题
464 | if (error) {
465 | //reject(error);
466 | verbose('webwxsync fail: ' + inspect(error));
467 | resolve(wxSession);
468 | return;
469 | }
470 | if (!body || body.BaseResponse.Ret !== 0) {
471 | verbose('webwxsync no body: ' + inspect(body));
472 | resolve(wxSession);
473 | return;
474 | }
475 | // 更新 synckey
476 | wxSession.SyncKey = body.SyncKey;
477 | //verbose("in websync body: " + inspect(body))
478 |
479 | // 更新联系人如果有的话
480 | cacheContact(body.ModContactList, wxSession);
481 | // 消息处理更新
482 | handleMsg(body.AddMsgList, wxSession);
483 | resolve(wxSession);
484 | });
485 | });
486 | }
487 | }
488 |
489 | /**
490 | * @param {String} username - 用户名
491 | * @param {Object} wxSession - 微信会话
492 | * @return {Function} 接受wxSession的函数该函数返回包含wxSession的Promise
493 | */
494 | function webwxbatchgetcontact(username, wxSession) {
495 | return new Promise((resolve, reject)=>{
496 | var postData = {
497 | BaseRequest: wxSession.BaseRequest,
498 | Count: 1,
499 | List: [
500 | {
501 | UserName: username,
502 | EncryChatRoomId: "",
503 | }
504 | ]
505 | };
506 | // console.log("为啥Promise里看不到运行情况")
507 | info('POST ' + `https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact`);
508 | request.post(`https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact`,
509 | {
510 | qs: {
511 | type: 'ex',
512 | r: Date.now(),
513 | },
514 | body: postData,
515 | json: true,
516 | jar: wxSession.wxJar,
517 | },
518 | (error, response, body)=> {
519 | verbose(body);
520 | // 错误处理
521 | if (error || !body) {
522 | return reject(error)
523 | }
524 | if (body.BaseResponse.Ret != 0) {
525 | return reject(body.BaseResponse.ErrMsg);
526 | }
527 | // 本地缓存
528 | if (!username.startsWith('@@')) { // 个人
529 | var user = body.ContactList[0]
530 | wxSession.memberList.push(user);
531 | } else { // 群组
532 | var group = body.ContactList[0]
533 | var groupRealName = group.NickName;
534 | var memberList = group.MemberList;
535 | wxSession.groupContact[username] = {
536 | memberList: memberList,
537 | nickName: groupRealName,
538 | };
539 | }
540 | resolve(wxSession);
541 | });
542 | });
543 | }
544 |
545 | /**
546 | * @param {String} msgId - 消息id
547 | * @param {Object} wxSession - 微信会话
548 | * @param {String} imgPath - 图像保存路径
549 | * @return {Promise} - 返回图像路径Promise
550 | */
551 | function webwxgetmsgimg(o, wxSession, imgPath){
552 | var msgId = o.MsgId
553 | return new Promise((resolve, reject)=>{
554 | var imgUrl = `https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg?&MsgID=${msgId}&skey=${querystring.escape(wxSession.BaseRequest.Skey)}`;
555 | // 保存图片到文件
556 | var req = request.get(imgUrl, {jar: wxSession.wxJar})
557 | req.on('error', (e) => {
558 | error('下载图像资源失败:', e);
559 | reject(e) //may not work if stream error
560 | })
561 | .on('response', (res) => {
562 | var ext = MINE_EXT[res.headers['content-type']]
563 | imgPath = imgPath + '.' + ext;
564 | req.pipe(fs.createWriteStream(imgPath));
565 | resolve(imgPath);
566 | })
567 | })
568 | }
569 |
570 | /**
571 | * @param {String} o - 消息对象
572 | * @param {Object} wxSession - 微信会话
573 | * @param {String} voicePath - 音频保存路径
574 | * @return {Promise} - 返回音频路径的Promise
575 | */
576 | function webwxgetvoice(o, wxSession, voicePath){
577 | var msgId = o.MsgId
578 | return new Promise((resolve, reject)=>{
579 | var voiceUrl = `https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice?&MsgID=${msgId}&skey=${querystring.escape(wxSession.BaseRequest.Skey)}`;
580 | // 保存图片到文件
581 | var req = request.get(voiceUrl, {jar: wxSession.wxJar});
582 | req.on('error', (e) => {
583 | error('下载音频资源失败:', e);
584 | reject(e)
585 | })
586 | .on('response', (res) => {
587 | var ext = MINE_EXT[res.headers['content-type']]
588 | voicePath = voicePath + '.' + ext;
589 | req.pipe(fs.createWriteStream(voicePath))
590 | resolve(voicePath)
591 | })
592 | });
593 | }
594 |
595 | /**
596 | * @param {String} o - 消息对象
597 | * @param {Object} wxSession - 微信会话
598 | * @param {String} videoPath - 视频保存路径
599 | * @return {Promise} - 返回视频路径的Promise
600 | */
601 | function webwxgetvideo(o, wxSession, videoPath){
602 | var msgId = o.MsgId
603 | return new Promise((resolve, reject)=>{
604 | var voiceUrl = `https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo?&MsgID=${msgId}&skey=${querystring.escape(wxSession.BaseRequest.Skey)}`;
605 | // 保存图片到文件
606 | var req = request.get(voiceUrl, {
607 | jar: wxSession.wxJar,
608 | headers: {
609 | Range: 'bytes=0-',
610 | }
611 | });
612 | req.on('error', (e) => {
613 | error('下载视频资源失败:', e);
614 | reject(e);
615 | })
616 | .on('response', (res) => {
617 | var ext = MINE_EXT[res.headers['content-type']]
618 | videoPath = videoPath + '.' + ext;
619 | req.pipe(fs.createWriteStream(videoPath))
620 | resolve(videoPath);
621 | })
622 | });
623 | }
624 |
625 | /**
626 | * 获取表情
627 | * @param {String} o - 消息对象
628 | * @param {Object} wxSession - 微信会话
629 | * @param {String} emotionPath - 视频保存路径
630 | * @return {Promise} - 返回表情保存路径的Promise
631 | */
632 | function webwxgetemoticon(o, wxSession, emotionPath){
633 | if (o.HasProductId) {
634 | return Promise.resolve("只能在手机上查看的表情")
635 | }
636 | var result = /cdnurl\s*=\s*"([^"]+)"/.exec(o.Content);
637 | if (result) {
638 | var emotionUrl = result[1];
639 | } else {
640 | return Promise.reject(new Error("NotValidEmoticon"))
641 | }
642 | return new Promise((resolve, reject)=>{
643 | if (!emotionUrl){
644 | let e = new Error("EmotionImageNotFound")
645 | e.message = o.Content
646 | return reject()
647 | }
648 | // 保存图片到文件
649 | // console.log(emotionUrl);
650 | var req = request.get(emotionUrl)
651 | req.on('error', (e) => {
652 | error('下载表情失败:', e);
653 | reject(e);
654 | })
655 | .on('response', (res) => {
656 | var ext = MINE_EXT[res.headers['content-type']]
657 | emotionPath = emotionPath + '.' + ext;
658 | req.pipe(fs.createWriteStream(emotionPath))
659 | resolve(emotionPath);
660 | })
661 | });
662 | }
663 |
664 | /**
665 | * @param {String} o - 消息对象
666 | * @param {Object} wxSession - 微信会话
667 | * @param {String} filePath - 文件保存路径
668 | * @return {Promise} - 返回文件路径的Promise
669 | */
670 | function webwxgetmedia(o, wxSession, filePath){
671 | var msgId = o.MsgId
672 | var pass_ticket = wxSession.pass_ticket;
673 | var uin = wxSession.BaseRequest.Uin;
674 | var webwx_data_ticket = wxSession.wxJar._jar.store.idx['qq.com']['/']['webwx_data_ticket'].value;
675 | return new Promise((resolve, reject)=>{
676 | var fileUrl = `https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia`;
677 | var params = {
678 | sender: o.FromUserName,
679 | mediaid: o.MediaId,
680 | filename: o.FileName,
681 | pass_ticket: pass_ticket,
682 | fromuser: uin,
683 | webwx_data_ticket: webwx_data_ticket,
684 | }
685 | // 保存图片到文件
686 | var req = request.get(fileUrl, {
687 | qs: params,
688 | headers: {
689 | Range: 'bytes=0-',
690 | },
691 | jar: wxSession.wxJar,
692 | });
693 | req.on('error', (e) => {
694 | error('下载文件资源失败:', e);
695 | reject(e);
696 | })
697 | .on('response', (res) => {
698 | filePath = filePath + '-' + o.FileName;
699 | req.pipe(fs.createWriteStream(filePath))
700 | resolve(filePath);
701 | })
702 | });
703 | }
704 | /**
705 | * 登出
706 | * @param {Object} wxSession - 微信会话
707 | * @return {Promise} - 返回登出结果的Promise
708 | */
709 | function webwxlogout(wxSession){
710 | var skey = wxSession.BaseRequest.Skey;
711 | var sid = wxSession.BaseRequest.Sid;
712 | var uin = wxSession.BaseRequest.Uin;
713 | var param = {
714 | redirect: "1",
715 | type: "0",
716 | skey: skey,
717 | }
718 | return new Promise((resolve, reject)=> {
719 | var formData = {
720 | sid: sid,
721 | uin: uin
722 | };
723 | var options = {
724 | baseUrl: 'https://wx.qq.com',
725 | uri: `/cgi-bin/mmwebwx-bin/webwxlogout`,
726 | method: 'POST',
727 | qs: param,
728 | form: formData,
729 | jar: wxSession.wxJar,
730 | }
731 | info(options.method + options.baseUrl + options.uri);
732 | var req = request(options, (error, response, body) => {
733 | verbose(body);
734 | if (error) {
735 | return reject(error);
736 | }
737 | if (response.statusCode == 301) {
738 | info('成功登出');
739 | return resolve(true);
740 | } else {
741 | error('登出失败');
742 | return resolve(false);
743 | }
744 | })
745 | });
746 | }
747 |
748 | // FIXME: clean it!
749 | /**
750 | * @param {Number} code - 错误码
751 | */
752 | function wxSessionStop(code, signal) {
753 | console.log('结束会话');
754 | throw new Error('wxSessionStop:' + code);
755 | }
756 |
757 | module.exports.getUUID = getUUID;
758 | module.exports.checkAndParseUUID = checkAndParseUUID;
759 | module.exports.showQRImage = showQRImage;
760 | module.exports.checkLogin = checkLogin;
761 | module.exports.parseRedirectUrl = parseRedirectUrl;
762 | module.exports.login = login;
763 | module.exports.getbaseRequest = getbaseRequest;
764 | module.exports.webwxinit = webwxinit;
765 | module.exports.webwxgetcontact = webwxgetcontact;
766 | module.exports.synccheck = synccheck;
767 | module.exports.webwxsync = webwxsync;
768 | module.exports.webwxsendmsg = webwxsendmsg;
769 | module.exports.webwxbatchgetcontact = webwxbatchgetcontact;
770 | module.exports.webwxgetmsgimg = webwxgetmsgimg;
771 | module.exports.webwxgetvoice = webwxgetvoice;
772 | module.exports.webwxgetvideo = webwxgetvideo;
773 | module.exports.webwxgetemoticon = webwxgetemoticon;
774 | module.exports.webwxlogout = webwxlogout;
775 | module.exports.webwxgetmedia = webwxgetmedia;
776 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wechat-user-bot",
3 | "version": "0.1.5",
4 | "description": "A wechat bot for normal users. Based on web wechat.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/HalfdogStudio/wechat-user-bot.git"
12 | },
13 | "keywords": [
14 | "wechat",
15 | "weixin",
16 | "robot"
17 | ],
18 | "author": "Liu Yuyang (sa@linuxer.me)",
19 | "license": "GPL-3.0",
20 | "bugs": {
21 | "url": "https://github.com/HalfdogStudio/wechat-user-bot/issues"
22 | },
23 | "homepage": "https://github.com/HalfdogStudio/wechat-user-bot#readme",
24 | "dependencies": {
25 | "request": "^2.67.0",
26 | "winston": "^2.1.1"
27 | },
28 | "devDependencies": {
29 | "mocha": "^2.3.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/screenshots/0.1.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HalfdogStudio/wechat-user-bot/8617f85931360319d25479a12768b3cb1cd24001/screenshots/0.1.3.png
--------------------------------------------------------------------------------
/screenshots/0.1.4-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HalfdogStudio/wechat-user-bot/8617f85931360319d25479a12768b3cb1cd24001/screenshots/0.1.4-2.gif
--------------------------------------------------------------------------------
/screenshots/0.1.4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HalfdogStudio/wechat-user-bot/8617f85931360319d25479a12768b3cb1cd24001/screenshots/0.1.4.gif
--------------------------------------------------------------------------------