├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── README.md
├── bot-qrcode.jpg
├── index.js
├── media
├── test.gif
├── test.mp4
└── test.txt
├── package-lock.json
├── package.json
├── run-core.js
├── src
├── core.js
├── interface
│ ├── contact.js
│ └── message.js
├── util
│ ├── conf.js
│ ├── global.js
│ ├── index.js
│ └── request.js
└── wechat.js
├── test
├── nock.js
├── response
│ ├── webwxbatchgetcontact
│ ├── webwxgetcontact
│ ├── webwxinit
│ └── webwxsync
└── unit.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # 4 space indentation
2 | [*]
3 | indent_style = space
4 | indent_size = 2
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "arrow-parens": [2, "as-needed"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | **/build/app.js
25 | **/build/app.js.map
26 | /lib
27 |
28 | # Dependency directory
29 | node_modules
30 |
31 | # Optional npm cache directory
32 | .npm
33 |
34 | # Optional REPL history
35 | .node_repl_history
36 |
37 | # personal
38 | /.vscode
39 | /typings
40 | .idea/*
41 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Example
24 | example
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | # personal
36 | /.vscode
37 | /typyings
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wechat4u.js
2 |
3 |  [](https://www.npmjs.org/package/wechat4u) [](http://www.qr-code-generator.com/phpqrcode/getCode.php?cht=qr&chl=http%3A%2F%2Fweixin.qq.com%2Fg%2FA1zJ47b19KtgMnAx&chs=180x180&choe=UTF-8&chld=L|0)
4 |
5 | ## Announcing wechat4u v0.7.14
6 |
7 | ### Features
8 |
9 | - 导出和导入保持微信登录的必要数据 bot.botData ([#160](https://github.com/nodeWechat/wechat4u/pull/160))
10 | - 修改联系人备注 bot.updateRemarkName(UserName, RemarkName) ([#121](https://github.com/nodeWechat/wechat4u/pull/121))
11 | - 修改群名 bot.updateChatRoomName(ChatRoomUserName, NewName) ([#168](https://github.com/nodeWechat/wechat4u/pull/168))
12 | - 转发消息 bot.forwardMsg(msg, toUserName)
13 | - 撤回消息 bot.revokeMsg(MsgID, toUserName)
14 |
15 | ### Changes
16 |
17 | - 修复大文件上传失败问题
18 | - 支持uos协议,所有微信均可使用,请先微信实名认证后使用
19 | - 发送消息的一类方法在成功时会返回完整响应数据
20 | - bot.user 对象中不再存储用户头像的 base64 数据
21 | - 移除 example 目录
22 | - 修复 Contact 和 Message 中数据某些数据不可枚举
23 | - 向上层代码传递完整的 Error 对象,并将原来的中文错误描述放在 err.tips
24 | - bot.getContact(Seq) 方法增加 Seq 参数,支持增量获取完整联系人
25 |
26 | ## 安装使用
27 |
28 | ```
29 | npm install --save wechat4u@latest
30 | ```
31 |
32 | ```javascript
33 | const Wechat = require('wechat4u')
34 | let bot = new Wechat()
35 | bot.start()
36 | // 或使用核心API
37 | // const WechatCore = require('wechat4u/lib/core')
38 | ```
39 |
40 | ## 开发测试
41 |
42 | ```shell
43 | git clone https://github.com/nodeWechat/wechat4u.git
44 | cd wechat4u
45 | npm install
46 | npm run core // 命令行模式
47 | npm run compile // babel编译
48 | ```
49 |
50 | ## 使用范例
51 |
52 | `node run-core.js`
53 |
54 | 逻辑见[代码](https://github.com/nodeWechat/wechat4u/blob/master/run-core.js),简明完整,一定要看
55 |
56 | ## 实例化Wechat类
57 |
58 | ```javascript
59 | let bot = new Wechat([botData])
60 | ```
61 |
62 | 若传入`botData`,则使用此机器人信息,重新开始之前的同步
63 |
64 | ## 实例属性
65 |
66 | 所有属性均只读
67 |
68 | ### bot.botData
69 |
70 | 可导出的实例基本信息,在下次new新bot时,可以填入此信息,重新同步
71 |
72 | ### bot.PROP
73 |
74 | 保持登录状态的必要信息
75 |
76 | ### bot.CONF
77 |
78 | 配置信息,包括当前服务器地址,API路径和一些常量
79 |
80 | 程序中需要使用CONF中的常量来判断当前状态的新消息类型
81 |
82 | ```javascript
83 | bot.state == bot.CONF.STATE.init // 初始化状态
84 | bot.state == bot.CONF.STATE.uuid // 已获取 UUID
85 | bot.state == bot.CONF.STATE.login // 已登录
86 | bot.state == bot.CONF.STATE.logout // 已退出登录
87 | msg.MsgType == bot.CONF.MSGTYPE_TEXT // 文本消息
88 | msg.MsgType == bot.CONF.MSGTYPE_IMAGE // 图片消息
89 | msg.MsgType == bot.CONF.MSGTYPE_VOICE // 语音消息
90 | msg.MsgType == bot.CONF.MSGTYPE_EMOTICON // 自定义表情消息
91 | msg.MsgType == bot.CONF.MSGTYPE_MICROVIDEO // 小视频消息
92 | msg.MsgType == bot.CONF.MSGTYPE_VIDEO // 视频消息
93 | ```
94 |
95 | ### bot.state
96 |
97 | 当前状态
98 |
99 | ### bot.user
100 |
101 | 当前登录用户信息
102 |
103 | ### bot.contacts
104 |
105 | 所有联系人,包括通讯录联系人,近期联系群,公众号
106 |
107 | key为联系人UserName,UserName是本次登录时每个联系人的UUID,不过下次登录会改变
108 |
109 | value为`Contact`对象,具体属性方法见`src/interface/contact.js`
110 |
111 | ### msg
112 |
113 | 登录后接受到的所有消息
114 |
115 | msg为`Message`对象,具体属性方法见`src/interface/message.js`
116 |
117 | ## 实例API
118 |
119 | ### bot.start()
120 |
121 | 启动实例,登录和保持同步
122 |
123 | 调用该方法后,通过监听事件来处理消息
124 |
125 | ### bot.restart()
126 |
127 | 利用实例已获取的必要信息,重新登录和进行同步
128 |
129 | ### bot.stop()
130 |
131 | 停止实例,退出登录
132 |
133 | 调用该方法后,通过监听`logout`事件来登出
134 |
135 | ### bot.setPollingMessageGetter(getter)
136 |
137 | 自定义心跳消息内容
138 |
139 | `getter` 函数返回心跳消息内容
140 |
141 | `typeof(getter())` 应为 `"string"`
142 |
143 | ```javascript
144 | bot.setRollingMessageGetter(function () {
145 | //
146 | return (new Date()).toJSON();
147 | });
148 | ```
149 |
150 | ### bot.setPollingIntervalGetter(getter)
151 |
152 | 自定义心跳间隔
153 |
154 | `getter` 函数返回心跳间隔(以毫秒为单位)
155 |
156 | `typeof(getter())` 应为 `"number"`
157 |
158 | ```javascript
159 | bot.setRollingIntervalGetter(function () {
160 | return 2 * 60 * 1000;
161 | });
162 | ```
163 |
164 | ### bot.setPollingTargetGetter(getter)
165 |
166 | 自定义心跳目标用户
167 |
168 | `getter` 函数返回目标用户的 `UserName` (形如 `@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` )
169 |
170 | `typeof(getter())` 应为 `"string"`
171 |
172 | 注: 如要使用 `bot.user.UserName` ,需在 `login` 事件后定义目标用户
173 |
174 | ```javascript
175 | bot.setRollingmeGetter(function () {
176 | return bot.user.UserName;
177 | });
178 | ```
179 |
180 | > 以下方法均返回Promise
181 |
182 | ### bot.sendText(msgString, toUserName)
183 |
184 | 发送文本消息,可以包含emoji(😒)和QQ表情([坏笑])
185 |
186 | ### bot.uploadMedia(Buffer | Stream | File, filename, toUserName)
187 |
188 | 上传媒体文件
189 |
190 | 返回
191 |
192 | ```javascript
193 | {
194 | name: name,
195 | size: size,
196 | ext: ext,
197 | mediatype: mediatype,
198 | mediaId: mediaId
199 | }
200 | ```
201 |
202 | ### bot.sendPic(mediaId, toUserName)
203 |
204 | 发送图片,mediaId为uploadMedia返回的mediaId
205 |
206 | ```javascript
207 | bot.uploadMedia(fs.createReadStream('test.png'))
208 | .then(res => {
209 | return bot.sendPic(res.mediaId, ToUserName)
210 | })
211 | .catch(err => {
212 | console.log(err)
213 | })
214 | ```
215 |
216 | ### bot.sendEmoticon(md5 | mediaId, toUserName)
217 |
218 | 发送表情,可是是表情的MD5或者uploadMedia返回的mediaId
219 |
220 | 表情的MD5,可以自己计算但是可能不存在在微信服务器中,也可以从微信返回的表情消息中获得
221 |
222 | ### bot.sendVideo(mediaId, toUserName)
223 |
224 | 发送视频
225 |
226 | ### bot.sendDoc(mediaId, name, size, ext, toUserName)
227 |
228 | 以应用卡片的形式发送文件,可以通过这个API发送语音
229 |
230 | ### bot.sendMsg(msg, toUserName)
231 |
232 | 对以上发送消息的方法的封装,是发送消息的通用方法
233 |
234 | 当`msg`为string时,发送文本消息
235 |
236 | 当`msg`为`{file:xxx,filename:'xxx.ext'}`时,发送对应媒体文件
237 |
238 | 当`msg`为`{emoticonMd5:xxx}`时,发送表情
239 |
240 | ```javascript
241 | bot.sendMsg({
242 | file: request('https://raw.githubusercontent.com/nodeWechat/wechat4u/master/bot-qrcode.jpg'),
243 | filename: 'bot-qrcode.jpg'
244 | }, ToUserName)
245 | .catch(err => {
246 | console.log(err)
247 | })
248 | ```
249 |
250 | ### bot.forwardMsg(msg, toUserName)
251 |
252 | 转发消息,`msg`为`message`事件传递的`msg`对象
253 |
254 | ### bot.revokeMsg(MsgID, toUserName)
255 |
256 | 撤回消息
257 |
258 | `MsgID`为发送消息后返回的代表消息的ID
259 |
260 | ```javascript
261 | bot.sendMsg('测试撤回', toUserName)
262 | .then(res => {
263 | return bot.revokeMsg(res.MsgID, toUserName)
264 | })
265 | .catch(err => {
266 | console.log(err)
267 | })
268 | ```
269 |
270 | ### bot.getContact(Seq)
271 |
272 | 获取通讯录中的联系人
273 |
274 | `Seq` 上一次调用 bot.getContact 后返回的 seq,第一次调用可不传
275 |
276 | ### bot.batchGetContact(contacts)
277 |
278 | 批量获取指定联系人数据
279 |
280 | `contacts` 数组,指定需要获取的数据
281 |
282 | 当`contacts`为`[{UserName: xxx}]`时,可获取指定联系人或群信息
283 |
284 | 当`contacts`为`[{UserName: xxx, EncryChatRoomId: xxx}]`时,可获取指定群内成员详细信息,EncryChatRoomId 可从群信息中获得
285 |
286 | ### bot.getHeadImg(HeadImgUrl)
287 |
288 | 获取联系人头像
289 |
290 | ```javascript
291 | bot.getHeadImg(bot.contacts[UserName].HeadImgUrl).then(res => {
292 | fs.writeFileSync(`${UserName}.jpg`, res.data)
293 | }).catch(err => {
294 | console.log(err)
295 | })
296 | ```
297 |
298 | ### bot.getMsgImg(MsgId)
299 |
300 | 获取图片或表情
301 |
302 | ```javascript
303 | bot.getMsgImg(msg.MsgId).then(res => {
304 | fs.writeFileSync(`${msg.MsgId}.jpg`, res.data)
305 | }).catch(err => {
306 | console.log(err)
307 | })
308 | ```
309 |
310 | ### bot.getVoice(MsgId)
311 |
312 | 获取语音
313 |
314 | ### bot.getVideo(MsgId)
315 |
316 | 获取小视频或视频
317 |
318 | ### bot.getDoc(UserName, MediaId, FileName)
319 |
320 | 获取文件,消息的`MsgType`为49且`AppMsgType`为6时即为文件。
321 |
322 |
323 | ### bot.addFriend(UserName, Content)
324 |
325 | 添加好友
326 |
327 | `UserName` 一般可从群信息中获得
328 |
329 | `Content` 验证信息
330 |
331 | ### bot.verifyUser(UserName, Ticket)
332 |
333 | 通过好友添加请求
334 |
335 | ### bot.createChatroom(Topic, MemberList)
336 |
337 | 创建群
338 |
339 | `Topic` 群聊名称
340 |
341 | `MemberList` 数组, 除自己外至少两人的UserName,格式为 [ {"UserName":"@250d8d156ad9f8b068c2e3df3464ecf2"}, {"UserName":"@42d725733741de6ac53cbe3738d8dd2e"} ]
342 |
343 | ### bot.updateChatroom(ChatRoomUserName, MemberList, fun)
344 |
345 | 更新群成员
346 |
347 | `ChatRoomUserName` '@@'开头的群UserName
348 |
349 | `MemberList` 数组,联系人UserNa
350 |
351 | `fun` 可选'addmember','delmember','invitemember'
352 |
353 | ### bot.updateChatRoomName(ChatRoomUserName, NewName)
354 |
355 | 更新群名称
356 |
357 | `ChatRoomUserName` '@@'开头的群UserName
358 |
359 | `NewName` 字符串,新的群名称
360 |
361 | ### bot.opLog(UserName, OP)
362 |
363 | 置顶或取消置顶联系人,可通过直接取消置顶群来获取群ChatRoomOwner
364 |
365 | OP == 0 取消置顶
366 |
367 | OP == 1 置顶
368 |
369 | ### bot.updateRemarkName(UserName, RemarkName)
370 |
371 | 设置联系人备注或标签
372 |
373 | ## 实例事件
374 |
375 | ### uuid
376 |
377 | 得到uuid,之后可以构造二维码或从微信服务器取得二维码
378 |
379 | ```javascript
380 | bot.on('uuid', uuid => {
381 | qrcode.generate('https://login.weixin.qq.com/l/' + uuid, {
382 | small: true
383 | })
384 | console.log('二维码链接:', 'https://login.weixin.qq.com/qrcode/' + uuid)
385 | })
386 | ```
387 |
388 | ### user-avatar
389 |
390 | 手机扫描后可以得到登录用户头像的Data URL
391 |
392 | ### login
393 |
394 | 手机确认登录
395 |
396 | ### logout
397 |
398 | 成功登出
399 |
400 | ### contacts-updated
401 |
402 | 联系人更新,可得到已更新的联系人列表
403 |
404 | ### message
405 |
406 | 所有通过同步得到的消息,通过`msg.MsgType`判断消息类型
407 |
408 | ```javascript
409 | bot.on('message', msg => {
410 | switch (msg.MsgType) {
411 | case bot.CONF.MSGTYPE_STATUSNOTIFY:
412 | break
413 | case bot.CONF.MSGTYPE_TEXT:
414 | break
415 | case bot.CONF.MSGTYPE_RECALLED:
416 | break
417 | }
418 | })
419 | ```
420 |
421 | ### error
422 |
423 | ## Contact对象和Message对象
424 |
425 | 每个contact,继承自 interface/contact,除原本 json 外,扩展以下属性:
426 |
427 | ```javascript
428 | contact.AvatarUrl // 处理过的头像地址
429 | contact.isSelf // 是否是登录用户本人
430 |
431 | contact.getDisplayName()
432 | contact.canSearch(keyword)
433 | ```
434 |
435 | 此外,wechat4u 在实例上提供 Contact 作为联系人的通用接口,扩展以下属性:
436 |
437 | ```javascript
438 | wechat.Contact.isRoomContact()
439 | wechat.Contact.isSpContact()
440 | wechat.Contact.isPublicContact()
441 |
442 | wechat.Contact.getUserByUserName()
443 | wechat.Contact.getSearchUser(keyword)
444 | ```
445 |
446 | 每个msg 对象继承自 interface/message,出原本 json 外,具有以下属性:
447 |
448 | ```javascript
449 | message.isSendBySelf // 是否是本人发送
450 |
451 | message.isSendBy(contact)
452 | message.getPeerUserName() // 获取所属对话的联系人 UserName
453 | message.getDisplayTime() // 获取形如 12:00 的时间戳信息
454 | ```
455 |
456 | ## 相关项目
457 |
458 | 关于微信网页端机器人的实现,已经有大量的轮子了。感谢各位大神!(排名不分先后。。收录的肯定也不齐。。)
459 |
460 | - [Python2 的 WeixinBot](https://github.com/Urinx/WeixinBot)
461 | - [QT 的 QWX](https://github.com/xiangzhai/qwx)
462 | - [Node,可能会写成uProxy插件的 uProxy_wechat](https://github.com/LeMasque/uProxy_wechat)
463 | - [Node,可在shell中直接运行的 wechat-user-bot](https://github.com/HalfdogStudio/wechat-user-bot)
464 | - [Python3 的 wechat_robot](https://github.com/lyyyuna/wechat_robot)
465 | - [开放协议 支持 QQ&微信 的 wxagent](https://github.com/kitech/wxagent)
466 | - [在微信网页版和 IRC 间搭建通道支持 IRC 操作的 wechatircd](https://github.com/MaskRay/wechatircd)
467 | - [Chrome 插件版的微信机器人](https://github.com/spacelan/weixin-bot-chrome-extension)
468 |
469 | 关于微信网页端的接口说明,也有好几篇分析的很厉害的文章。
470 |
471 | - [Reverland 大神的web 微信与基于node的微信机器人实现](http://reverland.org/javascript/2016/01/15/webchat-user-bot/)
472 | - [Urinx 大神的 API Map](https://github.com/Urinx/WeixinBot/blob/master/README.md)
473 | - [聂永 大神的 微信协议简单调研笔记](http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html)
474 |
475 | ## License
476 |
477 | MIT
478 |
--------------------------------------------------------------------------------
/bot-qrcode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nodeWechat/wechat4u/c775020944a1bf5e4a2d94f9f6517a2fd9e0005b/bot-qrcode.jpg
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * wechat4u.js
3 | * Copyright(c) 2016-2017 nodeWechat
4 | * MIT Licensed
5 | */
6 |
7 | 'use strict'
8 |
9 | module.exports = require('./src/wechat')
10 |
--------------------------------------------------------------------------------
/media/test.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nodeWechat/wechat4u/c775020944a1bf5e4a2d94f9f6517a2fd9e0005b/media/test.gif
--------------------------------------------------------------------------------
/media/test.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nodeWechat/wechat4u/c775020944a1bf5e4a2d94f9f6517a2fd9e0005b/media/test.mp4
--------------------------------------------------------------------------------
/media/test.txt:
--------------------------------------------------------------------------------
1 | # wechat4u.js
2 |
3 | 
4 |
5 | wechat4u@0.4.0更新了大量API,增强了稳定性
6 |
7 | 测试服务器[wechat4u.duapp.com](http://wechat4u.duapp.com)
8 | 具有文本表情自动回复,监控,群发功能
9 |
10 | ## 安装使用
11 |
12 | ```
13 | npm install --save wechat4u@latest
14 | ```
15 |
16 | ```javascript
17 | const Wechat = require('wechat')
18 | let bot = new Wechat()
19 | bot.start()
20 | // 或使用核心API
21 | // const WechatCore = require('wechat/lib/core')
22 | ```
23 |
24 | ## 开发测试
25 |
26 | ```
27 | git clone https://github.com/nodeWechat/wechat4u.git
28 | cd wechat4u
29 | npm install
30 | npm run example
31 | npm run core
32 | npm run compile
33 | ```
34 |
35 | ## 使用范例
36 |
37 | `node run-core.js`
38 |
39 | 逻辑见代码,简明完整
40 |
41 | ## 实例属性
42 |
43 | 所有属性均只读
44 |
45 | ##### bot.PROP
46 |
47 | 保持登录状态的必要信息
48 |
49 | ##### bot.CONF
50 |
51 | 配置信息,包括当前服务器地址,API路径和一些常量
52 |
53 | 程序中需要使用CONF中的常量来判断当前状态的新消息类型
54 |
55 | ```javascript
56 | bot.state === bot.CONF.STATE.init // 初始化状态
57 | bot.state === bot.CONF.STATE.uuid // 已获取 UUID
58 | bot.state === bot.CONF.STATE.login // 已登录
59 | bot.state === bot.CONF.STATE.logout // 已退出登录
60 | msg.MsgType == bot.CONF.MSGTYPE_TEXT // 文本消息
61 | msg.MsgType == bot.CONF.MSGTYPE_IMAGE // 图片消息
62 | msg.MsgType == bot.CONF.MSGTYPE_VOICE // 语音消息
63 | msg.MsgType == bot.CONF.MSGTYPE_EMOTICON // 自定义表情消息
64 | msg.MsgType == bot.CONF.MSGTYPE_MICROVIDEO // 小视频消息
65 | msg.MsgType == bot.CONF.MSGTYPE_VIDEO // 视频消息
66 | ```
67 |
68 | ##### bot.state
69 |
70 | 当前状态
71 |
72 | ##### bot.user
73 |
74 | 当前登录用户信息
75 |
76 | ##### bot.contacts
77 |
78 | 所有联系人,包括通讯录联系人,近期联系群,公众号
79 |
80 | key为联系人UserName,UserName是本次登录时每个联系人的UUID,不过下次登录会改变
81 |
82 | value为`Contact`对象,具体属性方法见`src/interface/contact.js`
83 |
84 | ##### msg
85 |
86 | 登录后接受到的所有消息
87 |
88 | msg为`Message`对象,具体属性方法见`src/interface/message.js`
89 |
90 | ## 实例API
91 |
92 | ##### bot.start()
93 |
94 | 启动实例,登录和保持同步
95 |
96 | ##### bot.stop()
97 |
98 | 停止实例,退出登录
99 |
100 | #### 以下方法均返回Promise
101 |
102 | ##### bot.sendText(msgString, toUserName)
103 |
104 | 发送文本消息,可以包含emoji(😒)和QQ表情([坏笑])
105 |
106 | ##### bot.uploadMedia(Stream | File)
107 |
108 | 上传媒体文件,返回:
109 |
110 | ```javascript
111 | {
112 | name: name,
113 | size: size,
114 | ext: ext,
115 | mediatype: mediatype,
116 | mediaId: mediaId
117 | }
118 | ```
119 |
120 | ##### bot.sendPic(mediaId, toUserName)
121 |
122 | 发送图片,mediaId为uploadMedia返回的mediaId
123 |
124 | ```javascript
125 | bot.uploadMedia(fs.createReadStream('test.png'))
126 | .then(res => {
127 | return bot.sendPic(res.mediaId, ToUserName)
128 | })
129 | .catch(err => {
130 | console.log(err)
131 | })
132 | ```
133 |
134 | ##### bot.sendEmoticon(md5 | mediaId, toUserName)
135 |
136 | 发送表情,可是是表情的MD5或者uploadMedia返回的mediaId
137 |
138 | 表情的MD5,可以自己计算但是可能不存在在微信服务器中,也可以从微信返回的表情消息中获得
139 |
140 | ##### bot.sendVideo(mediaId, toUserName)
141 |
142 | 发送视频
143 |
144 | ##### bot.sendDoc(mediaId, name, size, ext, toUserName)
145 |
146 | 以应用卡片的形式发送文件,可以通过这个API发送语音
147 |
148 | ##### bot.getHeadImg(HeadImgUrl)
149 |
150 | 获取联系人头像
151 |
152 | ```javascript
153 | bot.getHeadImg(bot.contacts[UserName].HeadImgUrl).then(res => {
154 | fs.writeFileSync(`${UserName}.jpg`, res.data)
155 | }).catch(err => {
156 | console.log(err)
157 | })
158 | ```
159 |
160 | ##### bot.getMsgImg(MsgId)
161 |
162 | 获取图片或表情
163 |
164 | ```javascript
165 | bot.getMsgImg(msg.MsgId).then(res => {
166 | fs.writeFileSync(`${msg.MsgId}.jpg`, res.data)
167 | }).catch(err => {
168 | console.log(err)
169 | })
170 | ```
171 |
172 | ##### bot.getVoice(MsgId)
173 |
174 | 获取语音
175 |
176 | ##### bot.getVideo(MsgId)
177 |
178 | 获取小视频或视频
179 |
180 | ## 实例事件
181 |
182 | ##### uuid
183 |
184 | 得到uuid,之后可以构造二维码或从微信服务器取得二维码
185 |
186 | ```javascript
187 | bot.on('uuid', uuid => {
188 | qrcode.generate('https://login.weixin.qq.com/l/' + uuid, {
189 | small: true
190 | })
191 | console.log('二维码链接:', 'https://login.weixin.qq.com/qrcode/' + uuid)
192 | })
193 | ```
194 |
195 | ##### user-avatar
196 |
197 | 手机扫描后可以得到登录用户头像的Data URL
198 |
199 | ##### login
200 |
201 | 手机确认登录
202 |
203 | ##### logout
204 |
205 | 成功登出
206 |
207 | ##### contacts-updated
208 |
209 | 联系人更新,可得到已更新的联系人列表
210 |
211 | ##### message
212 |
213 | 所有通过同步得到的消息,通过`msg.MsgType`判断消息类型
214 |
215 | ```javascript
216 | bot.on('message', msg => {
217 | switch (msg.MsgType) {
218 | case bot.CONF.MSGTYPE_STATUSNOTIFY:
219 | break
220 | case bot.CONF.MSGTYPE_TEXT:
221 | break
222 | case bot.CONF.MSGTYPE_RECALLED:
223 | break
224 | }
225 | })
226 | ```
227 |
228 | ##### error
229 |
230 | ## Contact对象和Message对象
231 |
232 | 每个contact,继承自 interface/contact,除原本 json 外,扩展以下属性:
233 |
234 | ```javascript
235 | contact.AvatarUrl // 处理过的头像地址
236 | contact.isSelf // 是否是登录用户本人
237 |
238 | contact.getDisplayName()
239 | contact.canSearch(keyword)
240 | ```
241 |
242 | 此外,wechat4u 在实例上提供 Contact 作为联系人的通用接口,扩展以下属性:
243 |
244 | ```javascript
245 | wechat.Contact.isRoomContact()
246 | wechat.Contact.isSpContact()
247 | wechat.Contact.isPublicContact()
248 |
249 | wechat.Contact.getUserByUserName()
250 | wechat.Contact.getSearchUser(keyword)
251 | ```
252 |
253 | 每个msg 对象继承自 interface/message,出原本 json 外,具有以下属性:
254 |
255 | ```javascript
256 | message.isSendBySelf // 是否是本人发送
257 |
258 | message.isSendBy(contact)
259 | message.getPeerUserName() // 获取所属对话的联系人 UserName
260 | message.getDisplayTime() // 获取形如 12:00 的时间戳信息
261 | ```
262 |
263 |
264 | ## 相关项目
265 |
266 | 关于微信网页端机器人的实现,已经有大量的轮子了。感谢各位大神!(排名不分先后。。收录的肯定也不齐。。)
267 |
268 | * [Python2 的 WeixinBot](https://github.com/Urinx/WeixinBot)
269 | * [QT 的 QWX](https://github.com/xiangzhai/qwx)
270 | * [Node,可能会写成uProxy插件的 uProxy_wechat](https://github.com/LeMasque/uProxy_wechat)
271 | * [Node,可在shell中直接运行的 wechat-user-bot](https://github.com/HalfdogStudio/wechat-user-bot)
272 | * [Python3 的 wechat_robot](https://github.com/lyyyuna/wechat_robot)
273 | * [开放协议 支持 QQ&微信 的 wxagent](https://github.com/kitech/wxagent)
274 | * [在微信网页版和 IRC 间搭建通道支持 IRC 操作的 wechatircd](https://github.com/MaskRay/wechatircd)
275 | * [Chrome 插件版的微信机器人](https://github.com/spacelan/weixin-bot-chrome-extension)
276 |
277 | 关于微信网页端的接口说明,也有好几篇分析的很厉害的文章。
278 |
279 | * [Reverland 大神的web 微信与基于node的微信机器人实现](http://reverland.org/javascript/2016/01/15/webchat-user-bot/)
280 | * [Urinx 大神的 API Map](https://github.com/Urinx/WeixinBot/blob/master/README.md)
281 | * [聂永 大神的 微信协议简单调研笔记](http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html)
282 |
283 | 好了,差不多就这些资料了。如果想要开发个自己的,那就开工吧!
284 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wechat4u",
3 | "version": "0.7.14",
4 | "description": "web wechat lib for user",
5 | "main": "lib/wechat.js",
6 | "scripts": {
7 | "compile": "babel src -d lib -s",
8 | "prepublish": "npm run compile",
9 | "lint": "eslint src",
10 | "clean": "rm -rf lib",
11 | "unit": "mocha test/unit.js --compilers js:babel-core/register",
12 | "test": "npm run lint && npm run unit",
13 | "core": "cross-env DEBUG=wechat,core node run-core.js"
14 | },
15 | "dependencies": {
16 | "axios": "^1.1.3",
17 | "bl": "^1.1.2",
18 | "debug": "^2.2.0",
19 | "form-data": "^2.1.2",
20 | "lodash": "^4.17.11",
21 | "mime": "^1.3.4"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/nodeWechat/wechat4u.git"
26 | },
27 | "author": "nodeWechat",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/nodeWechat/wechat4u/issues"
31 | },
32 | "homepage": "https://github.com/nodeWechat/wechat4u#readme",
33 | "devDependencies": {
34 | "babel-cli": "^6.14.0",
35 | "babel-preset-env": "^1.7.0",
36 | "babel-register": "^6.16.3",
37 | "chai": "^3.5.0",
38 | "cross-env": "^1.0.7",
39 | "eslint": "^5.14.1",
40 | "eslint-config-standard": "^5.3.1",
41 | "eslint-plugin-promise": "^1.1.0",
42 | "eslint-plugin-standard": "^1.3.2",
43 | "mocha": "^6.0.2",
44 | "nock": "^10.0.6",
45 | "qrcode-terminal": "^0.11.0",
46 | "replay": "^2.0.6",
47 | "request": "^2.88.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/run-core.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('babel-register')
3 | const Wechat = require('./src/wechat.js')
4 | const qrcode = require('qrcode-terminal')
5 | const fs = require('fs')
6 | const request = require('request')
7 |
8 | let bot
9 | /**
10 | * 尝试获取本地登录数据,免扫码
11 | * 这里演示从本地文件中获取数据
12 | */
13 | try {
14 | bot = new Wechat(require('./sync-data.json'))
15 | } catch (e) {
16 | bot = new Wechat()
17 | }
18 | /**
19 | * 启动机器人
20 | */
21 | if (bot.PROP.uin) {
22 | // 存在登录数据时,可以随时调用restart进行重启
23 | bot.restart()
24 | } else {
25 | bot.start()
26 | }
27 | /**
28 | * uuid事件,参数为uuid,根据uuid生成二维码
29 | */
30 | bot.on('uuid', uuid => {
31 | qrcode.generate('https://login.weixin.qq.com/l/' + uuid, {
32 | small: true
33 | })
34 | console.log('二维码链接:', 'https://login.weixin.qq.com/qrcode/' + uuid)
35 | })
36 | /**
37 | * 登录用户头像事件,手机扫描后可以得到登录用户头像的Data URL
38 | */
39 | bot.on('user-avatar', avatar => {
40 | console.log('登录用户头像Data URL:', avatar)
41 | })
42 | /**
43 | * 登录成功事件
44 | */
45 | bot.on('login', () => {
46 | console.log('登录成功')
47 | // 保存数据,将数据序列化之后保存到任意位置
48 | fs.writeFileSync('./sync-data.json', JSON.stringify(bot.botData))
49 | })
50 | /**
51 | * 登出成功事件
52 | */
53 | bot.on('logout', () => {
54 | console.log('登出成功')
55 | // 清除数据
56 | fs.unlinkSync('./sync-data.json')
57 | })
58 | /**
59 | * 联系人更新事件,参数为被更新的联系人列表
60 | */
61 | bot.on('contacts-updated', contacts => {
62 | console.log(contacts)
63 | console.log('联系人数量:', Object.keys(bot.contacts).length)
64 | })
65 | /**
66 | * 错误事件,参数一般为Error对象
67 | */
68 | bot.on('error', err => {
69 | console.error('错误:', err)
70 | })
71 | /**
72 | * 如何发送消息
73 | */
74 | bot.on('login', () => {
75 | /**
76 | * 演示发送消息到文件传输助手
77 | * 通常回复消息时可以用 msg.FromUserName
78 | */
79 | let ToUserName = 'filehelper'
80 |
81 | /**
82 | * 发送文本消息,可以包含emoji(😒)和QQ表情([坏笑])
83 | */
84 | bot.sendMsg('发送文本消息,可以包含emoji(😒)和QQ表情([坏笑])', ToUserName)
85 | .catch(err => {
86 | bot.emit('error', err)
87 | })
88 |
89 | /**
90 | * 通过表情MD5发送表情
91 | */
92 | bot.sendMsg({
93 | emoticonMd5: '00c801cdf69127550d93ca52c3f853ff'
94 | }, ToUserName)
95 | .catch(err => {
96 | bot.emit('error', err)
97 | })
98 |
99 | /**
100 | * 以下通过上传文件发送图片,视频,附件等
101 | * 通用方法为入下
102 | * file为多种类型
103 | * filename必填,主要为了判断文件类型
104 | */
105 | // bot.sendMsg({
106 | // file: Stream || Buffer || ArrayBuffer || File || Blob,
107 | // filename: 'bot-qrcode.jpg'
108 | // }, ToUserName)
109 | // .catch(err => {
110 | // bot.emit('error',err)
111 | // })
112 |
113 | /**
114 | * 发送图片
115 | */
116 | bot.sendMsg({
117 | file: request('https://raw.githubusercontent.com/nodeWechat/wechat4u/master/bot-qrcode.jpg'),
118 | filename: 'bot-qrcode.jpg'
119 | }, ToUserName)
120 | .catch(err => {
121 | bot.emit('error', err)
122 | })
123 |
124 | /**
125 | * 发送表情
126 | */
127 | bot.sendMsg({
128 | file: fs.createReadStream('./media/test.gif'),
129 | filename: 'test.gif'
130 | }, ToUserName)
131 | .catch(err => {
132 | bot.emit('error', err)
133 | })
134 |
135 | /**
136 | * 发送视频
137 | */
138 | bot.sendMsg({
139 | file: fs.createReadStream('./media/test.mp4'),
140 | filename: 'test.mp4'
141 | }, ToUserName)
142 | .catch(err => {
143 | bot.emit('error', err)
144 | })
145 |
146 | /**
147 | * 发送文件
148 | */
149 | bot.sendMsg({
150 | file: fs.createReadStream('./media/test.txt'),
151 | filename: 'test.txt'
152 | }, ToUserName)
153 | .catch(err => {
154 | bot.emit('error', err)
155 | })
156 |
157 | /**
158 | * 发送撤回消息请求
159 | */
160 | bot.sendMsg('测试撤回', ToUserName)
161 | .then(res => {
162 | // 需要取得待撤回消息的MsgID
163 | return bot.revokeMsg(res.MsgID, ToUserName)
164 | })
165 | .catch(err => {
166 | console.log(err)
167 | })
168 | })
169 | /**
170 | * 如何处理会话消息
171 | */
172 | bot.on('message', msg => {
173 | /**
174 | * 获取消息时间
175 | */
176 | console.log(`----------${msg.getDisplayTime()}----------`)
177 | /**
178 | * 获取消息发送者的显示名
179 | */
180 | console.log(bot.contacts[msg.FromUserName].getDisplayName())
181 | /**
182 | * 判断消息类型
183 | */
184 | switch (msg.MsgType) {
185 | case bot.CONF.MSGTYPE_TEXT:
186 | /**
187 | * 文本消息
188 | */
189 | console.log(msg.Content)
190 | break
191 | case bot.CONF.MSGTYPE_IMAGE:
192 | /**
193 | * 图片消息
194 | */
195 | console.log('图片消息,保存到本地')
196 | bot.getMsgImg(msg.MsgId).then(res => {
197 | fs.writeFileSync(`./media/${msg.MsgId}.jpg`, res.data)
198 | }).catch(err => {
199 | bot.emit('error', err)
200 | })
201 | break
202 | case bot.CONF.MSGTYPE_VOICE:
203 | /**
204 | * 语音消息
205 | */
206 | console.log('语音消息,保存到本地')
207 | bot.getVoice(msg.MsgId).then(res => {
208 | fs.writeFileSync(`./media/${msg.MsgId}.mp3`, res.data)
209 | }).catch(err => {
210 | bot.emit('error', err)
211 | })
212 | break
213 | case bot.CONF.MSGTYPE_EMOTICON:
214 | /**
215 | * 表情消息
216 | */
217 | console.log('表情消息,保存到本地')
218 | bot.getMsgImg(msg.MsgId).then(res => {
219 | fs.writeFileSync(`./media/${msg.MsgId}.gif`, res.data)
220 | }).catch(err => {
221 | bot.emit('error', err)
222 | })
223 | break
224 | case bot.CONF.MSGTYPE_VIDEO:
225 | case bot.CONF.MSGTYPE_MICROVIDEO:
226 | /**
227 | * 视频消息
228 | */
229 | console.log('视频消息,保存到本地')
230 | bot.getVideo(msg.MsgId).then(res => {
231 | fs.writeFileSync(`./media/${msg.MsgId}.mp4`, res.data)
232 | }).catch(err => {
233 | bot.emit('error', err)
234 | })
235 | break
236 | case bot.CONF.MSGTYPE_APP:
237 | if (msg.AppMsgType == 6) {
238 | /**
239 | * 文件消息
240 | */
241 | console.log('文件消息,保存到本地')
242 | bot.getDoc(msg.FromUserName, msg.MediaId, msg.FileName).then(res => {
243 | fs.writeFileSync(`./media/${msg.FileName}`, res.data)
244 | console.log(res.type);
245 | }).catch(err => {
246 | bot.emit('error', err)
247 | })
248 | }
249 | break
250 | default:
251 | break
252 | }
253 | })
254 | /**
255 | * 如何处理红包消息
256 | */
257 | bot.on('message', msg => {
258 | if (msg.MsgType == bot.CONF.MSGTYPE_SYS && /红包/.test(msg.Content)) {
259 | // 若系统消息中带有‘红包’,则认为是红包消息
260 | // wechat4u并不能自动收红包
261 | }
262 | })
263 | /**
264 | * 如何处理转账消息
265 | */
266 | bot.on('message', msg => {
267 | if (msg.MsgType == bot.CONF.MSGTYPE_APP && msg.AppMsgType == bot.CONF.APPMSGTYPE_TRANSFERS) {
268 | // 转账
269 | }
270 | })
271 | /**
272 | * 如何处理撤回消息
273 | */
274 | bot.on('message', msg => {
275 | if (msg.MsgType == bot.CONF.MSGTYPE_RECALLED) {
276 | // msg.Content是一个xml,关键信息是MsgId
277 | let MsgId = msg.Content.match(/(.*?)<\/msgid>.*?<\/replacemsg>/)[0]
278 | // 得到MsgId后,根据MsgId,从收到过的消息中查找被撤回的消息
279 | }
280 | })
281 | /**
282 | * 如何处理好友请求消息
283 | */
284 | bot.on('message', msg => {
285 | if (msg.MsgType == bot.CONF.MSGTYPE_VERIFYMSG) {
286 | bot.verifyUser(msg.RecommendInfo.UserName, msg.RecommendInfo.Ticket)
287 | .then(res => {
288 | console.log(`通过了 ${bot.Contact.getDisplayName(msg.RecommendInfo)} 好友请求`)
289 | })
290 | .catch(err => {
291 | bot.emit('error', err)
292 | })
293 | }
294 | })
295 | /**
296 | * 如何直接转发消息
297 | */
298 | bot.on('message', msg => {
299 | // 不是所有消息都可以直接转发
300 | bot.forwardMsg(msg, 'filehelper')
301 | .catch(err => {
302 | bot.emit('error', err)
303 | })
304 | })
305 | /**
306 | * 如何获取联系人头像
307 | */
308 | bot.on('message', msg => {
309 | bot.getHeadImg(bot.contacts[msg.FromUserName].HeadImgUrl).then(res => {
310 | fs.writeFileSync(`./media/${msg.FromUserName}.jpg`, res.data)
311 | }).catch(err => {
312 | bot.emit('error', err)
313 | })
314 | })
315 |
--------------------------------------------------------------------------------
/src/core.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import bl from 'bl'
3 | import _debug from 'debug'
4 | import FormData from 'form-data'
5 | import mime from 'mime'
6 | import {
7 | getCONF,
8 | Request,
9 | isStandardBrowserEnv,
10 | assert,
11 | getClientMsgId,
12 | getDeviceID
13 | } from './util'
14 |
15 | const debug = _debug('core')
16 | export class AlreadyLogoutError extends Error {
17 | constructor (message = 'already logout') {
18 | super(message)
19 | // fuck the babel
20 | this.constructor = AlreadyLogoutError
21 | this.__proto__ = AlreadyLogoutError.prototype
22 | }
23 | }
24 | const CHUNK_SIZE = 0.5 * 1024 * 1024 // 0.5 MB
25 | export default class WechatCore {
26 | constructor (data) {
27 | this.PROP = {
28 | uuid: '',
29 | uin: '',
30 | sid: '',
31 | skey: '',
32 | passTicket: '',
33 | formatedSyncKey: '',
34 | webwxDataTicket: '',
35 | syncKey: {
36 | List: []
37 | }
38 | }
39 | this.CONF = getCONF()
40 | this.COOKIE = {}
41 | this.user = {}
42 | if (data) {
43 | this.botData = data
44 | }
45 |
46 | this.request = new Request({
47 | Cookie: this.COOKIE
48 | })
49 | }
50 |
51 | get botData () {
52 | return {
53 | PROP: this.PROP,
54 | CONF: this.CONF,
55 | COOKIE: this.COOKIE,
56 | user: this.user
57 | }
58 | }
59 |
60 | set botData (data) {
61 | Object.keys(data).forEach(key => {
62 | Object.assign(this[key], data[key])
63 | })
64 | }
65 |
66 | getUUID () {
67 | return Promise.resolve().then(() => {
68 | return this.request({
69 | method: 'POST',
70 | url: this.CONF.API_jsLogin
71 | }).then(res => {
72 | let window = {
73 | QRLogin: {}
74 | }
75 | // res.data: "window.QRLogin.code = xxx; ..."
76 | // eslint-disable-next-line
77 | eval(res.data)
78 | assert.equal(window.QRLogin.code, 200, res)
79 |
80 | this.PROP.uuid = window.QRLogin.uuid
81 | return window.QRLogin.uuid
82 | })
83 | }).catch(err => {
84 | debug(err)
85 | err.tips = '获取UUID失败'
86 | throw err
87 | })
88 | }
89 |
90 | checkLogin () {
91 | return Promise.resolve().then(() => {
92 | let params = {
93 | 'tip': 0,
94 | 'uuid': this.PROP.uuid,
95 | 'loginicon': true,
96 | 'r': ~new Date()
97 | }
98 | return this.request({
99 | method: 'GET',
100 | url: this.CONF.API_login,
101 | params: params
102 | }).then(res => {
103 | let window = {}
104 |
105 | // eslint-disable-next-line
106 | eval(res.data)
107 |
108 | assert.notEqual(window.code, 400, res)
109 |
110 | if (window.code === 200) {
111 | this.CONF = getCONF(window.redirect_uri.match(/(?:\w+\.)+\w+/)[0])
112 | this.rediUri = window.redirect_uri
113 | } else if (window.code === 201 && window.userAvatar) {
114 | // this.user.userAvatar = window.userAvatar
115 | }
116 | return window
117 | })
118 | }).catch(err => {
119 | debug(err)
120 | err.tips = '获取手机确认登录信息失败'
121 | throw err
122 | })
123 | }
124 |
125 | login () {
126 | return Promise.resolve().then(() => {
127 | let headers = {}
128 | headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'
129 | headers['client-version'] = '2.0.0'
130 | headers['referer'] = 'https://wx.qq.com/?&lang=zh_CN&target=t'
131 | headers['extspam'] = 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA=='
132 | return this.request({
133 | method: 'GET',
134 | url: this.rediUri,
135 | maxRedirects: 0,
136 | headers: headers
137 | }).then(res => {
138 |
139 | }).catch(error => {
140 | if (error.response.status === 301) {
141 | let data = error.response.data
142 | let pm = data.match(/(.*)<\/ret>/)
143 | if (pm && pm[1] === '0') {
144 | this.PROP.skey = data.match(/(.*)<\/skey>/)[1]
145 | this.PROP.sid = data.match(/(.*)<\/wxsid>/)[1]
146 | this.PROP.uin = data.match(/(.*)<\/wxuin>/)[1]
147 | this.PROP.passTicket = data.match(/(.*)<\/pass_ticket>/)[1]
148 | }
149 | if (error.response.headers['set-cookie']) {
150 | error.response.headers['set-cookie'].forEach(item => {
151 | if (/webwx.*?data.*?ticket/i.test(item)) {
152 | this.PROP.webwxDataTicket = item.match(/=(.*?);/)[1]
153 | } else if (/wxuin/i.test(item)) {
154 | this.PROP.uin = item.match(/=(.*?);/)[1]
155 | } else if (/wxsid/i.test(item)) {
156 | this.PROP.sid = item.match(/=(.*?);/)[1]
157 | } else if (/pass_ticket/i.test(item)) {
158 | this.PROP.passTicket = item.match(/=(.*?);/)[1]
159 | }
160 | })
161 | }
162 | } else {
163 | throw error
164 | }
165 | })
166 | }).catch(err => {
167 | debug(err)
168 | err.tips = '登录失败'
169 | throw err
170 | })
171 | }
172 |
173 | init () {
174 | return Promise.resolve().then(() => {
175 | let t = Date.now()
176 | let r = t / -1579
177 | let params = {
178 | 'pass_ticket': this.PROP.passTicket,
179 | 'r': Math.ceil(r)
180 | }
181 | let data = {
182 | BaseRequest: this.getBaseRequest()
183 | }
184 | return this.request({
185 | method: 'POST',
186 | url: this.CONF.API_webwxinit,
187 | params: params,
188 | data: data
189 | }).then(res => {
190 | let data = res.data
191 | if (data.BaseResponse.Ret === this.CONF.SYNCCHECK_RET_LOGOUT) {
192 | throw new AlreadyLogoutError()
193 | }
194 | assert.equal(data.BaseResponse.Ret, 0, res)
195 | this.PROP.skey = data.SKey || this.PROP.skey
196 | this.updateSyncKey(data)
197 | Object.assign(this.user, data.User)
198 | return data
199 | })
200 | }).catch(err => {
201 | debug(err)
202 | err.tips = '微信初始化失败'
203 | throw err
204 | })
205 | }
206 |
207 | notifyMobile (to) {
208 | return Promise.resolve().then(() => {
209 | let params = {
210 | pass_ticket: this.PROP.passTicket,
211 | lang: 'zh_CN'
212 | }
213 | let data = {
214 | 'BaseRequest': this.getBaseRequest(),
215 | 'Code': to ? 1 : 3,
216 | 'FromUserName': this.user['UserName'],
217 | 'ToUserName': to || this.user['UserName'],
218 | 'ClientMsgId': Date.now()
219 | }
220 | return this.request({
221 | method: 'POST',
222 | url: this.CONF.API_webwxstatusnotify,
223 | params: params,
224 | data: data
225 | }).then(res => {
226 | let data = res.data
227 | assert.equal(data.BaseResponse.Ret, 0, res)
228 | })
229 | }).catch(err => {
230 | debug(err)
231 | err.tips = '手机状态通知失败'
232 | throw err
233 | })
234 | }
235 |
236 | getContact (seq = 0) {
237 | return Promise.resolve().then(() => {
238 | let params = {
239 | // 'pass_ticket': this.PROP.passTicket,
240 | 'seq': seq,
241 | 'skey': this.PROP.skey,
242 | 'r': +new Date()
243 | }
244 | return this.request({
245 | method: 'GET',
246 | url: this.CONF.API_webwxgetcontact,
247 | params: params
248 | }).then(res => {
249 | let data = res.data
250 | assert.equal(data.BaseResponse.Ret, 0, res)
251 | return data
252 | })
253 | }).catch(err => {
254 | debug(err)
255 | err.tips = '获取通讯录失败'
256 | throw err
257 | })
258 | }
259 |
260 | batchGetContact (contacts) {
261 | return Promise.resolve().then(() => {
262 | let params = {
263 | 'pass_ticket': this.PROP.passTicket,
264 | 'type': 'ex',
265 | 'r': +new Date(),
266 | 'lang': 'zh_CN'
267 | }
268 | let data = {
269 | 'BaseRequest': this.getBaseRequest(),
270 | 'Count': contacts.length,
271 | 'List': contacts
272 | }
273 | return this.request({
274 | method: 'POST',
275 | url: this.CONF.API_webwxbatchgetcontact,
276 | params: params,
277 | data: data
278 | }).then(res => {
279 | let data = res.data
280 | assert.equal(data.BaseResponse.Ret, 0, res)
281 |
282 | return data.ContactList
283 | })
284 | }).catch(err => {
285 | debug(err)
286 | err.tips = '批量获取联系人失败'
287 | throw err
288 | })
289 | }
290 |
291 | statReport (text) {
292 | return Promise.resolve().then(() => {
293 | text = text || {
294 | 'type': '[action-record]',
295 | 'data': {
296 | 'actions': [{
297 | 'type': 'click',
298 | 'action': '发送框',
299 | 'time': +new Date()
300 | }]
301 | }
302 | }
303 | text = JSON.stringify(text)
304 | let params = {
305 | 'pass_ticket': this.PROP.passTicket,
306 | 'fun': 'new',
307 | 'lang': 'zh_CN'
308 | }
309 | let data = {
310 | 'BaseRequest': this.getBaseRequest(),
311 | 'Count': 1,
312 | 'List': [{
313 | 'Text': text,
314 | 'Type': 1
315 | }]
316 | }
317 | return this.request({
318 | method: 'POST',
319 | url: this.CONF.API_webwxreport,
320 | params: params,
321 | data: data
322 | })
323 | }).catch(err => {
324 | debug(err)
325 | err.tips = '状态报告失败'
326 | throw err
327 | })
328 | }
329 |
330 | syncCheck () {
331 | return Promise.resolve().then(() => {
332 | let params = {
333 | 'r': +new Date(),
334 | 'sid': this.PROP.sid,
335 | 'uin': this.PROP.uin,
336 | 'skey': this.PROP.skey,
337 | 'deviceid': getDeviceID(),
338 | 'synckey': this.PROP.formatedSyncKey
339 | }
340 | return this.request({
341 | method: 'GET',
342 | url: this.CONF.API_synccheck,
343 | params: params
344 | }).then(res => {
345 | let window = {
346 | synccheck: {}
347 | }
348 |
349 | try {
350 | // eslint-disable-next-line
351 | eval(res.data)
352 | } catch (ex) {
353 | window.synccheck = { retcode: '0', selector: '0' }
354 | }
355 | if (window.synccheck.retcode === this.CONF.SYNCCHECK_RET_LOGOUT) {
356 | throw new AlreadyLogoutError()
357 | }
358 | assert.equal(window.synccheck.retcode, this.CONF.SYNCCHECK_RET_SUCCESS, res)
359 | return window.synccheck.selector
360 | })
361 | }).catch(err => {
362 | debug(err)
363 | err.tips = '同步失败'
364 | throw err
365 | })
366 | }
367 |
368 | sync () {
369 | return Promise.resolve().then(() => {
370 | let params = {
371 | 'sid': this.PROP.sid,
372 | 'skey': this.PROP.skey,
373 | 'pass_ticket': this.PROP.passTicket,
374 | 'lang': 'zh_CN'
375 | }
376 | let data = {
377 | 'BaseRequest': this.getBaseRequest(),
378 | 'SyncKey': this.PROP.syncKey,
379 | 'rr': ~new Date()
380 | }
381 | return this.request({
382 | method: 'POST',
383 | url: this.CONF.API_webwxsync,
384 | params: params,
385 | data: data
386 | }).then(res => {
387 | let data = res.data
388 | if (data.BaseResponse.Ret === this.CONF.SYNCCHECK_RET_LOGOUT) {
389 | throw new AlreadyLogoutError()
390 | }
391 | assert.equal(data.BaseResponse.Ret, 0, res)
392 |
393 | this.updateSyncKey(data)
394 | this.PROP.skey = data.SKey || this.PROP.skey
395 | return data
396 | })
397 | }).catch(err => {
398 | debug(err)
399 | err.tips = '获取新信息失败'
400 | throw err
401 | })
402 | }
403 |
404 | updateSyncKey (data) {
405 | if (data.SyncKey) {
406 | this.PROP.syncKey = data.SyncKey
407 | }
408 | if (data.SyncCheckKey) {
409 | let synckeylist = []
410 | for (let e = data.SyncCheckKey.List, o = 0, n = e.length; n > o; o++) {
411 | synckeylist.push(e[o]['Key'] + '_' + e[o]['Val'])
412 | }
413 | this.PROP.formatedSyncKey = synckeylist.join('|')
414 | } else if (!this.PROP.formatedSyncKey && data.SyncKey) {
415 | let synckeylist = []
416 | for (let e = data.SyncKey.List, o = 0, n = e.length; n > o; o++) {
417 | synckeylist.push(e[o]['Key'] + '_' + e[o]['Val'])
418 | }
419 | this.PROP.formatedSyncKey = synckeylist.join('|')
420 | }
421 | }
422 |
423 | logout () {
424 | return Promise.resolve().then(() => {
425 | let params = {
426 | redirect: 1,
427 | type: 0,
428 | skey: this.PROP.skey,
429 | lang: 'zh_CN'
430 | }
431 |
432 | // data加上会出错,不加data也能登出
433 | // let data = {
434 | // sid: this.PROP.sid,
435 | // uin: this.PROP.uin
436 | // }
437 | return this.request({
438 | method: 'POST',
439 | url: this.CONF.API_webwxlogout,
440 | params: params
441 | }).then(res => {
442 | return '登出成功'
443 | }).catch(err => {
444 | debug(err)
445 | return '可能登出成功'
446 | })
447 | })
448 | }
449 |
450 | sendText (msg, to) {
451 | return Promise.resolve().then(() => {
452 | let params = {
453 | 'pass_ticket': this.PROP.passTicket,
454 | 'lang': 'zh_CN'
455 | }
456 | let clientMsgId = getClientMsgId()
457 | let data = {
458 | 'BaseRequest': this.getBaseRequest(),
459 | 'Scene': 0,
460 | 'Msg': {
461 | 'Type': this.CONF.MSGTYPE_TEXT,
462 | 'Content': msg,
463 | 'FromUserName': this.user['UserName'],
464 | 'ToUserName': to,
465 | 'LocalID': clientMsgId,
466 | 'ClientMsgId': clientMsgId
467 | }
468 | }
469 | return this.request({
470 | method: 'POST',
471 | url: this.CONF.API_webwxsendmsg,
472 | params: params,
473 | data: data
474 | }).then(res => {
475 | let data = res.data
476 | assert.equal(data.BaseResponse.Ret, 0, res)
477 | return data
478 | })
479 | }).catch(err => {
480 | debug(err)
481 | err.tips = '发送文本信息失败'
482 | throw err
483 | })
484 | }
485 |
486 | sendEmoticon (id, to) {
487 | return Promise.resolve().then(() => {
488 | let params = {
489 | 'fun': 'sys',
490 | 'pass_ticket': this.PROP.passTicket,
491 | 'lang': 'zh_CN'
492 | }
493 | let clientMsgId = getClientMsgId()
494 | let data = {
495 | 'BaseRequest': this.getBaseRequest(),
496 | 'Scene': 0,
497 | 'Msg': {
498 | 'Type': this.CONF.MSGTYPE_EMOTICON,
499 | 'EmojiFlag': 2,
500 | 'FromUserName': this.user['UserName'],
501 | 'ToUserName': to,
502 | 'LocalID': clientMsgId,
503 | 'ClientMsgId': clientMsgId
504 | }
505 | }
506 |
507 | if (id.indexOf('@') === 0) {
508 | data.Msg.MediaId = id
509 | } else {
510 | data.Msg.EMoticonMd5 = id
511 | }
512 |
513 | return this.request({
514 | method: 'POST',
515 | url: this.CONF.API_webwxsendemoticon,
516 | params: params,
517 | data: data
518 | }).then(res => {
519 | let data = res.data
520 | assert.equal(data.BaseResponse.Ret, 0, res)
521 | return data
522 | })
523 | }).catch(err => {
524 | debug(err)
525 | err.tips = '发送表情信息失败'
526 | throw err
527 | })
528 | }
529 |
530 | // 根据文件大小切割form
531 | getMediaFormStreamData ({ name, data, type, mediatype, size, toUserName }) {
532 | let uploadMediaRequest = JSON.stringify({
533 | BaseRequest: this.getBaseRequest(),
534 | ClientMediaId: getClientMsgId(),
535 | TotalLen: size,
536 | StartPos: 0,
537 | DataLen: size,
538 | MediaType: 4,
539 | UploadType: 2,
540 | FromUserName: this.user.UserName,
541 | ToUserName: toUserName || this.user.UserName
542 | })
543 |
544 | // 小于0.5mb的文件直接返回form
545 | if (size <= CHUNK_SIZE) {
546 | let form = new FormData()
547 | form.append('name', name)
548 | form.append('type', type)
549 | form.append('lastModifiedDate', new Date().toGMTString())
550 | form.append('size', size)
551 | form.append('mediatype', mediatype)
552 | form.append('uploadmediarequest', uploadMediaRequest)
553 | form.append('webwx_data_ticket', this.PROP.webwxDataTicket)
554 | form.append('pass_ticket', encodeURI(this.PROP.passTicket))
555 | form.append('filename', data, {
556 | filename: name,
557 | contentType: type,
558 | knownLength: size
559 | })
560 |
561 | return form
562 | }
563 |
564 | // 大于0.5mb的文件要切割 chunk
565 | const totalChunksNum = Math.ceil(size / CHUNK_SIZE)
566 | const formList = []
567 |
568 | for (let i = 0; i < totalChunksNum; i++) {
569 | let startPos = i * CHUNK_SIZE
570 | let endPos = Math.min(size, startPos + CHUNK_SIZE)
571 | let chunk = data.slice(startPos, endPos)
572 |
573 | // 创建每个块的 FormData
574 | const form = new FormData()
575 | form.append('name', name)
576 | form.append('type', type)
577 | form.append('lastModifiedDate', new Date().toGMTString())
578 | form.append('size', size)
579 | form.append('mediatype', mediatype)
580 | form.append('uploadmediarequest', uploadMediaRequest)
581 | form.append('webwx_data_ticket', this.PROP.webwxDataTicket)
582 | form.append('pass_ticket', encodeURI(this.PROP.passTicket))
583 | form.append('id', 'WU_FILE_0')
584 | form.append('chunk', i)
585 | form.append('chunks', totalChunksNum)
586 | form.append('filename', chunk, {
587 | filename: name,
588 | contentType: type,
589 | knownLength: chunk.length
590 | })
591 | formList.push({
592 | data: form,
593 | headers: form.getHeaders()
594 | })
595 | }
596 |
597 | return formList
598 | }
599 |
600 | // file: Stream, Buffer, File, Blob
601 | uploadMedia (file, filename, toUserName) {
602 | return Promise.resolve().then(() => {
603 | let name, type, size, ext, mediatype, data
604 | return new Promise((resolve, reject) => {
605 | if ((typeof (File) !== 'undefined' && file.constructor === File) ||
606 | (typeof (Blob) !== 'undefined' && file.constructor === Blob)) {
607 | name = file.name || 'file'
608 | type = file.type
609 | size = file.size
610 | data = file
611 | return resolve()
612 | } else if (Buffer.isBuffer(file)) {
613 | if (!filename) {
614 | return reject(new Error('文件名未知'))
615 | }
616 | name = filename
617 | type = mime.lookup(name)
618 | size = file.length
619 | data = file
620 | return resolve()
621 | } else if (file.readable) {
622 | if (!file.path && !filename) {
623 | return reject(new Error('文件名未知'))
624 | }
625 | name = path.basename(file.path || filename)
626 | type = mime.lookup(name)
627 | file.pipe(bl((err, buffer) => {
628 | if (err) {
629 | return reject(err)
630 | }
631 | size = buffer.length
632 | data = buffer
633 | return resolve()
634 | }))
635 | }
636 | }).then(() => {
637 | ext = name.match(/.*\.(.*)/)
638 | if (ext) {
639 | ext = ext[1].toLowerCase()
640 | } else {
641 | ext = ''
642 | }
643 |
644 | switch (ext) {
645 | case 'bmp':
646 | case 'jpeg':
647 | case 'jpg':
648 | case 'png':
649 | mediatype = 'pic'
650 | break
651 | case 'mp4':
652 | mediatype = 'video'
653 | break
654 | default:
655 | mediatype = 'doc'
656 | }
657 |
658 | const formOrFormList = this.getMediaFormStreamData({ name, data, type, mediatype, size, toUserName })
659 |
660 | return new Promise((resolve, reject) => {
661 | if (isStandardBrowserEnv) {
662 | return resolve({
663 | data: formOrFormList,
664 | headers: {}
665 | })
666 | } else if (Array.isArray(formOrFormList)) {
667 | const bufferList = formOrFormList.reduce((arr, formObj) => {
668 | formObj.data.pipe(bl((err, buffer) => {
669 | if (err) {
670 | return reject(err)
671 | }
672 |
673 | arr.push({ data: buffer, headers: formObj.headers })
674 | }))
675 | return arr
676 | }, [])
677 |
678 | return resolve({ data: bufferList })
679 | } else {
680 | formOrFormList.pipe(bl((err, buffer) => {
681 | if (err) {
682 | return reject(err)
683 | }
684 | return resolve({
685 | data: buffer,
686 | headers: formOrFormList.getHeaders()
687 | })
688 | }))
689 | }
690 | })
691 | }).then(data => {
692 | let params = {
693 | f: 'json'
694 | }
695 |
696 | // 单块文件上传
697 | if (!Array.isArray(data.data)) {
698 | return this.request({
699 | method: 'POST',
700 | url: this.CONF.API_webwxuploadmedia,
701 | headers: data.headers,
702 | params,
703 | data: data.data
704 | })
705 | }
706 |
707 | const bufferList = data.data
708 | let currentChunkIndex = 0
709 |
710 | // 分块上传逻辑
711 | const processChunk = res => {
712 | if (currentChunkIndex < bufferList.length) {
713 | const chunkObj = bufferList[currentChunkIndex]
714 | return this.request({
715 | method: 'POST',
716 | url: this.CONF.API_webwxuploadmedia,
717 | headers: chunkObj.headers,
718 | params,
719 | data: chunkObj.data
720 | }).then(res => {
721 | currentChunkIndex++
722 | // 递归处理下一个块
723 | return processChunk(res)
724 | })
725 | } else {
726 | // 所有块上传完成
727 | return Promise.resolve({data: {
728 | MediaId: res.data.MediaId
729 | }})
730 | }
731 | }
732 |
733 | // 开始处理第一个块
734 | return processChunk()
735 | }).then(res => {
736 | let data = res.data
737 | let mediaId = data.MediaId
738 | assert.ok(mediaId, res)
739 |
740 | return {
741 | name: name,
742 | size: size,
743 | ext: ext,
744 | mediatype: mediatype,
745 | mediaId: mediaId
746 | }
747 | })
748 | }).catch(err => {
749 | debug(err)
750 | err.tips = '上传媒体文件失败'
751 | throw err
752 | })
753 | }
754 |
755 | sendPic (mediaId, to) {
756 | return Promise.resolve().then(() => {
757 | let params = {
758 | 'pass_ticket': this.PROP.passTicket,
759 | 'fun': 'async',
760 | 'f': 'json',
761 | 'lang': 'zh_CN'
762 | }
763 | let clientMsgId = getClientMsgId()
764 | let data = {
765 | 'BaseRequest': this.getBaseRequest(),
766 | 'Scene': 0,
767 | 'Msg': {
768 | 'Type': this.CONF.MSGTYPE_IMAGE,
769 | 'MediaId': mediaId,
770 | 'FromUserName': this.user.UserName,
771 | 'ToUserName': to,
772 | 'LocalID': clientMsgId,
773 | 'ClientMsgId': clientMsgId
774 | }
775 | }
776 | return this.request({
777 | method: 'POST',
778 | url: this.CONF.API_webwxsendmsgimg,
779 | params: params,
780 | data: data
781 | }).then(res => {
782 | let data = res.data
783 | assert.equal(data.BaseResponse.Ret, 0, res)
784 | return data
785 | })
786 | }).catch(err => {
787 | debug(err)
788 | err.tips = '发送图片失败'
789 | throw err
790 | })
791 | }
792 |
793 | sendVideo (mediaId, to) {
794 | return Promise.resolve().then(() => {
795 | let params = {
796 | 'pass_ticket': this.PROP.passTicket,
797 | 'fun': 'async',
798 | 'f': 'json',
799 | 'lang': 'zh_CN'
800 | }
801 | let clientMsgId = getClientMsgId()
802 | let data = {
803 | 'BaseRequest': this.getBaseRequest(),
804 | 'Scene': 0,
805 | 'Msg': {
806 | 'Type': this.CONF.MSGTYPE_VIDEO,
807 | 'MediaId': mediaId,
808 | 'FromUserName': this.user.UserName,
809 | 'ToUserName': to,
810 | 'LocalID': clientMsgId,
811 | 'ClientMsgId': clientMsgId
812 | }
813 | }
814 | return this.request({
815 | method: 'POST',
816 | url: this.CONF.API_webwxsendmsgvedio,
817 | params: params,
818 | data: data
819 | }).then(res => {
820 | let data = res.data
821 | assert.equal(data.BaseResponse.Ret, 0, res)
822 | return data
823 | })
824 | }).catch(err => {
825 | debug(err)
826 | err.tips = '发送视频失败'
827 | throw err
828 | })
829 | }
830 |
831 | sendDoc (mediaId, name, size, ext, to) {
832 | return Promise.resolve().then(() => {
833 | let params = {
834 | 'pass_ticket': this.PROP.passTicket,
835 | 'fun': 'async',
836 | 'f': 'json',
837 | 'lang': 'zh_CN'
838 | }
839 | let clientMsgId = getClientMsgId()
840 | let data = {
841 | 'BaseRequest': this.getBaseRequest(),
842 | 'Scene': 0,
843 | 'Msg': {
844 | 'Type': this.CONF.APPMSGTYPE_ATTACH,
845 | 'Content': `${name}6${size}${mediaId}${ext}`,
846 | 'FromUserName': this.user.UserName,
847 | 'ToUserName': to,
848 | 'LocalID': clientMsgId,
849 | 'ClientMsgId': clientMsgId
850 | }
851 | }
852 | return this.request({
853 | method: 'POST',
854 | url: this.CONF.API_webwxsendappmsg,
855 | params: params,
856 | data: data
857 | }).then(res => {
858 | let data = res.data
859 | assert.equal(data.BaseResponse.Ret, 0, res)
860 | return data
861 | })
862 | }).catch(err => {
863 | debug(err)
864 | err.tips = '发送文件失败'
865 | throw err
866 | })
867 | }
868 |
869 | forwardMsg (msg, to) {
870 | return Promise.resolve().then(() => {
871 | let params = {
872 | 'pass_ticket': this.PROP.passTicket,
873 | 'fun': 'async',
874 | 'f': 'json',
875 | 'lang': 'zh_CN'
876 | }
877 | let clientMsgId = getClientMsgId()
878 | let data = {
879 | 'BaseRequest': this.getBaseRequest(),
880 | 'Scene': 2,
881 | 'Msg': {
882 | 'Type': msg.MsgType,
883 | 'MediaId': '',
884 | 'Content': msg.Content.replace(/</g, '<').replace(/>/g, '>').replace(/^.*:\n/, ''),
885 | 'FromUserName': this.user.UserName,
886 | 'ToUserName': to,
887 | 'LocalID': clientMsgId,
888 | 'ClientMsgId': clientMsgId
889 | }
890 | }
891 | let url
892 | switch (msg.MsgType) {
893 | case this.CONF.MSGTYPE_TEXT:
894 | url = this.CONF.API_webwxsendmsg
895 | if (msg.SubMsgType === this.CONF.MSGTYPE_LOCATION) {
896 | data.Msg.Type = this.CONF.MSGTYPE_LOCATION
897 | data.Msg.Content = msg.OriContent.replace(/</g, '<').replace(/>/g, '>')
898 | }
899 | break
900 | case this.CONF.MSGTYPE_IMAGE:
901 | url = this.CONF.API_webwxsendmsgimg
902 | break
903 | case this.CONF.MSGTYPE_EMOTICON:
904 | url = this.CONF.API_webwxsendemoticon
905 | params.fun = 'sys'
906 | data.Msg.EMoticonMd5 = msg.Content.replace(/^[\s\S]*?md5\s?=\s?"(.*?)"[\s\S]*?$/, '$1')
907 | if (!data.Msg.EMoticonMd5) {
908 | throw new Error('商店表情不能转发')
909 | }
910 | data.Msg.EmojiFlag = 2
911 | data.Scene = 0
912 | delete data.Msg.MediaId
913 | delete data.Msg.Content
914 | break
915 | case this.CONF.MSGTYPE_MICROVIDEO:
916 | case this.CONF.MSGTYPE_VIDEO:
917 | url = this.CONF.API_webwxsendmsgvedio
918 | data.Msg.Type = this.CONF.MSGTYPE_VIDEO
919 | break
920 | case this.CONF.MSGTYPE_APP:
921 | url = this.CONF.API_webwxsendappmsg
922 | data.Msg.Type = msg.AppMsgType
923 | data.Msg.Content = data.Msg.Content.replace(
924 | /^[\s\S]*?()[\s\S]*?(<\/attachid>[\s\S]*?<\/appmsg>)[\s\S]*?$/,
925 | `$1${msg.MediaId}$2`)
926 | break
927 | default:
928 | throw new Error('该消息类型不能直接转发')
929 | }
930 | return this.request({
931 | method: 'POST',
932 | url: url,
933 | params: params,
934 | data: data
935 | }).then(res => {
936 | let data = res.data
937 | assert.equal(data.BaseResponse.Ret, 0, res)
938 | return data
939 | })
940 | }).catch(err => {
941 | debug(err)
942 | err.tips = '转发消息失败'
943 | throw err
944 | })
945 | }
946 |
947 | getMsgImg (msgId) {
948 | return Promise.resolve().then(() => {
949 | let params = {
950 | MsgID: msgId,
951 | skey: this.PROP.skey,
952 | type: 'big'
953 | }
954 |
955 | return this.request({
956 | method: 'GET',
957 | url: this.CONF.API_webwxgetmsgimg,
958 | params: params,
959 | responseType: 'arraybuffer'
960 | }).then(res => {
961 | return {
962 | data: res.data,
963 | type: res.headers['content-type']
964 | }
965 | })
966 | }).catch(err => {
967 | debug(err)
968 | err.tips = '获取图片或表情失败'
969 | throw err
970 | })
971 | }
972 |
973 | getVideo (msgId) {
974 | return Promise.resolve().then(() => {
975 | let params = {
976 | MsgID: msgId,
977 | skey: this.PROP.skey
978 | }
979 |
980 | return this.request({
981 | method: 'GET',
982 | url: this.CONF.API_webwxgetvideo,
983 | headers: {
984 | 'Range': 'bytes=0-'
985 | },
986 | params: params,
987 | responseType: 'arraybuffer'
988 | }).then(res => {
989 | return {
990 | data: res.data,
991 | type: res.headers['content-type']
992 | }
993 | })
994 | }).catch(err => {
995 | debug(err)
996 | err.tips = '获取视频失败'
997 | throw err
998 | })
999 | }
1000 |
1001 | getVoice (msgId) {
1002 | return Promise.resolve().then(() => {
1003 | let params = {
1004 | MsgID: msgId,
1005 | skey: this.PROP.skey
1006 | }
1007 |
1008 | return this.request({
1009 | method: 'GET',
1010 | url: this.CONF.API_webwxgetvoice,
1011 | params: params,
1012 | responseType: 'arraybuffer'
1013 | }).then(res => {
1014 | return {
1015 | data: res.data,
1016 | type: res.headers['content-type']
1017 | }
1018 | })
1019 | }).catch(err => {
1020 | debug(err)
1021 | err.tips = '获取声音失败'
1022 | throw err
1023 | })
1024 | }
1025 |
1026 | getHeadImg (HeadImgUrl) {
1027 | return Promise.resolve().then(() => {
1028 | let url = this.CONF.origin + HeadImgUrl
1029 | return this.request({
1030 | method: 'GET',
1031 | url: url,
1032 | responseType: 'arraybuffer'
1033 | }).then(res => {
1034 | return {
1035 | data: res.data,
1036 | type: res.headers['content-type']
1037 | }
1038 | })
1039 | }).catch(err => {
1040 | debug(err)
1041 | err.tips = '获取头像失败'
1042 | throw err
1043 | })
1044 | }
1045 |
1046 | getDoc (FromUserName, MediaId, FileName) {
1047 | return Promise.resolve().then(() => {
1048 | let params = {
1049 | sender: FromUserName,
1050 | mediaid: MediaId,
1051 | filename: FileName,
1052 | fromuser: this.user.UserName,
1053 | pass_ticket: this.PROP.passTicket,
1054 | webwx_data_ticket: this.PROP.webwxDataTicket
1055 | }
1056 | return this.request({
1057 | method: 'GET',
1058 | url: this.CONF.API_webwxdownloadmedia,
1059 | params: params,
1060 | responseType: 'arraybuffer'
1061 | }).then(res => {
1062 | return {
1063 | data: res.data,
1064 | type: res.headers['content-type']
1065 | }
1066 | })
1067 | }).catch(err => {
1068 | debug(err)
1069 | err.tips = '获取文件失败'
1070 | throw err
1071 | })
1072 | }
1073 |
1074 | verifyUser (UserName, Ticket) {
1075 | return Promise.resolve().then(() => {
1076 | let params = {
1077 | 'pass_ticket': this.PROP.passTicket,
1078 | 'lang': 'zh_CN'
1079 | }
1080 | let data = {
1081 | 'BaseRequest': this.getBaseRequest(),
1082 | 'Opcode': 3,
1083 | 'VerifyUserListSize': 1,
1084 | 'VerifyUserList': [{
1085 | 'Value': UserName,
1086 | 'VerifyUserTicket': Ticket
1087 | }],
1088 | 'VerifyContent': '',
1089 | 'SceneListCount': 1,
1090 | 'SceneList': [33],
1091 | 'skey': this.PROP.skey
1092 | }
1093 | return this.request({
1094 | method: 'POST',
1095 | url: this.CONF.API_webwxverifyuser,
1096 | params: params,
1097 | data: data
1098 | }).then(res => {
1099 | let data = res.data
1100 | assert.equal(data.BaseResponse.Ret, 0, res)
1101 | return data
1102 | })
1103 | }).catch(err => {
1104 | debug(err)
1105 | err.tips = '通过好友请求失败'
1106 | throw err
1107 | })
1108 | }
1109 |
1110 | /**
1111 | * 添加好友
1112 | * @param UserName 待添加用户的UserName
1113 | * @param content
1114 | * @returns {Promise.}
1115 | */
1116 | addFriend (UserName, content = '我是' + this.user.NickName) {
1117 | let params = {
1118 | 'pass_ticket': this.PROP.passTicket,
1119 | 'lang': 'zh_CN'
1120 | }
1121 |
1122 | let data = {
1123 | 'BaseRequest': this.getBaseRequest(),
1124 | 'Opcode': 2,
1125 | 'VerifyUserListSize': 1,
1126 | 'VerifyUserList': [{
1127 | 'Value': UserName,
1128 | 'VerifyUserTicket': ''
1129 | }],
1130 | 'VerifyContent': content,
1131 | 'SceneListCount': 1,
1132 | 'SceneList': [33],
1133 | 'skey': this.PROP.skey
1134 | }
1135 |
1136 | return this.request({
1137 | method: 'POST',
1138 | url: this.CONF.API_webwxverifyuser,
1139 | params: params,
1140 | data: data
1141 | }).then(res => {
1142 | let data = res.data
1143 | assert.equal(data.BaseResponse.Ret, 0, res)
1144 | return data
1145 | }).catch(err => {
1146 | debug(err)
1147 | err.tips = '添加好友失败'
1148 | throw err
1149 | })
1150 | }
1151 |
1152 | // Topic: Chatroom name
1153 | // MemberList format:
1154 | // [
1155 | // {"UserName":"@250d8d156ad9f8b068c2e3df3464ecf2"},
1156 | // {"UserName":"@42d725733741de6ac53cbe3738d8dd2e"}
1157 | // ]
1158 | createChatroom (Topic, MemberList) {
1159 | return Promise.resolve().then(() => {
1160 | let params = {
1161 | 'pass_ticket': this.PROP.passTicket,
1162 | 'lang': 'zh_CN',
1163 | 'r': ~new Date()
1164 | }
1165 | let data = {
1166 | BaseRequest: this.getBaseRequest(),
1167 | MemberCount: MemberList.length,
1168 | MemberList: MemberList,
1169 | Topic: Topic
1170 | }
1171 | return this.request({
1172 | method: 'POST',
1173 | url: this.CONF.API_webwxcreatechatroom,
1174 | params: params,
1175 | data: data
1176 | }).then(res => {
1177 | let data = res.data
1178 | assert.equal(data.BaseResponse.Ret, 0, res)
1179 | return data
1180 | })
1181 | }).catch(err => {
1182 | debug(err)
1183 | err.tips = '创建群失败'
1184 | throw err
1185 | })
1186 | }
1187 |
1188 | // fun: 'addmember' or 'delmember' or 'invitemember'
1189 | updateChatroom (ChatRoomUserName, MemberList, fun) {
1190 | return Promise.resolve().then(() => {
1191 | let params = {
1192 | fun: fun
1193 | }
1194 | let data = {
1195 | BaseRequest: this.getBaseRequest(),
1196 | ChatRoomName: ChatRoomUserName
1197 | }
1198 | if (fun === 'addmember') {
1199 | data.AddMemberList = MemberList.toString()
1200 | } else if (fun === 'delmember') {
1201 | data.DelMemberList = MemberList.toString()
1202 | } else if (fun === 'invitemember') {
1203 | data.InviteMemberList = MemberList.toString()
1204 | }
1205 | return this.request({
1206 | method: 'POST',
1207 | url: this.CONF.API_webwxupdatechatroom,
1208 | params: params,
1209 | data: data
1210 | }).then(res => {
1211 | let data = res.data
1212 | assert.equal(data.BaseResponse.Ret, 0, res)
1213 | return data
1214 | })
1215 | }).catch(err => {
1216 | debug(err)
1217 | err.tips = '邀请或踢出群成员失败'
1218 | throw err
1219 | })
1220 | }
1221 |
1222 | // OP: 1 联系人置顶 0 取消置顶
1223 | // 若不传RemarkName,则会覆盖以设置的联系人备注名
1224 | opLog (UserName, OP, RemarkName) {
1225 | return Promise.resolve().then(() => {
1226 | let params = {
1227 | pass_ticket: this.PROP.passTicket
1228 | }
1229 | let data = {
1230 | BaseRequest: this.getBaseRequest(),
1231 | CmdId: 3,
1232 | OP: OP,
1233 | RemarkName: RemarkName,
1234 | UserName: UserName
1235 | }
1236 | return this.request({
1237 | method: 'POST',
1238 | url: this.CONF.API_webwxoplog,
1239 | params: params,
1240 | data: data
1241 | }).then(res => {
1242 | let data = res.data
1243 | assert.equal(data.BaseResponse.Ret, 0, res)
1244 | return data
1245 | })
1246 | }).catch(err => {
1247 | debug(err)
1248 | err.tips = '置顶或取消置顶失败'
1249 | throw err
1250 | })
1251 | }
1252 |
1253 | updateRemarkName (UserName, RemarkName) {
1254 | return Promise.resolve().then(() => {
1255 | let params = {
1256 | pass_ticket: this.PROP.passTicket,
1257 | 'lang': 'zh_CN'
1258 | }
1259 | let data = {
1260 | BaseRequest: this.getBaseRequest(),
1261 | CmdId: 2,
1262 | RemarkName: RemarkName,
1263 | UserName: UserName
1264 | }
1265 | return this.request({
1266 | method: 'POST',
1267 | url: this.CONF.API_webwxoplog,
1268 | params: params,
1269 | data: data
1270 | }).then(res => {
1271 | let data = res.data
1272 | assert.equal(data.BaseResponse.Ret, 0, res)
1273 | return data
1274 | })
1275 | }).catch(err => {
1276 | debug(err)
1277 | err.tips = '设置用户标签失败'
1278 | throw err
1279 | })
1280 | }
1281 |
1282 | updateChatRoomName (ChatRoomUserName, NewName) {
1283 | return Promise.resolve().then(() => {
1284 | let params = {
1285 | 'fun': 'modtopic'
1286 | }
1287 | let data = {
1288 | BaseRequest: this.getBaseRequest(),
1289 | ChatRoomName: ChatRoomUserName,
1290 | NewTopic: NewName
1291 | }
1292 | return this.request({
1293 | method: 'POST',
1294 | url: this.CONF.API_webwxupdatechatroom,
1295 | params: params,
1296 | data: data
1297 | }).then(res => {
1298 | let data = res.data
1299 | assert.equal(data.BaseResponse.Ret, 0, res)
1300 | })
1301 | }).catch(err => {
1302 | debug(err)
1303 | throw new Error('更新群名失败')
1304 | })
1305 | }
1306 |
1307 | revokeMsg (msgId, toUserName) {
1308 | return Promise.resolve().then(() => {
1309 | let data = {
1310 | BaseRequest: this.getBaseRequest(),
1311 | SvrMsgId: msgId,
1312 | ToUserName: toUserName,
1313 | ClientMsgId: getClientMsgId()
1314 | }
1315 | let headers = {}
1316 | headers['ContentType'] = 'application/json; charset=UTF-8'
1317 | headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
1318 | return this.request({
1319 | method: 'POST',
1320 | url: this.CONF.API_webwxrevokemsg,
1321 | data: data,
1322 | headers: headers
1323 | }).then(res => {
1324 | let data = res.data
1325 | assert.equal(data.BaseResponse.Ret, 0, res)
1326 | return data
1327 | })
1328 | }).catch(err => {
1329 | debug(err)
1330 | throw new Error('撤回消息失败')
1331 | })
1332 | }
1333 |
1334 | getBaseRequest () {
1335 | return {
1336 | Uin: parseInt(this.PROP.uin),
1337 | Sid: this.PROP.sid,
1338 | Skey: this.PROP.skey,
1339 | DeviceID: getDeviceID()
1340 | }
1341 | }
1342 | }
1343 |
--------------------------------------------------------------------------------
/src/interface/contact.js:
--------------------------------------------------------------------------------
1 | import {
2 | convertEmoji,
3 | getCONF
4 | } from '../util'
5 | const CONF = getCONF()
6 |
7 | /* Contact Object Example
8 | {
9 | "Uin": 0,
10 | "UserName": "",
11 | "NickName": "",
12 | "HeadImgUrl": "",
13 | "ContactFlag": 3,
14 | "MemberCount": 0,
15 | "MemberList": [],
16 | "RemarkName": "",
17 | "HideInputBarFlag": 0,
18 | "Sex": 0,
19 | "Signature": "",
20 | "VerifyFlag": 8,
21 | "OwnerUin": 0,
22 | "PYInitial": "",
23 | "PYQuanPin": "",
24 | "RemarkPYInitial": "",
25 | "RemarkPYQuanPin": "",
26 | "StarFriend": 0,
27 | "AppAccountFlag": 0,
28 | "Statues": 0,
29 | "AttrStatus": 0,
30 | "Province": "",
31 | "City": "",
32 | "Alias": "Urinxs",
33 | "SnsFlag": 0,
34 | "UniFriend": 0,
35 | "DisplayName": "",
36 | "ChatRoomId": 0,
37 | "KeyWord": "gh_",
38 | "EncryChatRoomId": ""
39 | }
40 | */
41 | export function getUserByUserName (memberList, UserName) {
42 | if (!memberList.length) return null
43 |
44 | return memberList.find(contact => contact.UserName === UserName)
45 | }
46 |
47 | export function getDisplayName (contact) {
48 | if (isRoomContact(contact)) {
49 | if (contact.MemberCount >= 2) {
50 | return '[群] ' + (contact.RemarkName || contact.DisplayName || contact.NickName ||
51 | `${getDisplayName(contact.MemberList[0])}、${getDisplayName(contact.MemberList[1])}`)
52 | } else {
53 | return '[群] ' + (contact.RemarkName || contact.DisplayName || contact.NickName ||
54 | `${getDisplayName(contact.MemberList[0])}`)
55 | }
56 | } else {
57 | return contact.DisplayName || contact.RemarkName || contact.NickName || contact.UserName
58 | }
59 | }
60 |
61 | export function isRoomContact (contact) {
62 | return contact.UserName ? /^@@|@chatroom$/.test(contact.UserName) : false
63 | }
64 |
65 | export function isSpContact (contact) {
66 | return CONF.SPECIALUSERS.indexOf(contact.UserName) >= 0
67 | }
68 |
69 | export function isPublicContact (contact) {
70 | return contact.VerifyFlag & CONF.MM_USERATTRVERIFYFALG_BIZ_BRAND
71 | }
72 |
73 | const contactProto = {
74 | init: function (instance) {
75 | // 纠正错误以后保持兼容
76 | this.OriginalNickName = this.OrignalNickName = this.NickName
77 | this.OriginalRemarkName = this.OrignalRemarkName = this.RemarkName
78 | this.OriginalDisplayName = this.OrignalDisplayName = this.DisplayName
79 | this.NickName = convertEmoji(this.NickName)
80 | this.RemarkName = convertEmoji(this.RemarkName)
81 | this.DisplayName = convertEmoji(this.DisplayName)
82 | this.isSelf = this.UserName === instance.user.UserName
83 |
84 | return this
85 | },
86 | getDisplayName: function () {
87 | return getDisplayName(this)
88 | },
89 | canSearch: function (keyword) {
90 | if (!keyword) return false
91 | keyword = keyword.toUpperCase()
92 |
93 | let isSatisfy = key => (key || '').toUpperCase().indexOf(keyword) >= 0
94 | return (
95 | isSatisfy(this.RemarkName) ||
96 | isSatisfy(this.RemarkPYQuanPin) ||
97 | isSatisfy(this.NickName) ||
98 | isSatisfy(this.PYQuanPin) ||
99 | isSatisfy(this.Alias) ||
100 | isSatisfy(this.KeyWord)
101 | )
102 | }
103 | }
104 |
105 | export default function ContactFactory (instance) {
106 | return {
107 | extend: function (contactObj) {
108 | contactObj = Object.setPrototypeOf(contactObj, contactProto)
109 | return contactObj.init(instance)
110 | },
111 | getUserByUserName: function (UserName) {
112 | return instance.contacts[UserName]
113 | },
114 | getSearchUser: function (keyword) {
115 | let users = []
116 | for (let key in instance.contacts) {
117 | if (instance.contacts[key].canSearch(keyword)) {
118 | users.push(instance.contacts[key])
119 | }
120 | }
121 | return users
122 | },
123 | isSelf: function (contact) {
124 | return contact.isSelf || contact.UserName === instance.user.UserName
125 | },
126 | getDisplayName,
127 | isRoomContact,
128 | isPublicContact,
129 | isSpContact
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/interface/message.js:
--------------------------------------------------------------------------------
1 | import {convertEmoji, formatNum} from '../util'
2 | /* Message Object Example
3 | {
4 | "FromUserName": "",
5 | "ToUserName": "",
6 | "Content": "",
7 | "StatusNotifyUserName": "",
8 | "ImgWidth": 0,
9 | "PlayLength": 0,
10 | "RecommendInfo": {},
11 | "StatusNotifyCode": 4,
12 | "NewMsgId": "",
13 | "Status": 3,
14 | "VoiceLength": 0,
15 | "ForwardFlag": 0,
16 | "AppMsgType": 0,
17 | "Ticket": "",
18 | "AppInfo": {...},
19 | "Url": "",
20 | "ImgStatus": 1,
21 | "MsgType": 1,
22 | "ImgHeight": 0,
23 | "MediaId": "",
24 | "MsgId": "",
25 | "FileName": "",
26 | "HasProductId": 0,
27 | "FileSize": "",
28 | "CreateTime": 0,
29 | "SubMsgType": 0
30 | }
31 | */
32 |
33 | const messageProto = {
34 | init: function (instance) {
35 | this.MsgType = +this.MsgType
36 | this.isSendBySelf = this.FromUserName === instance.user.UserName || this.FromUserName === ''
37 |
38 | this.OriginalContent = this.Content
39 | if (this.FromUserName.indexOf('@@') === 0) {
40 | this.Content = this.Content.replace(/^@.*?(?=:)/, match => {
41 | let user = instance.contacts[this.FromUserName].MemberList.find(member => {
42 | return member.UserName === match
43 | })
44 | return user ? instance.Contact.getDisplayName(user) : match
45 | })
46 | }
47 |
48 | this.Content = this.Content.replace(/</g, '<').replace(/>/g, '>').replace(/
/g, '\n')
49 | this.Content = convertEmoji(this.Content)
50 |
51 | return this
52 | },
53 | isSendBy: function (contact) {
54 | return this.FromUserName === contact.UserName
55 | },
56 | getPeerUserName: function () {
57 | return this.isSendBySelf ? this.ToUserName : this.FromUserName
58 | },
59 | getDisplayTime: function () {
60 | var time = new Date(1e3 * this.CreateTime)
61 | return time.getHours() + ':' + formatNum(time.getMinutes(), 2)
62 | }
63 | }
64 |
65 | export default function MessageFactory (instance) {
66 | return {
67 | extend: function (messageObj) {
68 | messageObj = Object.setPrototypeOf(messageObj, messageProto)
69 | return messageObj.init(instance)
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/util/conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | export const CONF = {
4 | LANG: 'zh-CN',
5 | EMOTICON_REG: 'img\\sclass="(qq)?emoji (qq)?emoji([\\da-f]*?)"\\s(text="[^<>(\\s]*")?\\s?src="[^<>(\\s]*"\\s*',
6 | RES_PATH: '/zh_CN/htmledition/v2/',
7 | oplogCmdId: {
8 | TOPCONTACT: 3,
9 | MODREMARKNAME: 2
10 | },
11 | SP_CONTACT_FILE_HELPER: 'filehelper',
12 | SP_CONTACT_NEWSAPP: 'newsapp',
13 | SP_CONTACT_RECOMMEND_HELPER: 'fmessage',
14 | CONTACTFLAG_CONTACT: 1,
15 | CONTACTFLAG_CHATCONTACT: 2,
16 | CONTACTFLAG_CHATROOMCONTACT: 4,
17 | CONTACTFLAG_BLACKLISTCONTACT: 8,
18 | CONTACTFLAG_DOMAINCONTACT: 16,
19 | CONTACTFLAG_HIDECONTACT: 32,
20 | CONTACTFLAG_FAVOURCONTACT: 64,
21 | CONTACTFLAG_3RDAPPCONTACT: 128,
22 | CONTACTFLAG_SNSBLACKLISTCONTACT: 256,
23 | CONTACTFLAG_NOTIFYCLOSECONTACT: 512,
24 | CONTACTFLAG_TOPCONTACT: 2048,
25 | MM_USERATTRVERIFYFALG_BIZ: 1,
26 | MM_USERATTRVERIFYFALG_FAMOUS: 2,
27 | MM_USERATTRVERIFYFALG_BIZ_BIG: 4,
28 | MM_USERATTRVERIFYFALG_BIZ_BRAND: 8,
29 | MM_USERATTRVERIFYFALG_BIZ_VERIFIED: 16,
30 | MM_DATA_TEXT: 1,
31 | MM_DATA_HTML: 2,
32 | MM_DATA_IMG: 3,
33 | MM_DATA_PRIVATEMSG_TEXT: 11,
34 | MM_DATA_PRIVATEMSG_HTML: 12,
35 | MM_DATA_PRIVATEMSG_IMG: 13,
36 | MM_DATA_VOICEMSG: 34,
37 | MM_DATA_PUSHMAIL: 35,
38 | MM_DATA_QMSG: 36,
39 | MM_DATA_VERIFYMSG: 37,
40 | MM_DATA_PUSHSYSTEMMSG: 38,
41 | MM_DATA_QQLIXIANMSG_IMG: 39,
42 | MM_DATA_POSSIBLEFRIEND_MSG: 40,
43 | MM_DATA_SHARECARD: 42,
44 | MM_DATA_VIDEO: 43,
45 | MM_DATA_VIDEO_IPHONE_EXPORT: 44,
46 | MM_DATA_EMOJI: 47,
47 | MM_DATA_LOCATION: 48,
48 | MM_DATA_APPMSG: 49,
49 | MM_DATA_VOIPMSG: 50,
50 | MM_DATA_STATUSNOTIFY: 51,
51 | MM_DATA_VOIPNOTIFY: 52,
52 | MM_DATA_VOIPINVITE: 53,
53 | MM_DATA_MICROVIDEO: 62,
54 | MM_DATA_SYSNOTICE: 9999,
55 | MM_DATA_SYS: 1e4,
56 | MM_DATA_RECALLED: 10002,
57 | MSGTYPE_TEXT: 1,
58 | MSGTYPE_IMAGE: 3,
59 | MSGTYPE_VOICE: 34,
60 | MSGTYPE_VIDEO: 43,
61 | MSGTYPE_MICROVIDEO: 62,
62 | MSGTYPE_EMOTICON: 47,
63 | MSGTYPE_APP: 49,
64 | MSGTYPE_VOIPMSG: 50,
65 | MSGTYPE_VOIPNOTIFY: 52,
66 | MSGTYPE_VOIPINVITE: 53,
67 | MSGTYPE_LOCATION: 48,
68 | MSGTYPE_STATUSNOTIFY: 51,
69 | MSGTYPE_SYSNOTICE: 9999,
70 | MSGTYPE_POSSIBLEFRIEND_MSG: 40,
71 | MSGTYPE_VERIFYMSG: 37,
72 | MSGTYPE_SHARECARD: 42,
73 | MSGTYPE_SYS: 1e4,
74 | MSGTYPE_RECALLED: 10002,
75 | MSG_SEND_STATUS_READY: 0,
76 | MSG_SEND_STATUS_SENDING: 1,
77 | MSG_SEND_STATUS_SUCC: 2,
78 | MSG_SEND_STATUS_FAIL: 5,
79 | APPMSGTYPE_TEXT: 1,
80 | APPMSGTYPE_IMG: 2,
81 | APPMSGTYPE_AUDIO: 3,
82 | APPMSGTYPE_VIDEO: 4,
83 | APPMSGTYPE_URL: 5,
84 | APPMSGTYPE_ATTACH: 6,
85 | APPMSGTYPE_OPEN: 7,
86 | APPMSGTYPE_EMOJI: 8,
87 | APPMSGTYPE_VOICE_REMIND: 9,
88 | APPMSGTYPE_SCAN_GOOD: 10,
89 | APPMSGTYPE_GOOD: 13,
90 | APPMSGTYPE_EMOTION: 15,
91 | APPMSGTYPE_CARD_TICKET: 16,
92 | APPMSGTYPE_REALTIME_SHARE_LOCATION: 17,
93 | APPMSGTYPE_TRANSFERS: 2e3,
94 | APPMSGTYPE_RED_ENVELOPES: 2001,
95 | APPMSGTYPE_READER_TYPE: 100001,
96 | UPLOAD_MEDIA_TYPE_IMAGE: 1,
97 | UPLOAD_MEDIA_TYPE_VIDEO: 2,
98 | UPLOAD_MEDIA_TYPE_AUDIO: 3,
99 | UPLOAD_MEDIA_TYPE_ATTACHMENT: 4,
100 | PROFILE_BITFLAG_NOCHANGE: 0,
101 | PROFILE_BITFLAG_CHANGE: 190,
102 | CHATROOM_NOTIFY_OPEN: 1,
103 | CHATROOM_NOTIFY_CLOSE: 0,
104 | StatusNotifyCode_READED: 1,
105 | StatusNotifyCode_ENTER_SESSION: 2,
106 | StatusNotifyCode_INITED: 3,
107 | StatusNotifyCode_SYNC_CONV: 4,
108 | StatusNotifyCode_QUIT_SESSION: 5,
109 | VERIFYUSER_OPCODE_ADDCONTACT: 1,
110 | VERIFYUSER_OPCODE_SENDREQUEST: 2,
111 | VERIFYUSER_OPCODE_VERIFYOK: 3,
112 | VERIFYUSER_OPCODE_VERIFYREJECT: 4,
113 | VERIFYUSER_OPCODE_SENDERREPLY: 5,
114 | VERIFYUSER_OPCODE_RECVERREPLY: 6,
115 | ADDSCENE_PF_QQ: 4,
116 | ADDSCENE_PF_EMAIL: 5,
117 | ADDSCENE_PF_CONTACT: 6,
118 | ADDSCENE_PF_WEIXIN: 7,
119 | ADDSCENE_PF_GROUP: 8,
120 | ADDSCENE_PF_UNKNOWN: 9,
121 | ADDSCENE_PF_MOBILE: 10,
122 | ADDSCENE_PF_WEB: 33,
123 | TIMEOUT_SYNC_CHECK: 0,
124 | EMOJI_FLAG_GIF: 2,
125 | KEYCODE_BACKSPACE: 8,
126 | KEYCODE_ENTER: 13,
127 | KEYCODE_SHIFT: 16,
128 | KEYCODE_ESC: 27,
129 | KEYCODE_DELETE: 34,
130 | KEYCODE_ARROW_LEFT: 37,
131 | KEYCODE_ARROW_UP: 38,
132 | KEYCODE_ARROW_RIGHT: 39,
133 | KEYCODE_ARROW_DOWN: 40,
134 | KEYCODE_NUM2: 50,
135 | KEYCODE_AT: 64,
136 | KEYCODE_NUM_ADD: 107,
137 | KEYCODE_NUM_MINUS: 109,
138 | KEYCODE_ADD: 187,
139 | KEYCODE_MINUS: 189,
140 | MM_NOTIFY_CLOSE: 0,
141 | MM_NOTIFY_OPEN: 1,
142 | MM_SOUND_CLOSE: 0,
143 | MM_SOUND_OPEN: 1,
144 | MM_SEND_FILE_STATUS_QUEUED: 0,
145 | MM_SEND_FILE_STATUS_SENDING: 1,
146 | MM_SEND_FILE_STATUS_SUCCESS: 2,
147 | MM_SEND_FILE_STATUS_FAIL: 3,
148 | MM_SEND_FILE_STATUS_CANCEL: 4,
149 | MM_EMOTICON_WEB: '_web',
150 |
151 | SYNCCHECK_RET_SUCCESS: 0,
152 | SYNCCHECK_RET_LOGOUT: 1101,
153 | SYNCCHECK_SELECTOR_NORMAL: 0,
154 | SYNCCHECK_SELECTOR_MSG: 2,
155 | SYNCCHECK_SELECTOR_MOBILEOPEN: 7,
156 | STATE: {
157 | init: 'init',
158 | uuid: 'uuid',
159 | login: 'login',
160 | logout: 'logout'
161 | },
162 | SPECIALUSERS: ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage',
163 | 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend',
164 | 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip',
165 | 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11',
166 | 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11',
167 | 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'
168 | ]
169 | }
170 |
171 | export function getCONF (host) {
172 | host = host || 'wx.qq.com'
173 | let origin = `https://${host}`
174 | let loginUrl = 'login.wx.qq.com'
175 | let fileUrl = 'file.wx.qq.com'
176 | let pushUrl = 'webpush.weixin.qq.com'
177 | let matchResult = host.match(/(\w+)(.qq.com|.wechat.com)/)
178 | if (matchResult && matchResult[1] && matchResult[2]) {
179 | let prefix = matchResult[1]
180 | let suffix = matchResult[2]
181 | if (suffix === '.qq.com') {
182 | prefix = ~['wx', 'wx2', 'wx8'].indexOf(prefix) ? prefix : 'wx'
183 | } else {
184 | prefix = ~['web', 'web2'].indexOf(prefix) ? prefix : 'web'
185 | }
186 | loginUrl = `login.${prefix}${suffix}`
187 | fileUrl = `file.${prefix}${suffix}`
188 | pushUrl = `webpush.${prefix}${suffix}`
189 | }
190 | let conf = {}
191 | conf.origin = origin
192 | conf.baseUri = origin + '/cgi-bin/mmwebwx-bin'
193 | conf.API_jsLogin = 'https://' + loginUrl + '/jslogin?appid=wx782c26e4c19acffb&fun=new&lang=zh-CN&redirect_uri=https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop'
194 | conf.API_login = 'https://' + loginUrl + '/cgi-bin/mmwebwx-bin/login'
195 | conf.API_synccheck = 'https://' + pushUrl + '/cgi-bin/mmwebwx-bin/synccheck'
196 | conf.API_webwxdownloadmedia = 'https://' + fileUrl + '/cgi-bin/mmwebwx-bin/webwxgetmedia'
197 | conf.API_webwxuploadmedia = 'https://' + fileUrl + '/cgi-bin/mmwebwx-bin/webwxuploadmedia'
198 | conf.API_webwxpreview = origin + '/cgi-bin/mmwebwx-bin/webwxpreview'
199 | conf.API_webwxinit = origin + '/cgi-bin/mmwebwx-bin/webwxinit'
200 | conf.API_webwxgetcontact = origin + '/cgi-bin/mmwebwx-bin/webwxgetcontact'
201 | conf.API_webwxsync = origin + '/cgi-bin/mmwebwx-bin/webwxsync'
202 | conf.API_webwxbatchgetcontact = origin + '/cgi-bin/mmwebwx-bin/webwxbatchgetcontact'
203 | conf.API_webwxgeticon = origin + '/cgi-bin/mmwebwx-bin/webwxgeticon'
204 | conf.API_webwxsendmsg = origin + '/cgi-bin/mmwebwx-bin/webwxsendmsg'
205 | conf.API_webwxsendmsgimg = origin + '/cgi-bin/mmwebwx-bin/webwxsendmsgimg'
206 | conf.API_webwxsendmsgvedio = origin + '/cgi-bin/mmwebwx-bin/webwxsendvideomsg'
207 | conf.API_webwxsendemoticon = origin + '/cgi-bin/mmwebwx-bin/webwxsendemoticon'
208 | conf.API_webwxsendappmsg = origin + '/cgi-bin/mmwebwx-bin/webwxsendappmsg'
209 | conf.API_webwxgetheadimg = origin + '/cgi-bin/mmwebwx-bin/webwxgetheadimg'
210 | conf.API_webwxgetmsgimg = origin + '/cgi-bin/mmwebwx-bin/webwxgetmsgimg'
211 | conf.API_webwxgetmedia = origin + '/cgi-bin/mmwebwx-bin/webwxgetmedia'
212 | conf.API_webwxgetvideo = origin + '/cgi-bin/mmwebwx-bin/webwxgetvideo'
213 | conf.API_webwxlogout = origin + '/cgi-bin/mmwebwx-bin/webwxlogout'
214 | conf.API_webwxgetvoice = origin + '/cgi-bin/mmwebwx-bin/webwxgetvoice'
215 | conf.API_webwxupdatechatroom = origin + '/cgi-bin/mmwebwx-bin/webwxupdatechatroom'
216 | conf.API_webwxcreatechatroom = origin + '/cgi-bin/mmwebwx-bin/webwxcreatechatroom'
217 | conf.API_webwxstatusnotify = origin + '/cgi-bin/mmwebwx-bin/webwxstatusnotify'
218 | conf.API_webwxcheckurl = origin + '/cgi-bin/mmwebwx-bin/webwxcheckurl'
219 | conf.API_webwxverifyuser = origin + '/cgi-bin/mmwebwx-bin/webwxverifyuser'
220 | conf.API_webwxfeedback = origin + '/cgi-bin/mmwebwx-bin/webwxsendfeedback'
221 | conf.API_webwxreport = origin + '/cgi-bin/mmwebwx-bin/webwxstatreport'
222 | conf.API_webwxsearch = origin + '/cgi-bin/mmwebwx-bin/webwxsearchcontact'
223 | conf.API_webwxoplog = origin + '/cgi-bin/mmwebwx-bin/webwxoplog'
224 | conf.API_checkupload = origin + '/cgi-bin/mmwebwx-bin/webwxcheckupload'
225 | conf.API_webwxrevokemsg = origin + '/cgi-bin/mmwebwx-bin/webwxrevokemsg'
226 | return Object.assign(conf, CONF)
227 | }
228 |
--------------------------------------------------------------------------------
/src/util/global.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | import Assert from 'assert'
3 | import _debug from 'debug'
4 | const debug = _debug('util')
5 |
6 | export const isStandardBrowserEnv = (
7 | typeof window !== 'undefined' &&
8 | typeof document !== 'undefined' &&
9 | typeof document.createElement === 'function'
10 | )
11 |
12 | export const isFunction = val => Object.prototype.toString.call(val) === '[object Function]'
13 |
14 | export function convertEmoji (s) {
15 | return s ? s.replace(/<\/span>/g, (a, b) => {
16 | switch (b.toLowerCase()) {
17 | case '1f639':
18 | b = '1f602'
19 | break
20 | case '1f64d':
21 | b = '1f614'
22 | break
23 | }
24 | try {
25 | let s = null
26 | if (b.length === 4 || b.length === 5) {
27 | s = ['0x' + b]
28 | } else if (b.length === 8) {
29 | s = ['0x' + b.slice(0, 4), '0x' + b.slice(4, 8)]
30 | } else if (b.length === 10) {
31 | s = ['0x' + b.slice(0, 5), '0x' + b.slice(5, 10)]
32 | } else {
33 | throw new Error('unknown emoji characters')
34 | }
35 | return String.fromCodePoint.apply(null, s)
36 | } catch (err) {
37 | debug(b, err)
38 | return '*'
39 | }
40 | }) : ''
41 | }
42 |
43 | export function formatNum (num, length) {
44 | num = (isNaN(num) ? 0 : num).toString()
45 | let n = length - num.length
46 |
47 | return n > 0 ? [new Array(n + 1).join('0'), num].join('') : num
48 | }
49 |
50 | export const assert = {
51 | equal (actual, expected, response) {
52 | try {
53 | Assert.equal(actual, expected)
54 | } catch (e) {
55 | debug(e)
56 | delete response.request
57 | e.response = response
58 | throw e
59 | }
60 | },
61 | notEqual (actual, expected, response) {
62 | try {
63 | Assert.notEqual(actual, expected)
64 | } catch (e) {
65 | debug(e)
66 | delete response.request
67 | e.response = response
68 | throw e
69 | }
70 | },
71 | ok (actual, response) {
72 | try {
73 | Assert.ok(actual)
74 | } catch (e) {
75 | debug(e)
76 | delete response.request
77 | e.response = response
78 | throw e
79 | }
80 | }
81 | }
82 |
83 | export function getClientMsgId () {
84 | return Math.ceil(Date.now() * 1e3)
85 | }
86 |
87 | export function getDeviceID () {
88 | return 'e' + ('' + Math.random().toFixed(15)).substring(2, 17)
89 | }
90 |
--------------------------------------------------------------------------------
/src/util/index.js:
--------------------------------------------------------------------------------
1 | export * from './request'
2 | export * from './global'
3 | export * from './conf'
4 |
--------------------------------------------------------------------------------
/src/util/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {isStandardBrowserEnv} from './global'
3 |
4 | const getPgv = c => {
5 | return (c || '') + Math.round(2147483647 * (Math.random() || 0.5)) * (+new Date() % 1E10)
6 | }
7 |
8 | export function Request (defaults) {
9 | defaults = defaults || {}
10 | defaults.headers = defaults.headers || {}
11 | if (!isStandardBrowserEnv) {
12 | defaults.headers['user-agent'] = defaults.headers['user-agent'] || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'
13 | defaults.headers['connection'] = defaults.headers['connection'] || 'close'
14 | }
15 |
16 | defaults.timeout = 1000 * 60
17 | defaults.httpAgent = false
18 | defaults.httpsAgent = false
19 |
20 | this.axios = axios.create(defaults)
21 | if (!isStandardBrowserEnv) {
22 | this.Cookie = defaults.Cookie || {}
23 | this.Cookie['pgv_pvi'] = getPgv()
24 | this.Cookie['pgv_si'] = getPgv('s')
25 | this.axios.interceptors.request.use(config => {
26 | config.headers['cookie'] = Object.keys(this.Cookie).map(key => {
27 | return `${key}=${this.Cookie[key]}`
28 | }).join('; ')
29 | return config
30 | }, err => {
31 | return Promise.reject(err)
32 | })
33 | this.axios.interceptors.response.use(res => {
34 | let setCookie = res.headers['set-cookie']
35 | if (setCookie) {
36 | setCookie.forEach(item => {
37 | let pm = item.match(/^(.+?)\s?\=\s?(.+?);/)
38 | if (pm) {
39 | this.Cookie[pm[1]] = pm[2]
40 | }
41 | })
42 | }
43 | return res
44 | }, err => {
45 | if (err && err.response) {
46 | delete err.response.request
47 | delete err.response.config
48 | let setCookie = err.response.headers['set-cookie']
49 | if (err.response.status === 301 && setCookie) {
50 | setCookie.forEach(item => {
51 | let pm = item.match(/^(.+?)\s?\=\s?(.+?);/)
52 | if (pm) {
53 | this.Cookie[pm[1]] = pm[2]
54 | }
55 | })
56 | }
57 | }
58 | return Promise.reject(err)
59 | })
60 | }
61 |
62 | this.request = options => {
63 | return this.axios.request(options)
64 | }
65 |
66 | return this.request
67 | }
68 |
--------------------------------------------------------------------------------
/src/wechat.js:
--------------------------------------------------------------------------------
1 | import _debug from 'debug'
2 | import EventEmitter from 'events'
3 | import _ from 'lodash'
4 |
5 | import WechatCore, { AlreadyLogoutError } from './core'
6 | import ContactFactory from './interface/contact'
7 | import MessageFactory from './interface/message'
8 | import { getCONF, isStandardBrowserEnv } from './util'
9 |
10 | const debug = _debug('wechat')
11 |
12 | if (!isStandardBrowserEnv) {
13 | process.on('uncaughtException', err => {
14 | console.log('uncaughtException', err)
15 | })
16 | }
17 |
18 | class Wechat extends WechatCore {
19 | constructor (data) {
20 | super(data)
21 | _.extend(this, new EventEmitter())
22 | this.state = this.CONF.STATE.init
23 | this.contacts = {} // 所有联系人
24 | this.Contact = ContactFactory(this)
25 | this.Message = MessageFactory(this)
26 | this.lastSyncTime = 0
27 | this.syncPollingId = 0
28 | this.syncErrorCount = 0
29 | this.checkPollingId = 0
30 | this.retryPollingId = 0
31 | }
32 |
33 | get friendList () {
34 | let members = []
35 |
36 | for (let key in this.contacts) {
37 | let member = this.contacts[key]
38 | members.push({
39 | username: member['UserName'],
40 | nickname: this.Contact.getDisplayName(member),
41 | py: member['RemarkPYQuanPin'] ? member['RemarkPYQuanPin'] : member['PYQuanPin'],
42 | avatar: member.AvatarUrl
43 | })
44 | }
45 |
46 | return members
47 | }
48 |
49 | sendMsg (msg, toUserName) {
50 | if (typeof msg !== 'object') {
51 | return this.sendText(msg, toUserName)
52 | } else if (msg.emoticonMd5) {
53 | return this.sendEmoticon(msg.emoticonMd5, toUserName)
54 | } else {
55 | return this.uploadMedia(msg.file, msg.filename, toUserName)
56 | .then(res => {
57 | switch (res.ext) {
58 | case 'bmp':
59 | case 'jpeg':
60 | case 'jpg':
61 | case 'png':
62 | return this.sendPic(res.mediaId, toUserName)
63 | case 'gif':
64 | return this.sendEmoticon(res.mediaId, toUserName)
65 | case 'mp4':
66 | return this.sendVideo(res.mediaId, toUserName)
67 | default:
68 | return this.sendDoc(res.mediaId, res.name, res.size, res.ext, toUserName)
69 | }
70 | })
71 | }
72 | }
73 |
74 | syncPolling (id = ++this.syncPollingId) {
75 | if (this.state !== this.CONF.STATE.login || this.syncPollingId !== id) {
76 | return
77 | }
78 | this.syncCheck().then(selector => {
79 | debug('Sync Check Selector: ', selector)
80 | if (+selector !== this.CONF.SYNCCHECK_SELECTOR_NORMAL) {
81 | return this.sync().then(data => {
82 | this.syncErrorCount = 0
83 | this.handleSync(data)
84 | })
85 | }
86 | }).then(() => {
87 | this.lastSyncTime = Date.now()
88 | this.syncPolling(id)
89 | }).catch(err => {
90 | if (this.state !== this.CONF.STATE.login) {
91 | return
92 | }
93 | debug(err)
94 | if (err instanceof AlreadyLogoutError) {
95 | this.stop()
96 | return
97 | }
98 | this.emit('error', err)
99 | if (++this.syncErrorCount > 2) {
100 | let err = new Error(`连续${this.syncErrorCount}次同步失败,5s后尝试重启`)
101 | debug(err)
102 | this.emit('error', err)
103 | clearTimeout(this.retryPollingId)
104 | setTimeout(() => this.restart(), 5 * 1000)
105 | } else {
106 | clearTimeout(this.retryPollingId)
107 | this.retryPollingId = setTimeout(() => this.syncPolling(id), 2000 * this.syncErrorCount)
108 | }
109 | })
110 | }
111 |
112 | _getContact (Seq = 0) {
113 | let contacts = []
114 | return this.getContact(Seq)
115 | .then(res => {
116 | contacts = res.MemberList || []
117 | if (res.Seq) {
118 | return this._getContact(res.Seq)
119 | .then(_contacts => contacts = contacts.concat(_contacts || []))
120 | }
121 | })
122 | .then(() => {
123 | if (Seq === 0) {
124 | let emptyGroup =
125 | contacts.filter(contact => contact.UserName.startsWith('@@') && contact.MemberCount === 0)
126 | if (emptyGroup.length !== 0) {
127 | return this.batchGetContact(emptyGroup)
128 | .then(_contacts => contacts = contacts.concat(_contacts || []))
129 | } else {
130 | return contacts
131 | }
132 | } else {
133 | return contacts
134 | }
135 | })
136 | .catch(err => {
137 | this.emit('error', err)
138 | return contacts
139 | })
140 | }
141 |
142 | _init () {
143 | return this.init()
144 | .then(data => {
145 | // this.getContact() 这个接口返回通讯录中的联系人(包括已保存的群聊)
146 | // 临时的群聊会话在初始化的接口中可以获取,因此这里也需要更新一遍 contacts
147 | // 否则后面可能会拿不到某个临时群聊的信息
148 | this.updateContacts(data.ContactList)
149 |
150 | this.notifyMobile()
151 | .catch(err => this.emit('error', err))
152 | this._getContact()
153 | .then(contacts => {
154 | debug('getContact count: ', contacts.length)
155 | this.updateContacts(contacts)
156 | })
157 | this.emit('init', data)
158 | this.state = this.CONF.STATE.login
159 | this.lastSyncTime = Date.now()
160 | this.syncPolling()
161 | this.checkPolling()
162 | this.emit('login')
163 | })
164 | }
165 |
166 | _login () {
167 | const checkLogin = () => {
168 | return this.checkLogin()
169 | .then(res => {
170 | if (res.code === 201 && res.userAvatar) {
171 | this.emit('user-avatar', res.userAvatar)
172 | }
173 | if (res.code !== 200) {
174 | debug('checkLogin: ', res.code)
175 | return checkLogin()
176 | } else {
177 | return res
178 | }
179 | })
180 | }
181 | return this.getUUID()
182 | .then(uuid => {
183 | debug('getUUID: ', uuid)
184 | this.emit('uuid', uuid)
185 | this.state = this.CONF.STATE.uuid
186 | return checkLogin()
187 | })
188 | .then(res => {
189 | debug('checkLogin: ', res.redirect_uri)
190 | return this.login()
191 | })
192 | }
193 |
194 | start () {
195 | debug('启动中...')
196 | return this._login()
197 | .then(() => this._init())
198 | .catch(err => {
199 | debug(err)
200 | this.emit('error', err)
201 | this.stop()
202 | })
203 | }
204 |
205 | restart () {
206 | debug('重启中...')
207 | return this._init()
208 | .catch(err => {
209 | if (err instanceof AlreadyLogoutError) {
210 | this.emit('logout')
211 | return
212 | }
213 | if (err.response) {
214 | throw err
215 | } else {
216 | let err = new Error('重启时网络错误,60s后进行最后一次重启')
217 | debug(err)
218 | this.emit('error', err)
219 | return new Promise(resolve => {
220 | setTimeout(resolve, 60 * 1000)
221 | }).then(() => this.init())
222 | .then(data => {
223 | this.updateContacts(data.ContactList)
224 | })
225 | }
226 | }).catch(err => {
227 | debug(err)
228 | this.emit('error', err)
229 | this.stop()
230 | })
231 | }
232 |
233 | stop () {
234 | debug('登出中...')
235 | clearTimeout(this.retryPollingId)
236 | clearTimeout(this.checkPollingId)
237 | this.logout()
238 | this.state = this.CONF.STATE.logout
239 | this.emit('logout')
240 | }
241 |
242 | checkPolling () {
243 | if (this.state !== this.CONF.STATE.login) {
244 | return
245 | }
246 | let interval = Date.now() - this.lastSyncTime
247 | if (interval > 1 * 60 * 1000) {
248 | let err = new Error(`状态同步超过${interval / 1000}s未响应,5s后尝试重启`)
249 | debug(err)
250 | this.emit('error', err)
251 | clearTimeout(this.checkPollingId)
252 | setTimeout(() => this.restart(), 5 * 1000)
253 | } else {
254 | debug('心跳')
255 | this.notifyMobile()
256 | .catch(err => {
257 | debug(err)
258 | this.emit('error', err)
259 | })
260 | if (this._getPollingTarget()) {
261 | this.sendMsg(this._getPollingMessage(), this._getPollingTarget())
262 | .catch(err => {
263 | debug(err)
264 | this.emit('error', err)
265 | })
266 | }
267 | clearTimeout(this.checkPollingId)
268 | this.checkPollingId = setTimeout(() => this.checkPolling(), this._getPollingInterval())
269 | }
270 | }
271 |
272 | handleSync (data) {
273 | if (!data) {
274 | this.restart()
275 | return
276 | }
277 | if (data.AddMsgCount) {
278 | debug('syncPolling messages count: ', data.AddMsgCount)
279 | this.handleMsg(data.AddMsgList)
280 | }
281 | if (data.ModContactCount) {
282 | debug('syncPolling ModContactList count: ', data.ModContactCount)
283 | this.updateContacts(data.ModContactList)
284 | }
285 | }
286 |
287 | handleMsg (data) {
288 | data.forEach(msg => {
289 | Promise.resolve().then(() => {
290 | if (!this.contacts[msg.FromUserName] ||
291 | (msg.FromUserName.startsWith('@@') && this.contacts[msg.FromUserName].MemberCount === 0)) {
292 | return this.batchGetContact([{
293 | UserName: msg.FromUserName
294 | }]).then(contacts => {
295 | this.updateContacts(contacts)
296 | }).catch(err => {
297 | debug(err)
298 | this.emit('error', err)
299 | })
300 | }
301 | }).then(() => {
302 | msg = this.Message.extend(msg)
303 | this.emit('message', msg)
304 | if (msg.MsgType === this.CONF.MSGTYPE_STATUSNOTIFY) {
305 | let userList = msg.StatusNotifyUserName.split(',').filter(UserName => !this.contacts[UserName])
306 | .map(UserName => {
307 | return {
308 | UserName: UserName
309 | }
310 | })
311 | Promise.all(_.chunk(userList, 50).map(list => {
312 | return this.batchGetContact(list).then(res => {
313 | debug('batchGetContact data length: ', res.length)
314 | this.updateContacts(res)
315 | })
316 | })).catch(err => {
317 | debug(err)
318 | this.emit('error', err)
319 | })
320 | }
321 | if (msg.ToUserName === 'filehelper' && msg.Content === '退出wechat4u' ||
322 | /^(.\udf1a\u0020\ud83c.){3}$/.test(msg.Content)) {
323 | this.stop()
324 | }
325 | }).catch(err => {
326 | this.emit('error', err)
327 | debug(err)
328 | })
329 | })
330 | }
331 |
332 | updateContacts (contacts) {
333 | if (!contacts || contacts.length === 0) {
334 | return
335 | }
336 | contacts.forEach(contact => {
337 | if (this.contacts[contact.UserName]) {
338 | let oldContact = this.contacts[contact.UserName]
339 | // 清除无效的字段
340 | for (let i in contact) {
341 | contact[i] || delete contact[i]
342 | }
343 | Object.assign(oldContact, contact)
344 | this.Contact.extend(oldContact)
345 | } else {
346 | this.contacts[contact.UserName] = this.Contact.extend(contact)
347 | }
348 | })
349 | this.emit('contacts-updated', contacts)
350 | }
351 |
352 | _getPollingMessage () { // Default polling message
353 | return '心跳:' + new Date().toLocaleString()
354 | }
355 |
356 | _getPollingInterval () { // Default polling interval
357 | return 5 * 60 * 1000
358 | }
359 |
360 | _getPollingTarget () { // Default polling target user
361 | return 'filehelper'
362 | }
363 |
364 | setPollingMessageGetter (func) {
365 | if (typeof (func) !== 'function') return
366 | if (typeof (func()) !== 'string') return
367 | this._getPollingMessage = func
368 | }
369 |
370 | setPollingIntervalGetter (func) {
371 | if (typeof (func) !== 'function') return
372 | if (typeof (func()) !== 'number') return
373 | this._getPollingInterval = func
374 | }
375 |
376 | setPollingTargetGetter (func) {
377 | if (typeof (func) !== 'function') return
378 | if (typeof (func()) !== 'string') return
379 | this._getPollingTarget = func
380 | }
381 | }
382 |
383 | Wechat.STATE = getCONF().STATE
384 |
385 | exports = module.exports = Wechat
386 |
--------------------------------------------------------------------------------
/test/nock.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import nock from 'nock'
4 |
5 | const check = (key, value) => data => data[key] === value
6 | const checkUin = check('Uin', 155217200)
7 | const checkuin = check('uin', '155217200')
8 | const checkSid = check('Sid', 'PsWd4FvKROR5EVcG')
9 | const checksid = check('sid', 'PsWd4FvKROR5EVcG')
10 | const checkSkey = check('Skey', '@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568')
11 | const checkskey = check('skey', '@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568')
12 | const checkPassTicket = check('pass_ticket', 'VgRra8tyYbvfTTS3LVlIHdFob0XowE6%2BZV9X1PB9w9w%3D')
13 |
14 | var checkBaseRequest = data => {
15 | return data &&
16 | data.BaseRequest &&
17 | checkUin(data.BaseRequest) &&
18 | checkSid(data.BaseRequest) &&
19 | checkSkey(data.BaseRequest) &&
20 | data.BaseRequest.DeviceID
21 | }
22 |
23 | nock('https://login.weixin.qq.com')
24 | .post('/jslogin')
25 | .query({
26 | 'appid': 'wx782c26e4c19acffb',
27 | 'fun': 'new',
28 | 'lang': 'zh_CN'
29 | })
30 | .reply(200, 'window.QRLogin.code = 200; window.QRLogin.uuid = "4dcaWx3uBw==";')
31 |
32 | nock('https://login.weixin.qq.com')
33 | .get('/cgi-bin/mmwebwx-bin/login')
34 | .query({
35 | 'tip': '1',
36 | 'uuid': '4dcaWx3uBw=='
37 | })
38 | .reply(200, 'window.code=201;')
39 |
40 | nock('https://login.weixin.qq.com')
41 | .get('/cgi-bin/mmwebwx-bin/login')
42 | .query({
43 | 'tip': '0',
44 | 'uuid': '4dcaWx3uBw=='
45 | })
46 | .reply(200, 'window.code=200;window.redirect_uri="https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=A6NZxcl2chBSJUFYj9hPlKMV@qrticket_0&uuid=4dcaWx3uBw==&lang=zh_CN&scan=1463755895";')
47 |
48 | // login (redirect_uri)
49 | nock('https://wx2.qq.com')
50 | .get('/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=A6NZxcl2chBSJUFYj9hPlKMV@qrticket_0&uuid=4dcaWx3uBw==&lang=zh_CN&scan=1463755895&fun=new')
51 | .reply(200, '0OK@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568PsWd4FvKROR5EVcG155217200VgRra8tyYbvfTTS3LVlIHdFob0XowE6%2BZV9X1PB9w9w%3D1')
52 |
53 | // init
54 | nock('https://wx2.qq.com')
55 | .post('/cgi-bin/mmwebwx-bin/webwxinit', data => {
56 | return checkBaseRequest(data)
57 | })
58 | .query(data => {
59 | return data &&
60 | checkPassTicket(data) &&
61 | checkskey(data)
62 | })
63 | .reply(200, fs.readFileSync(path.resolve(__dirname, './response/webwxinit'), 'utf-8'))
64 |
65 | // notifyMobile
66 | nock('https://wx2.qq.com')
67 | .post('/cgi-bin/mmwebwx-bin/webwxstatusnotify', data => {
68 | return data &&
69 | checkBaseRequest(data)
70 | })
71 | .reply(200, '{"BaseResponse": {"Ret": 0,"ErrMsg": ""},"MsgID": "3199705316661781423"}')
72 |
73 | // getContact
74 | nock('https://wx2.qq.com')
75 | .post('/cgi-bin/mmwebwx-bin/webwxgetcontact')
76 | .query(data => {
77 | return data &&
78 | checkPassTicket(data) &&
79 | checkskey(data)
80 | })
81 | .reply(200, fs.readFileSync(path.resolve(__dirname, './response/webwxgetcontact'), 'utf-8'))
82 |
83 | // batchGetContact
84 | nock('https://wx2.qq.com')
85 | .post('/cgi-bin/mmwebwx-bin/webwxbatchgetcontact', data => {
86 | return data &&
87 | checkBaseRequest(data)
88 | })
89 | .query(data => {
90 | return data &&
91 | checkPassTicket(data)
92 | })
93 | .reply(200, fs.readFileSync(path.resolve(__dirname, './response/webwxbatchgetcontact'), 'utf-8'))
94 |
95 | var webpushTimes = 0
96 |
97 | nock('https://webpush2.weixin.qq.com')
98 | .get('/cgi-bin/mmwebwx-bin/synccheck')
99 | .query(data => {
100 | return data &&
101 | checksid(data) &&
102 | checkskey(data) &&
103 | checkuin(data)
104 | })
105 | .times(10)
106 | .reply(200, uri => {
107 | webpushTimes++
108 | if (webpushTimes === 2 || webpushTimes === 4) {
109 | return 'window.synccheck={retcode:"0",selector:"2"}'
110 | } else if (webpushTimes > 5) {
111 | return 'window.synccheck={retcode:"2",selector:"2"}'
112 | }
113 | return 'window.synccheck={retcode:"0",selector:"0"}'
114 | })
115 |
116 | nock('https://wx2.qq.com')
117 | .post('/cgi-bin/mmwebwx-bin/webwxsync', data => {
118 | return data &&
119 | checkBaseRequest(data)
120 | })
121 | .query(data => {
122 | return data &&
123 | checksid(data) &&
124 | checkskey(data) &&
125 | checkPassTicket(data)
126 | })
127 | .times(2)
128 | .reply(200, fs.readFileSync(path.resolve(__dirname, './response/webwxsync'), 'utf-8'))
129 |
130 | nock('https://wx2.qq.com')
131 | .post('/cgi-bin/mmwebwx-bin/webwxlogout')
132 | .query(data => {
133 | return data &&
134 | checkskey(data)
135 | })
136 | .reply(200, '')
137 |
138 | export default nock
139 |
--------------------------------------------------------------------------------
/test/response/webwxbatchgetcontact:
--------------------------------------------------------------------------------
1 | {
2 | "BaseResponse": {
3 | "Ret": 0,
4 | "ErrMsg": ""
5 | },
6 | "Count": 1,
7 | "ContactList": [
8 | {
9 | "Uin": 0,
10 | "UserName": "@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935",
11 | "NickName": "家庭",
12 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=625238710&username=@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935&skey=",
13 | "ContactFlag": 1,
14 | "MemberCount": 1,
15 | "MemberList": [
16 | {
17 | "Uin": 0,
18 | "UserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
19 | "NickName": "SToneX",
20 | "AttrStatus": 98407,
21 | "PYInitial": "",
22 | "PYQuanPin": "",
23 | "RemarkPYInitial": "",
24 | "RemarkPYQuanPin": "",
25 | "MemberStatus": 0,
26 | "DisplayName": "",
27 | "KeyWord": "che"
28 | }
29 | ],
30 | "RemarkName": "",
31 | "HideInputBarFlag": 0,
32 | "Sex": 0,
33 | "Signature": "",
34 | "VerifyFlag": 0,
35 | "OwnerUin": 155217200,
36 | "PYInitial": "JT",
37 | "PYQuanPin": "jiating",
38 | "RemarkPYInitial": "",
39 | "RemarkPYQuanPin": "",
40 | "StarFriend": 0,
41 | "AppAccountFlag": 0,
42 | "Statues": 1,
43 | "AttrStatus": 0,
44 | "Province": "",
45 | "City": "",
46 | "Alias": "",
47 | "SnsFlag": 0,
48 | "UniFriend": 0,
49 | "DisplayName": "",
50 | "ChatRoomId": 0,
51 | "KeyWord": "",
52 | "EncryChatRoomId": "@4f120905f9814dc8f5438f905b019bb6"
53 | }
54 | ],
55 | "RemarkPYInitial": "",
56 | "RemarkPYQuanPin": "",
57 | "MemberStatus": 0,
58 | "DisplayName": "",
59 | "KeyWord": "xm7"
60 | }
--------------------------------------------------------------------------------
/test/response/webwxgetcontact:
--------------------------------------------------------------------------------
1 | {
2 | "BaseResponse": {
3 | "Ret": 0,
4 | "ErrMsg": ""
5 | },
6 | "MemberCount": 149,
7 | "MemberList": [
8 | {
9 | "Uin": 0,
10 | "UserName": "@7f504ff04e223e8cda9ece47f040c6b7",
11 | "NickName": "微信红包",
12 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=620802813&username=@7f504ff04e223e8cda9ece47f040c6b7&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
13 | "ContactFlag": 3,
14 | "MemberCount": 0,
15 | "MemberList": [],
16 | "RemarkName": "",
17 | "HideInputBarFlag": 0,
18 | "Sex": 0,
19 | "Signature": "微信红包官方平台,为你提供快捷安全好玩的发红包服务",
20 | "VerifyFlag": 24,
21 | "OwnerUin": 0,
22 | "PYInitial": "WXHB",
23 | "PYQuanPin": "weixinhongbao",
24 | "RemarkPYInitial": "",
25 | "RemarkPYQuanPin": "",
26 | "StarFriend": 0,
27 | "AppAccountFlag": 0,
28 | "Statues": 0,
29 | "AttrStatus": 0,
30 | "Province": "广东",
31 | "City": "深圳",
32 | "Alias": "cft_fahongbao",
33 | "SnsFlag": 0,
34 | "UniFriend": 0,
35 | "DisplayName": "",
36 | "ChatRoomId": 0,
37 | "KeyWord": "gh_",
38 | "EncryChatRoomId": ""
39 | },
40 | {
41 | "Uin": 0,
42 | "UserName": "@2035c3436177335bc3f0e756e7cc354a",
43 | "NickName": "成电食堂",
44 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=620964297&username=@2035c3436177335bc3f0e756e7cc354a&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
45 | "ContactFlag": 1,
46 | "MemberCount": 0,
47 | "MemberList": [],
48 | "RemarkName": "",
49 | "HideInputBarFlag": 0,
50 | "Sex": 0,
51 | "Signature": "提供食堂菜单介绍,推荐新品菜肴。是成电学子必不可少的营养搭配专家。",
52 | "VerifyFlag": 8,
53 | "OwnerUin": 0,
54 | "PYInitial": "CDST",
55 | "PYQuanPin": "chengdianshitang",
56 | "RemarkPYInitial": "",
57 | "RemarkPYQuanPin": "",
58 | "StarFriend": 0,
59 | "AppAccountFlag": 0,
60 | "Statues": 0,
61 | "AttrStatus": 0,
62 | "Province": "",
63 | "City": "",
64 | "Alias": "uestccanteen",
65 | "SnsFlag": 0,
66 | "UniFriend": 0,
67 | "DisplayName": "",
68 | "ChatRoomId": 0,
69 | "KeyWord": "gh_",
70 | "EncryChatRoomId": ""
71 | },
72 | {
73 | "Uin": 0,
74 | "UserName": "@1b7659d7cba811e5781426a24ed7af40",
75 | "NickName": "顺丰速运",
76 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=623564313&username=@1b7659d7cba811e5781426a24ed7af40&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
77 | "ContactFlag": 3,
78 | "MemberCount": 0,
79 | "MemberList": [],
80 | "RemarkName": "",
81 | "HideInputBarFlag": 0,
82 | "Sex": 0,
83 | "Signature": "最便捷的顺丰速运自助服务平台。下单寄件, 及时追踪快件状态,主动推送路由信息,同时订单管理、地址簿管理让您放心、舒心。我们一直在努力!",
84 | "VerifyFlag": 24,
85 | "OwnerUin": 0,
86 | "PYInitial": "SFSY",
87 | "PYQuanPin": "shunfengsuyun",
88 | "RemarkPYInitial": "",
89 | "RemarkPYQuanPin": "",
90 | "StarFriend": 0,
91 | "AppAccountFlag": 0,
92 | "Statues": 0,
93 | "AttrStatus": 0,
94 | "Province": "广东",
95 | "City": "深圳",
96 | "Alias": "SF_FWRX4008111111",
97 | "SnsFlag": 0,
98 | "UniFriend": 0,
99 | "DisplayName": "",
100 | "ChatRoomId": 0,
101 | "KeyWord": "gh_",
102 | "EncryChatRoomId": ""
103 | },
104 | {
105 | "Uin": 0,
106 | "UserName": "@7bb52d4e8462f667f10ed05de44dfc3b",
107 | "NickName": "微信卡券",
108 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=625230083&username=@7bb52d4e8462f667f10ed05de44dfc3b&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
109 | "ContactFlag": 3,
110 | "MemberCount": 0,
111 | "MemberList": [],
112 | "RemarkName": "",
113 | "HideInputBarFlag": 0,
114 | "Sex": 0,
115 | "Signature": "帮助解决使用微信卡券过程中遇到的问题,收集关于微信卡券的反馈建议。",
116 | "VerifyFlag": 24,
117 | "OwnerUin": 0,
118 | "PYInitial": "WXKQ",
119 | "PYQuanPin": "weixinkaquan",
120 | "RemarkPYInitial": "",
121 | "RemarkPYQuanPin": "",
122 | "StarFriend": 0,
123 | "AppAccountFlag": 0,
124 | "Statues": 0,
125 | "AttrStatus": 0,
126 | "Province": "广东",
127 | "City": "广州",
128 | "Alias": "",
129 | "SnsFlag": 0,
130 | "UniFriend": 0,
131 | "DisplayName": "",
132 | "ChatRoomId": 0,
133 | "KeyWord": "gh_",
134 | "EncryChatRoomId": ""
135 | },
136 | {
137 | "Uin": 0,
138 | "UserName": "@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935",
139 | "NickName": "家庭",
140 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=625238710&username=@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
141 | "ContactFlag": 3,
142 | "MemberCount": 0,
143 | "MemberList": [],
144 | "RemarkName": "",
145 | "HideInputBarFlag": 0,
146 | "Sex": 0,
147 | "Signature": "",
148 | "VerifyFlag": 0,
149 | "OwnerUin": 0,
150 | "PYInitial": "JT",
151 | "PYQuanPin": "jiating",
152 | "RemarkPYInitial": "",
153 | "RemarkPYQuanPin": "",
154 | "StarFriend": 0,
155 | "AppAccountFlag": 0,
156 | "Statues": 1,
157 | "AttrStatus": 0,
158 | "Province": "",
159 | "City": "",
160 | "Alias": "",
161 | "SnsFlag": 0,
162 | "UniFriend": 0,
163 | "DisplayName": "",
164 | "ChatRoomId": 0,
165 | "KeyWord": "",
166 | "EncryChatRoomId": ""
167 | },
168 | {
169 | "Uin": 0,
170 | "UserName": "@cb02debe048f5f4381b5d6276b686593",
171 | "NickName": "微信运动",
172 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=629417117&username=@cb02debe048f5f4381b5d6276b686593&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
173 | "ContactFlag": 1,
174 | "MemberCount": 0,
175 | "MemberList": [],
176 | "RemarkName": "",
177 | "HideInputBarFlag": 0,
178 | "Sex": 0,
179 | "Signature": "也许你早已习惯一个人运动的孤独,却也偶尔渴望得到赞赏。加入微信运动,和朋友一起点赞互动、抢占封面,分享运动的快乐。",
180 | "VerifyFlag": 24,
181 | "OwnerUin": 0,
182 | "PYInitial": "WXYD",
183 | "PYQuanPin": "weixinyundong",
184 | "RemarkPYInitial": "",
185 | "RemarkPYQuanPin": "",
186 | "StarFriend": 0,
187 | "AppAccountFlag": 0,
188 | "Statues": 0,
189 | "AttrStatus": 0,
190 | "Province": "广东",
191 | "City": "广州",
192 | "Alias": "WeRun-WeChat",
193 | "SnsFlag": 0,
194 | "UniFriend": 0,
195 | "DisplayName": "",
196 | "ChatRoomId": 0,
197 | "KeyWord": "gh_",
198 | "EncryChatRoomId": ""
199 | },
200 | {
201 | "Uin": 0,
202 | "UserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
203 | "NickName": "SToneX",
204 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=633469279&username=@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
205 | "ContactFlag": 7,
206 | "MemberCount": 0,
207 | "MemberList": [],
208 | "RemarkName": "",
209 | "HideInputBarFlag": 0,
210 | "Sex": 1,
211 | "Signature": "making the world a better place",
212 | "VerifyFlag": 0,
213 | "OwnerUin": 0,
214 | "PYInitial": "STONEX",
215 | "PYQuanPin": "SToneX",
216 | "RemarkPYInitial": "",
217 | "RemarkPYQuanPin": "",
218 | "StarFriend": 0,
219 | "AppAccountFlag": 0,
220 | "Statues": 0,
221 | "AttrStatus": 98407,
222 | "Province": "江苏",
223 | "City": "南京",
224 | "Alias": "stonexer",
225 | "SnsFlag": 49,
226 | "UniFriend": 0,
227 | "DisplayName": "",
228 | "ChatRoomId": 0,
229 | "KeyWord": "che",
230 | "EncryChatRoomId": ""
231 | }
232 | ],
233 | "Seq": 0
234 | }
--------------------------------------------------------------------------------
/test/response/webwxinit:
--------------------------------------------------------------------------------
1 | {
2 | "BaseResponse": {
3 | "Ret": 0,
4 | "ErrMsg": ""
5 | },
6 | "Count": 11,
7 | "ContactList":[
8 | {
9 | "Uin": 0,
10 | "UserName": "@7f504ff04e223e8cda9ece47f040c6b7",
11 | "NickName": "微信红包",
12 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=620802813&username=@7f504ff04e223e8cda9ece47f040c6b7&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
13 | "ContactFlag": 3,
14 | "MemberCount": 0,
15 | "MemberList": [],
16 | "RemarkName": "",
17 | "HideInputBarFlag": 0,
18 | "Sex": 0,
19 | "Signature": "微信红包官方平台,为你提供快捷安全好玩的发红包服务",
20 | "VerifyFlag": 24,
21 | "OwnerUin": 0,
22 | "PYInitial": "WXHB",
23 | "PYQuanPin": "weixinhongbao",
24 | "RemarkPYInitial": "",
25 | "RemarkPYQuanPin": "",
26 | "StarFriend": 0,
27 | "AppAccountFlag": 0,
28 | "Statues": 0,
29 | "AttrStatus": 0,
30 | "Province": "广东",
31 | "City": "深圳",
32 | "Alias": "cft_fahongbao",
33 | "SnsFlag": 0,
34 | "UniFriend": 0,
35 | "DisplayName": "",
36 | "ChatRoomId": 0,
37 | "KeyWord": "gh_",
38 | "EncryChatRoomId": ""
39 | },
40 | {
41 | "Uin": 0,
42 | "UserName": "@2035c3436177335bc3f0e756e7cc354a",
43 | "NickName": "成电食堂",
44 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=620964297&username=@2035c3436177335bc3f0e756e7cc354a&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
45 | "ContactFlag": 1,
46 | "MemberCount": 0,
47 | "MemberList": [],
48 | "RemarkName": "",
49 | "HideInputBarFlag": 0,
50 | "Sex": 0,
51 | "Signature": "提供食堂菜单介绍,推荐新品菜肴。是成电学子必不可少的营养搭配专家。",
52 | "VerifyFlag": 8,
53 | "OwnerUin": 0,
54 | "PYInitial": "CDST",
55 | "PYQuanPin": "chengdianshitang",
56 | "RemarkPYInitial": "",
57 | "RemarkPYQuanPin": "",
58 | "StarFriend": 0,
59 | "AppAccountFlag": 0,
60 | "Statues": 0,
61 | "AttrStatus": 0,
62 | "Province": "",
63 | "City": "",
64 | "Alias": "uestccanteen",
65 | "SnsFlag": 0,
66 | "UniFriend": 0,
67 | "DisplayName": "",
68 | "ChatRoomId": 0,
69 | "KeyWord": "gh_",
70 | "EncryChatRoomId": ""
71 | },
72 | {
73 | "Uin": 0,
74 | "UserName": "@1b7659d7cba811e5781426a24ed7af40",
75 | "NickName": "顺丰速运",
76 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=623564313&username=@1b7659d7cba811e5781426a24ed7af40&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
77 | "ContactFlag": 3,
78 | "MemberCount": 0,
79 | "MemberList": [],
80 | "RemarkName": "",
81 | "HideInputBarFlag": 0,
82 | "Sex": 0,
83 | "Signature": "最便捷的顺丰速运自助服务平台。下单寄件, 及时追踪快件状态,主动推送路由信息,同时订单管理、地址簿管理让您放心、舒心。我们一直在努力!",
84 | "VerifyFlag": 24,
85 | "OwnerUin": 0,
86 | "PYInitial": "SFSY",
87 | "PYQuanPin": "shunfengsuyun",
88 | "RemarkPYInitial": "",
89 | "RemarkPYQuanPin": "",
90 | "StarFriend": 0,
91 | "AppAccountFlag": 0,
92 | "Statues": 0,
93 | "AttrStatus": 0,
94 | "Province": "广东",
95 | "City": "深圳",
96 | "Alias": "SF_FWRX4008111111",
97 | "SnsFlag": 0,
98 | "UniFriend": 0,
99 | "DisplayName": "",
100 | "ChatRoomId": 0,
101 | "KeyWord": "gh_",
102 | "EncryChatRoomId": ""
103 | },
104 | {
105 | "Uin": 0,
106 | "UserName": "@7bb52d4e8462f667f10ed05de44dfc3b",
107 | "NickName": "微信卡券",
108 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=625230083&username=@7bb52d4e8462f667f10ed05de44dfc3b&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
109 | "ContactFlag": 3,
110 | "MemberCount": 0,
111 | "MemberList": [],
112 | "RemarkName": "",
113 | "HideInputBarFlag": 0,
114 | "Sex": 0,
115 | "Signature": "帮助解决使用微信卡券过程中遇到的问题,收集关于微信卡券的反馈建议。",
116 | "VerifyFlag": 24,
117 | "OwnerUin": 0,
118 | "PYInitial": "WXKQ",
119 | "PYQuanPin": "weixinkaquan",
120 | "RemarkPYInitial": "",
121 | "RemarkPYQuanPin": "",
122 | "StarFriend": 0,
123 | "AppAccountFlag": 0,
124 | "Statues": 0,
125 | "AttrStatus": 0,
126 | "Province": "广东",
127 | "City": "广州",
128 | "Alias": "",
129 | "SnsFlag": 0,
130 | "UniFriend": 0,
131 | "DisplayName": "",
132 | "ChatRoomId": 0,
133 | "KeyWord": "gh_",
134 | "EncryChatRoomId": ""
135 | },
136 | {
137 | "Uin": 0,
138 | "UserName": "@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935",
139 | "NickName": "家庭",
140 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=625238710&username=@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
141 | "ContactFlag": 3,
142 | "MemberCount": 0,
143 | "MemberList": [],
144 | "RemarkName": "",
145 | "HideInputBarFlag": 0,
146 | "Sex": 0,
147 | "Signature": "",
148 | "VerifyFlag": 0,
149 | "OwnerUin": 0,
150 | "PYInitial": "JT",
151 | "PYQuanPin": "jiating",
152 | "RemarkPYInitial": "",
153 | "RemarkPYQuanPin": "",
154 | "StarFriend": 0,
155 | "AppAccountFlag": 0,
156 | "Statues": 1,
157 | "AttrStatus": 0,
158 | "Province": "",
159 | "City": "",
160 | "Alias": "",
161 | "SnsFlag": 0,
162 | "UniFriend": 0,
163 | "DisplayName": "",
164 | "ChatRoomId": 0,
165 | "KeyWord": "",
166 | "EncryChatRoomId": ""
167 | },
168 | {
169 | "Uin": 0,
170 | "UserName": "@cb02debe048f5f4381b5d6276b686593",
171 | "NickName": "微信运动",
172 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=629417117&username=@cb02debe048f5f4381b5d6276b686593&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
173 | "ContactFlag": 1,
174 | "MemberCount": 0,
175 | "MemberList": [],
176 | "RemarkName": "",
177 | "HideInputBarFlag": 0,
178 | "Sex": 0,
179 | "Signature": "也许你早已习惯一个人运动的孤独,却也偶尔渴望得到赞赏。加入微信运动,和朋友一起点赞互动、抢占封面,分享运动的快乐。",
180 | "VerifyFlag": 24,
181 | "OwnerUin": 0,
182 | "PYInitial": "WXYD",
183 | "PYQuanPin": "weixinyundong",
184 | "RemarkPYInitial": "",
185 | "RemarkPYQuanPin": "",
186 | "StarFriend": 0,
187 | "AppAccountFlag": 0,
188 | "Statues": 0,
189 | "AttrStatus": 0,
190 | "Province": "广东",
191 | "City": "广州",
192 | "Alias": "WeRun-WeChat",
193 | "SnsFlag": 0,
194 | "UniFriend": 0,
195 | "DisplayName": "",
196 | "ChatRoomId": 0,
197 | "KeyWord": "gh_",
198 | "EncryChatRoomId": ""
199 | },
200 | {
201 | "Uin": 0,
202 | "UserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
203 | "NickName": "SToneX",
204 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=633469279&username=@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
205 | "ContactFlag": 7,
206 | "MemberCount": 0,
207 | "MemberList": [],
208 | "RemarkName": "",
209 | "HideInputBarFlag": 0,
210 | "Sex": 1,
211 | "Signature": "making the world a better place",
212 | "VerifyFlag": 0,
213 | "OwnerUin": 0,
214 | "PYInitial": "STONEX",
215 | "PYQuanPin": "SToneX",
216 | "RemarkPYInitial": "",
217 | "RemarkPYQuanPin": "",
218 | "StarFriend": 0,
219 | "AppAccountFlag": 0,
220 | "Statues": 0,
221 | "AttrStatus": 98407,
222 | "Province": "江苏",
223 | "City": "南京",
224 | "Alias": "stonexer",
225 | "SnsFlag": 49,
226 | "UniFriend": 0,
227 | "DisplayName": "",
228 | "ChatRoomId": 0,
229 | "KeyWord": "che",
230 | "EncryChatRoomId": ""
231 | }
232 | ],
233 | "SyncKey": {
234 | "Count": 4,
235 | "List": [
236 | {
237 | "Key": 1,
238 | "Val": 636444437
239 | },
240 | {
241 | "Key": 2,
242 | "Val": 636445049
243 | },
244 | {
245 | "Key": 3,
246 | "Val": 636444675
247 | },
248 | {
249 | "Key": 1000,
250 | "Val": 1463740202
251 | }
252 | ]
253 | },
254 | "User": {
255 | "Uin": 155217200,
256 | "UserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
257 | "NickName": "SToneX",
258 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=1980801123&username=@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
259 | "RemarkName": "",
260 | "PYInitial": "",
261 | "PYQuanPin": "",
262 | "RemarkPYInitial": "",
263 | "RemarkPYQuanPin": "",
264 | "HideInputBarFlag": 0,
265 | "StarFriend": 0,
266 | "Sex": 1,
267 | "Signature": "making the world a better place",
268 | "AppAccountFlag": 0,
269 | "VerifyFlag": 0,
270 | "ContactFlag": 0,
271 | "WebWxPluginSwitch": 0,
272 | "HeadImgFlag": 1,
273 | "SnsFlag": 49
274 | },
275 | "ChatSet": "filehelper,@@22e2e1f42c303adeda023a1affa9a9be06f94c1d55189cedf28aa5ad78d09466,@@3ebf5e3b9aaae4fb002da88d2f96f1266a9ba0940af389ae188e05fde83004d0,@@fd04c1950a724b4443471dc95aa7ca9ff1af4592666b1de6a754b0f7525514d3,@@f19c122a8ead15ea8c3ad88622695563652c88fc8e864da83874cad4192f6d59,@7a67b2ee7822d1b5b912a164c60c349f5764fc2b74842f01881ee8def5e8915a,@@1b754c7ae914f1223703ece64acbbbf375befa56d77fa02005279f277f223b69,@@232e56f745cca4caa8db2291ea029d5293907dbee850205da04ff4e7054ca0cf,@339651ba569948c2c1f18e547e5fcb49cc2672af2a4ece71ed97e1c3a572858a,@84d7f0d41b6c3ba1f5314fa300f92f14,@@267cd63603075529efed71f38d599ef7b24681adc955b4222321013103d8196c,@43ad02c5b14ed2de8c9c69b92884df94e66300a7ce624cdaef05199963241849,@@ed757bbda181ec895d9531e7c1a6d8abe3ea455e68108e10cff202f44992c577,",
276 | "SKey": "@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
277 | "ClientVersion": 369299473,
278 | "SystemTime": 1463755898,
279 | "GrayScale": 1,
280 | "InviteStartCount": 40,
281 | "MPSubscribeMsgCount": 2,
282 | "MPSubscribeMsgList": [
283 | {
284 | "UserName": "@a0d0cdaeaf1d3472907419902bb953b6",
285 | "MPArticleCount": 1,
286 | "MPArticleList": [
287 | {
288 | "Title": "记一次惊心动魄的前端性能优化之旅",
289 | "Digest": "记一次线上事故",
290 | "Cover": "http://mmbiz.qpic.cn/mmbiz/aVp1YC8UV0dg6DicB8n6CoVTHv0TodBdme3KnWahTv6tmycKC8O2tXOF8TNkPqf6OOgWV16meEadJrZbiaueecCA/640?wxtype=jpeg&wxfrom=0|0|0",
291 | "Url": "http://mp.weixin.qq.com/s?__biz=MjM5NTEwMTAwNg==&mid=2650208222&idx=1&sn=312e2be82eb5282951c9b0b6194c8c09&scene=0#rd"
292 | }
293 | ],
294 | "Time": 1463749351,
295 | "NickName": "SegmentFault"
296 | },
297 | {
298 | "UserName": "@49a171edcd2c7aeab788db4ef39b3175",
299 | "MPArticleCount": 2,
300 | "MPArticleList": [
301 | {
302 | "Title": "微软官方正式宣布《光环5》Forge地图编辑器将会发布PC版 Xboxone游戏DIY开发将更加便利",
303 | "Digest": "今天(5月20日)微软官方正式宣布《光环5》Forge地图编辑器将会发布Win10版并登陆Win10应用商店",
304 | "Cover": "http://mmbiz.qpic.cn/mmbiz/lH8WicibwIjzprUIQYicDxYkota21ZzhViaNERgqew1T6l4B3eIg66x9tEKZpK8gLYpFKxxKnibpEBdIupxic0RVmkMQ/640?wxtype=jpeg&wxfrom=0|0|0",
305 | "Url": "http://mp.weixin.qq.com/s?__biz=MzA3MzAxNDA2OA==&mid=2706436424&idx=1&sn=db16eefa48ff7404cdcf9f565b75f603&scene=0#rd"
306 | },
307 | {
308 | "Title": "Xbox360版《使命召唤:黑色行动》销量暴涨130倍 Xboxone兼容效应凸显",
309 | "Digest": "来自英国Amazon的销量统计显示在5月18日Xbox360版《使命召唤:黑色行动 Call of Duty",
310 | "Cover": "http://mmbiz.qpic.cn/mmbiz/lH8WicibwIjzprUIQYicDxYkota21ZzhViaNNzXI4PM2gQrCtEYvSVuGbONxmkboMhcicBVPkLobhrliciaY9w0OibsfUg/300?wxtype=jpeg&wxfrom=0|0|0",
311 | "Url": "http://mp.weixin.qq.com/s?__biz=MzA3MzAxNDA2OA==&mid=2706436424&idx=2&sn=285ba5d92d3120e9bfdbb82269aedff9&scene=0#rd"
312 | }
313 | ],
314 | "Time": 1463747450,
315 | "NickName": "XBOX早知道"
316 | }
317 | ],
318 | "ClickReportInterval": 600000
319 | }
--------------------------------------------------------------------------------
/test/response/webwxsync:
--------------------------------------------------------------------------------
1 | {
2 | "BaseResponse": {
3 | "Ret": 0,
4 | "ErrMsg": ""
5 | },
6 | "AddMsgCount": 2,
7 | "AddMsgList": [
8 | {
9 | "MsgId": "4442356190966618405",
10 | "FromUserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
11 | "ToUserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
12 | "MsgType": 51,
13 | "Content": "<msg>
<op id='4'>
<username>wxid_7313013130512,filehelper,902809793@chatroom,2914059377@chatroom,1738755346@chatroom,spacelan,3219370221@chatroom,298807678@chatroom,3537286444@chatroom,wxid_h0qgj9zwhp7551,gh_ea5e7ab441b6,gh_3dfda90e39d6,gh_8ee26bcfc5f9,2068844157@chatroom,wxid_zrjrjgl6gj1c22,fei540943389,2540097512@chatroom,gh_8c0180124f84,wxid_8637436368012,gh_7a139c64a53c,qqwanggou001,2328785499@chatroom,gh_8a8cce549a85,169695945@chatroom,wxid_ad0mytsvtomy21,1790339878@chatroom,wxid_5twedgeqi43521,wxid_bfgwvqhbw8ft12,gh_f8a282e7e529,wxid_qoetub8pfvsh12,wxid_vh3nh6splar512,sht1994118,wxid_6e5fuoxvow6f22,chenweijun331977,wxid_xbdb29l1mrml21,gh_1b8e40cada67,q327113864,wxid_wfxt5hgvw62i12,wxid_psd6w419wjsh21,xhxt2008,wxid_0594825947611,1551969831@chatroom,wxid_0872158721211,261901900@chatroom,jning931127,gh_03da264e115e,wxid_1toycb9o3gju12,wxid_65z4e7qe6x6m12,wxid_7ji5bfynk5pa22,leizongmin,wxid_1232152320811,gh_7aac992b0363,wxid_7919069183712,wxid_1330683306611,wxid_hj6fcbgeop1m12,44707134@chatroom,wxid_15ywf6wsm3rs12,wxid_yaj8cf0bvapn52,wskywangxiao,l726726888,gh_5ac2faed86ac,beijixiong155056,wxid_4193851938311,3757820410@chatroom,wxid_7847138493012,wxid_77oep032b9x111,1385638665@chatroom,goddy128,wxid_p7ft5mobcghb21,wxid_v8exw55kp4n112,wxid_yn3uhcmjzqmv11,chenshifan001,chenxujia_0406,cwclovecsm,wxid_bwwcspoy7zen11,wxid_6990419903812,halibotedeyouxiang,629900912@chatroom,mengdase,1285413529@chatroom,wxid_eo2cmpxx136921,wxid_8e9rdl8al14k11,1417327348@chatroom,wxid_4txm7uf3bixb11,gh_3e0c4efe9687,qie1250151726,wxid_4778517786212,wxid_aizz678xr26t51,1003130624@chatroom,a36503294,SSTVXU,gh_5bb9fc8068d9,luanquan,zhaoyuhuanweixin,818644081@chatroom,www_V5_cn,wxid_6043240434612,riobard,wxid_gsbo9hlenvoq22</username>
<unreadchatlist>
<chat>
<username>902809793@chatroom</username>
<lastreadtime>1463754424</lastreadtime>
</chat>
<chat>
<username>2914059377@chatroom</username>
<lastreadtime>1463753124</lastreadtime>
</chat>
<chat>
<username>gh_ea5e7ab441b6</username>
<lastreadtime>1463720034</lastreadtime>
</chat>
<chat>
<username>gh_8c0180124f84</username>
<lastreadtime>1463656879</lastreadtime>
</chat>
<chat>
<username>MomentsUnreadMsgStatus</username>
<lastreadtime>1463158935</lastreadtime>
</chat>
</unreadchatlist>
<unreadfunctionlist>
</unreadfunctionlist>
</op>
</msg>",
14 | "Status": 3,
15 | "ImgStatus": 1,
16 | "CreateTime": 1463755899,
17 | "VoiceLength": 0,
18 | "PlayLength": 0,
19 | "FileName": "",
20 | "FileSize": "",
21 | "MediaId": "",
22 | "Url": "",
23 | "AppMsgType": 0,
24 | "StatusNotifyCode": 4,
25 | "StatusNotifyUserName": "@339651ba569948c2c1f18e547e5fcb49cc2672af2a4ece71ed97e1c3a572858a,filehelper,@@22e2e1f42c303adeda023a1affa9a9be06f94c1d55189cedf28aa5ad78d09466,@@3ebf5e3b9aaae4fb002da88d2f96f1266a9ba0940af389ae188e05fde83004d0,@@fd04c1950a724b4443471dc95aa7ca9ff1af4592666b1de6a754b0f7525514d3,@70ab99ca1c2e4df48de528ae29ce0fbd,@@1b754c7ae914f1223703ece64acbbbf375befa56d77fa02005279f277f223b69,@@180fa413a597f0f313529f756a974f021ba02ff8f8388c1588c76b844fa71935,@@f19c122a8ead15ea8c3ad88622695563652c88fc8e864da83874cad4192f6d59,@7a67b2ee7822d1b5b912a164c60c349f5764fc2b74842f01881ee8def5e8915a,@d89e585741b44686b43781cbd83fcdda,@97109f88e39691870bd7b691693ef2ad,@abd907b0a27c909b474822c71e34d0af,@@232e56f745cca4caa8db2291ea029d5293907dbee850205da04ff4e7054ca0cf,@8e6e7e848881943024373475e5eb2137f62761d7c137c3f2337222e2466e292c,@84d7f0d41b6c3ba1f5314fa300f92f14,@@267cd63603075529efed71f38d599ef7b24681adc955b4222321013103d8196c,@b2eaf06e6fca0a59091aec4dc53e546c,@43ad02c5b14ed2de8c9c69b92884df94e66300a7ce624cdaef05199963241849,@5228c41c5864e6876061fd96a3676ef2,@973a9a9c494ea40fe42143adcdedbd84,@@ed757bbda181ec895d9531e7c1a6d8abe3ea455e68108e10cff202f44992c577,@1b7659d7cba811e5781426a24ed7af40,@@bdb11cc12d5b6134c18bafb3870bb18f3981b4b19e90fa83ea55a57ef1c24e05,@e532dcd6984a21d1f8c40d9ee14ab79ad31f0bf1bf3183215668a7c0e1eeaf1f,@@5a95f75e97fce4ac94e00ca84df8028fc95ac88784facad9462b10eab8bdda76,@6e503d143b232ea63caa4489215683cd930ac62e71b7615eecdecdadcc455fd2,@3b233e3bf704f036c441d0d2ad1f9cabe0ff2fbc87a0f4f2c57e48c39aeb28ed,@2e8eefceee672de78b9921dd2bd931e5,@6821db112aa438d0b408a14196493f59d14116a3153b00bfb83586f3fc23e5b1,@2af4f90d933ba52fffe0e558d551bb871e004197808f83f9ad21ae3f92f4de83,@16fa931dba40ade8a4ea6e7c898ba5e1,@a5d0dfda5a1345ee07f5623e22ee05309bf306f3e9e7fa5f95421b15a1ef15fe,@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8,@510073889f1172c62900fbde22a9f6a147b535a1f0b066bb22e7318661e13113,@a7e4ac5ed53e8126c6bc291d4f54018b,@77606b6ef1f4664c6077d19c39b925d4,@106c13e1bd6fef28fa4327e23b960933e0d63c39c0f04c4450b1a66552211e8d,@944995d2a2106a4544d2752e605ef09fdd5698e63b9fa40e9c824b5388dd73f8,@489901ee910fb3a6ad6439e184f07fb6,@f6e939701047a89b52d071251e80a374206b4bf1eeb9140498a01076134933f6,@@2506a204db5af496004ed404bfa12959031f868bd77f0f311be474c883da0463,@dd8f7e98b68df1dd30206a19724761c282ad491a090de98d66239e0895808c7a,@@b23663d8223d095f09955211032a62893ee604b78d5c731089f84b4cdf6f6234,@913d088123f85a47dec47a05fb7349c6,@13312c0060d14d05ce45ec6f5c738213,@946a23b9c0025311114d7002190ddfce78a7ab1030e1961ccbb6342262ed45f2,@5850474942d686dac2372900d508ecf96777ce265e90d1f33739c38d62fad2c1,@ee5481813de66d3db4bf84153dd7c257c2961e151fca9ca31592a703be035ccf,@f842fe4a42b4ac43495d73e866ca62f8,@85912117ff22032e16b6ba62d525b4a0cb360c06f91b58a2745fdbf1c2b48346,@7bb52d4e8462f667f10ed05de44dfc3b,@4fcb8ac339e5250fee41781ebcd6bf8cb8caf0ea455b84a3bf690e1e8ec7ef49,@3d0cf36747a5f8a9299a6a4b8d7551e05d613c94618996b28b2716b74168728f,@3263b6785dbf48511e4e7b622080d0c62c84668f824fa124394b2d3e3ddb279e,@@0b59b0670e3c529a6a38a1b1a1460514fadc462307a18cf981791aaaa590d76d,@e120944a65eba3d106767dcdd033fff191713ee63b6f4b21855001d57f32ca77,@fe6d418bb94e6d775c449c944f44dd3ef33d1c8a61a06dacadf0f77a3a698f0b,@42fc42968c82e6ef6c5f373c8b460443,@947fc8e6b8bdd97a1bb4c6baf5b414eb,@d58b074a91f1d008f63fd0af3221e722,@45ca12e75c49deadb31d65b1c88204f5e76abbcc72484bb92b6c9e26961c5699,@1fefaaba7e843cea1b71a9992ed9f0c7fb38bdf8496e3a73e8479369c1b53de3,@@2ac25e5869f6d8a0369f4d333a554f1f11f26f800c2d6edb576dea836d864cf6,@51c41a81f0802376e26ad0d3c2fbc89c89e9206555621cacae0c4e967c8c9ad1,@955976a16011603f3c8533697c72aa4d7aba9290a9c295b8d79c61ca39eb7ad6,@@476fe5b2b15ac216a727cb09f3bc260897ed8d7448049672aef11b63ddadc28a,@411b3d15d08bef67d797ae7f0978d1e3,@6648d6570c26491b69c94857fba82abbd0ea783badc198d5dd6630c09b365b4d,@fe89ad3f479b5d0b3319443b01718d1396111e8f77eca218c22928b5c00ab1cf,@2d0c47fd0fce4872536191e9dfe55d99590f0ea6fe17f5df050fb8307386e9ed,@05c4ff9934f2bd8a7aec830086851aab,@925f8b60fd7d13129eb572313fab5212,@d1c9e9dcc37407877870e188fc9888fa,@1a37dfe536da44aa9f84f60c28c16e6f905179474f773591f6523883743d4d50,@b136de5596d3a7da4d965a088b2284fd224c756ccf2f8a219b37bbf5ad7fd2be,@7962f256818fceaec3b958d92ded8ee20dcee8c843a3a3757362405ed2131896,@@3fbf03446ab041dd4e652942cdc6bf343a44248f9e7ae25ee823d4a92ebddbdb,@544820fc7fd9c197dc46d9bdb5abdbf5,@@b3622827e463af2667ebe4c408d89e781a628ef557a87df6dcf5adfbdb7b06b3,@f2ec960d1736e9fd23a71cabf8a978900a3413ae78fd4bb9387eb1a8d306249f,@ed137e1c8513f0e6dc56f27b34f4380eefc0471a5fc080675c64f11c1ba27e90,@@73ee96919fddc6092eabcb27438eba628bc56df2bb5f89ee01368d6cb817d8af,@b2d92a727462efa35b7e8d33d31a3770208b750559bb454902d937c873d1e646,@358505f3cf454c1eca7177af2af1b846,@6329ec4916697109f731dab67c2c991d,@7c2530024418b1c94d5d7fb0f696793ac2b6edf806010bcc6ee7ce7e0229d52d,@b7f0b9a12a4e515efd797898437364b01135fad314d8b4fd3e2e6579606d5a2a,@@0d03fd688236be730d875af6a81d50f1dffbee01e7e23b1f06850e0150395981,@d53c6d659f5ef174bdc3c58b449ae27c,@df186b358f43330a231fa296f0c85e25,@4843f6c9705ed2f2fbe8c1cccd344cbe,@9fc24b6488400c5132c32ab8dbc0d59b,@970afe19850fae35664ce3992061b0167a6c1a13ee0e6b959ac93a1eaa29fa1f,@@9ed64a46473dbf59ca76bd021ec280333f6b49f69d09b5da270981f7b7ebcb48,@03c96f47661b3ab4535b525e6ff363a4,@55d1b9bf453003b86b0829db529171766c0aa13763b5f5b32a8c531a332d5064,@c78beceecfd88c03289a302d2c9c6407,@eaf122e72abe21c72a13ac9d6224c15ad99bfa6fc317fca584a1140cc59e2f63",
26 | "RecommendInfo": {
27 | "UserName": "",
28 | "NickName": "",
29 | "QQNum": 0,
30 | "Province": "",
31 | "City": "",
32 | "Content": "",
33 | "Signature": "",
34 | "Alias": "",
35 | "Scene": 0,
36 | "VerifyFlag": 0,
37 | "AttrStatus": 0,
38 | "Sex": 0,
39 | "Ticket": "",
40 | "OpCode": 0
41 | },
42 | "ForwardFlag": 0,
43 | "AppInfo": {
44 | "AppID": "",
45 | "Type": 0
46 | },
47 | "HasProductId": 0,
48 | "Ticket": "",
49 | "ImgHeight": 0,
50 | "ImgWidth": 0,
51 | "SubMsgType": 0,
52 | "NewMsgId": 4442356190966618405
53 | },
54 | {
55 | "MsgId": "6599448710267926107",
56 | "FromUserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
57 | "ToUserName": "@2ea0ad63b5550c8842c95cb987adea2491fc9e3db0899696f696149cd30688e8",
58 | "MsgType": 1,
59 | "Content": "Hello World",
60 | "Status": 3,
61 | "ImgStatus": 1,
62 | "CreateTime": 1463839052,
63 | "VoiceLength": 0,
64 | "PlayLength": 0,
65 | "FileName": "",
66 | "FileSize": "",
67 | "MediaId": "",
68 | "Url": "",
69 | "AppMsgType": 0,
70 | "StatusNotifyCode": 0,
71 | "StatusNotifyUserName": "",
72 | "RecommendInfo": {
73 | "UserName": "",
74 | "NickName": "",
75 | "QQNum": 0,
76 | "Province": "",
77 | "City": "",
78 | "Content": "",
79 | "Signature": "",
80 | "Alias": "",
81 | "Scene": 0,
82 | "VerifyFlag": 0,
83 | "AttrStatus": 0,
84 | "Sex": 0,
85 | "Ticket": "",
86 | "OpCode": 0
87 | },
88 | "ForwardFlag": 0,
89 | "AppInfo": {
90 | "AppID": "",
91 | "Type": 0
92 | },
93 | "HasProductId": 0,
94 | "Ticket": "",
95 | "ImgHeight": 0,
96 | "ImgWidth": 0,
97 | "SubMsgType": 0,
98 | "NewMsgId": 6599448710267926107
99 | }
100 | ],
101 | "ModContactCount": 1,
102 | "ModContactList": [
103 | {
104 | "UserName": "@@3ebf5e3b9aaae4fb002da88d2f96f1266a9ba0940af389ae188e05fde83004d0",
105 | "NickName": "Vue.js Podcast 群二",
106 | "Sex": 0,
107 | "HeadImgUpdateFlag": 1,
108 | "ContactType": 0,
109 | "Alias": "",
110 | "ChatRoomOwner": "@944995d2a2106a4544d2752e605ef09fdd5698e63b9fa40e9c824b5388dd73f8",
111 | "HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=0&username=@@3ebf5e3b9aaae4fb002da88d2f96f1266a9ba0940af389ae188e05fde83004d0&skey=@crypt_8e4ad7fa_2703a47aaf8cd4d3e61b855795e38568",
112 | "ContactFlag": 3,
113 | "MemberCount": 462,
114 | "MemberList": [
115 | {
116 | "Uin": 195765975,
117 | "UserName": "@e0ec8ddd68b83d04a51839008fdaa78e",
118 | "NickName": "吃圡死月不會碼代碼",
119 | "AttrStatus": 37851687,
120 | "PYInitial": "",
121 | "PYQuanPin": "",
122 | "RemarkPYInitial": "",
123 | "RemarkPYQuanPin": "",
124 | "MemberStatus": 0,
125 | "DisplayName": "",
126 | "KeyWord": ""
127 | }
128 | ],
129 | "HideInputBarFlag": 0,
130 | "Signature": "",
131 | "VerifyFlag": 0,
132 | "RemarkName": "",
133 | "Statues": 0,
134 | "AttrStatus": 0,
135 | "Province": "",
136 | "City": "",
137 | "SnsFlag": 0,
138 | "KeyWord": ""
139 | }
140 | ],
141 | "DelContactCount": 0,
142 | "DelContactList": [],
143 | "ModChatRoomMemberCount": 0,
144 | "ModChatRoomMemberList": [],
145 | "Profile": {
146 | "BitFlag": 0,
147 | "UserName": {
148 | "Buff": ""
149 | },
150 | "NickName": {
151 | "Buff": ""
152 | },
153 | "BindUin": 0,
154 | "BindEmail": {
155 | "Buff": ""
156 | },
157 | "BindMobile": {
158 | "Buff": ""
159 | },
160 | "Status": 0,
161 | "Sex": 0,
162 | "PersonalCard": 0,
163 | "Alias": "",
164 | "HeadImgUpdateFlag": 0,
165 | "HeadImgUrl": "",
166 | "Signature": ""
167 | },
168 | "ContinueFlag": 0,
169 | "SyncKey": {
170 | "Count": 10,
171 | "List": [
172 | {
173 | "Key": 1,
174 | "Val": 636444437
175 | },
176 | {
177 | "Key": 2,
178 | "Val": 636445051
179 | },
180 | {
181 | "Key": 3,
182 | "Val": 636444675
183 | },
184 | {
185 | "Key": 11,
186 | "Val": 636445020
187 | },
188 | {
189 | "Key": 13,
190 | "Val": 636434059
191 | },
192 | {
193 | "Key": 201,
194 | "Val": 1463755899
195 | },
196 | {
197 | "Key": 203,
198 | "Val": 1463752834
199 | },
200 | {
201 | "Key": 1000,
202 | "Val": 1463747283
203 | },
204 | {
205 | "Key": 1001,
206 | "Val": 1463740232
207 | },
208 | {
209 | "Key": 1002,
210 | "Val": 1463719049
211 | }
212 | ]
213 | },
214 | "SKey": ""
215 | }
--------------------------------------------------------------------------------
/test/unit.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | import {expect} from 'chai'
3 |
4 | import * as util from '../src/util'
5 | import MessageFactory from '../src/interface/message'
6 | import ContactFactory, * as contactMethod from '../src/interface/contact'
7 |
8 | describe('Util', () => {
9 | it('is not browser', () => {
10 | expect(util.isStandardBrowserEnv).to.equal(false)
11 | })
12 |
13 | it('is function', () => {
14 | expect(util.isFunction(() => {})).to.equal(true)
15 | expect(util.isFunction(null)).to.equal(false)
16 | })
17 |
18 | it('convert emoji method', () => {
19 | expect(util.convertEmoji('').charCodeAt()).to.equal(42)
20 | expect(util.convertEmoji('')).to.equal('')
21 | expect(util.convertEmoji(undefined)).to.equal('')
22 | })
23 |
24 | it('format num', () => {
25 | expect(util.formatNum(0, 2)).to.equal('00')
26 | expect(util.formatNum(2, 2)).to.equal('02')
27 | expect(util.formatNum(20, 2)).to.equal('20')
28 | })
29 | })
30 |
31 | describe('Message interface', () => {
32 | describe('Message: ', () => {
33 | let immutNewMessage1 = {
34 | FromUserName: 'test',
35 | Content: '<a>
bc'
36 | }
37 |
38 | let immutNewMessage2 = {
39 | FromUserName: '123',
40 | Content: '<a>
bc'
41 | }
42 |
43 | let Message
44 | let newMessage1
45 | let newMessage2
46 |
47 | beforeEach(() => {
48 | Message = MessageFactory({user: {UserName: 'test'}})
49 | newMessage1 = Message.extend(immutNewMessage1)
50 | newMessage2 = Message.extend(immutNewMessage2)
51 | })
52 |
53 | it('property stable', () => {
54 | expect(newMessage1.FromUserName).to.equal('test')
55 | })
56 |
57 | it('content parse', () => {
58 | expect(newMessage1.Content).to.equal('\nbc')
59 | })
60 |
61 | it('isSendBySelf', () => {
62 | expect(newMessage1.isSendBySelf).to.equal(true)
63 | expect(newMessage2.isSendBySelf).to.equal(false)
64 | })
65 |
66 | it('isSendBy', () => {
67 | expect(newMessage2.isSendBy({UserName: '123'})).to.equal(true)
68 | expect(newMessage2.isSendBy({UserName: 'test'})).to.equal(false)
69 | })
70 | })
71 | })
72 |
73 | describe('Contact interface: ', () => {
74 | describe('method: ', () => {
75 | it('get user by UserName', () => {
76 | var user = {UserName: 'test'}
77 | var list = [user]
78 |
79 | expect(contactMethod.getUserByUserName(list, 'test')).to.equal(user)
80 | })
81 |
82 | it('is room contact', () => {
83 | expect(contactMethod.isRoomContact({UserName: '@@123'})).to.equal(true)
84 | expect(contactMethod.isRoomContact({UserName: '123'})).to.equal(false)
85 | })
86 | })
87 |
88 | describe('Contact: ', () => {
89 | let immutUser1 = {
90 | UserName: 'test',
91 | NickName: 'test',
92 | HeadImgUrl: '/test'
93 | }
94 |
95 | let immutUser2 = {
96 | UserName: '@@test',
97 | NickName: 'test',
98 | HeadImgUrl: '/test'
99 | }
100 |
101 | let immutInstance = {
102 | user: immutUser1,
103 | baseUri: 'https://wx2.qq.com/',
104 | contacts: {}
105 | }
106 |
107 | let Contact
108 | let instance
109 |
110 | beforeEach(() => {
111 | instance = Object.assign({}, immutInstance)
112 |
113 | Contact = ContactFactory(instance)
114 |
115 | instance.contacts[immutUser1.UserName] = Contact.extend(immutUser1)
116 | instance.contacts[immutUser2.UserName] = Contact.extend(immutUser2)
117 | })
118 |
119 | it('property stable', () => {
120 | const user1 = instance.contacts[immutUser1.UserName]
121 | expect(user1.NickName).to.equal('test')
122 | })
123 |
124 | it('getDisplayName', () => {
125 | const user1 = instance.contacts[immutUser1.UserName]
126 | expect(user1.getDisplayName()).to.equal('test')
127 | })
128 |
129 | it('can search', () => {
130 | const user1 = instance.contacts[immutUser1.UserName]
131 | const user2 = instance.contacts[immutUser2.UserName]
132 | expect(user1.canSearch('te')).to.equal(true)
133 | expect(user2.canSearch('123')).to.equal(false)
134 | })
135 |
136 | it('isSelf', () => {
137 | const user1 = instance.contacts[immutUser1.UserName]
138 | const user2 = instance.contacts[immutUser2.UserName]
139 | expect(user1.isSelf).to.equal(true)
140 | expect(user2.isSelf).to.equal(false)
141 | })
142 |
143 | it('get user by username', () => {
144 | const user1 = instance.contacts[immutUser1.UserName]
145 | expect(Contact.getUserByUserName('test')).to.equal(user1)
146 | })
147 |
148 | it('get search user', () => {
149 | const user1 = instance.contacts[immutUser1.UserName]
150 | expect(Contact.getSearchUser('te')[0]).to.equal(user1)
151 | expect(Contact.getSearchUser('123').length).to.equal(0)
152 | })
153 |
154 | it('isRoomContact', () => {
155 | const user1 = instance.contacts[immutUser1.UserName]
156 | const user2 = instance.contacts[immutUser2.UserName]
157 | expect(Contact.isRoomContact(user2)).to.equal(true)
158 | expect(Contact.isRoomContact(user1)).to.equal(false)
159 | })
160 | })
161 | })
162 |
--------------------------------------------------------------------------------