├── .gitignore ├── .jshintrc ├── .travis.yml ├── MIT-License ├── Makefile ├── README.en.md ├── README.md ├── figures ├── api.graffle ├── wechat.graffle └── wechat.png ├── index.js ├── lib ├── events.js ├── list.js ├── session.js └── wechat.js ├── package.json └── test ├── config.js ├── events.test.js ├── get_message.test.js ├── list.test.js ├── list2.test.js ├── parse.test.js ├── reply.test.js ├── session.test.js ├── support.js ├── talk.test.js ├── wechat.test.js ├── wechat2.test.js ├── wechat3.test.js ├── wechat_encrypted.test.js ├── wechat_multi.test.js ├── wechat_nohandle.test.js └── wechat_nosignature.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example 3 | .DS_Store 4 | coverage 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "module", 5 | "require", 6 | "__dirname", 7 | "process", 8 | "console", 9 | "it", 10 | "xit", 11 | "describe", 12 | "xdescribe", 13 | "before", 14 | "beforeEach", 15 | "after", 16 | "afterEach" 17 | ], 18 | 19 | "node": true, 20 | "es5": true, 21 | "bitwise": true, 22 | "curly": true, 23 | "eqeqeq": true, 24 | "forin": false, 25 | "immed": true, 26 | "latedef": true, 27 | "noarg": true, 28 | "noempty": true, 29 | "nonew": true, 30 | "plusplus": false, 31 | "undef": true, 32 | "strict": false, 33 | "trailing": false, 34 | "globalstrict": true, 35 | "nonstandard": true, 36 | "white": false, 37 | "indent": 2, 38 | "expr": true, 39 | "multistr": true, 40 | "onevar": false, 41 | "unused": "vars" 42 | } 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | script: make test-coveralls 8 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jackson Tian 2 | http://weibo.com/shyvo 3 | 4 | The MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | ISTANBUL = ./node_modules/.bin/istanbul 5 | MOCHA = ./node_modules/mocha/bin/_mocha 6 | COVERALLS = ./node_modules/coveralls/bin/coveralls.js 7 | 8 | test: 9 | @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 10 | $(MOCHA_OPTS) \ 11 | $(TESTS) 12 | 13 | test-cov: 14 | @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 15 | 16 | test-coveralls: 17 | @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 18 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 19 | @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage 20 | 21 | test-all: test test-coveralls 22 | 23 | .PHONY: test 24 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | wechat [![NPM version](https://badge.fury.io/js/wechat.png)](http://badge.fury.io/js/wechat) [![Build Status](https://travis-ci.org/node-webot/wechat.png?branch=master)](https://travis-ci.org/node-webot/wechat) [![Dependencies Status](https://david-dm.org/node-webot/wechat.png)](https://david-dm.org/node-webot/wechat) [![Coverage Status](https://coveralls.io/repos/node-webot/wechat/badge.png)](https://coveralls.io/r/node-webot/wechat) 2 | ====== 3 | 4 | Wechat is a middleware and SDK of Wechat Official Account Admin Platform (mp.weixin.qq.com). 5 | 6 | This wechat document is translated by [Guo Yu](https://github.com/turingou/), if you have some understanding problems, please feel free open an issue [here](https://github.com/turingou/wechat/issues). 7 | 8 | ## Features 9 | 10 | - Auto reply (text, image, videos, music, thumbnails posts are supported) 11 | - CRM message (text, image, videos, music, thumbnails posts are supported) 12 | - Menu settings (CRD are supported) 13 | - QR codes (CR are supported, both temporary and permanent) 14 | - Group settings (CRUD are supported) 15 | - Followers infomation (fetching user's info or followers list) 16 | - Media (upload or download) 17 | - Reply Waiter (good for surveys) 18 | - Sessions 19 | - OAuth API 20 | - Payment (deliver notify and order query) 21 | 22 | API details located [here](http://node-webot.github.io/wechat/api.html) 23 | 24 | ## Installation 25 | 26 | ``` 27 | npm install wechat 28 | ``` 29 | 30 | ## Use with Connect/Express 31 | 32 | ``` 33 | var wechat = require('wechat'); 34 | 35 | app.use(connect.query()); // Or app.use(express.query()); 36 | app.use('/wechat', wechat('some token', function (req, res, next) { 37 | // message is located in req.weixin 38 | var message = req.weixin; 39 | if (message.FromUserName === 'diaosi') { 40 | // reply with text 41 | res.reply('hehe'); 42 | } else if (message.FromUserName === 'text') { 43 | // another way to reply with text 44 | res.reply({ 45 | content: 'text object', 46 | type: 'text' 47 | }); 48 | } else if (message.FromUserName === 'hehe') { 49 | // reply with music 50 | res.reply({ 51 | type: "music", 52 | content: { 53 | title: "Just some music", 54 | description: "I have nothing to lose", 55 | musicUrl: "http://mp3.com/xx.mp3", 56 | hqMusicUrl: "http://mp3.com/xx.mp3" 57 | } 58 | }); 59 | } else { 60 | // reply with thumbnails posts 61 | res.reply([ 62 | { 63 | title: 'Come to fetch me', 64 | description: 'or you want to play in another way ?', 65 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 66 | url: 'http://nodeapi.cloudfoundry.com/' 67 | } 68 | ]); 69 | } 70 | })); 71 | ``` 72 | 73 | *Tips*: you'll have to apply `token` at [Wechat platform (this page is in Chinese)](http://mp.weixin.qq.com/cgi-bin/callbackprofile?type=info&t=wxm-developer-ahead&lang=zh_CN) 74 | 75 | ### Reply Messages 76 | 77 | auto reply a message when your followers send a message to you. also text, image, videos, music, thumbnails posts are supported. details API goes [here (official documents)](http://mp.weixin.qq.com/wiki/index.php?title=发送被动响应消息) 78 | 79 | #### Reply with text 80 | ``` 81 | res.reply('Hello world!'); 82 | // or 83 | res.reply({type: "text", content: 'Hello world!'}); 84 | ``` 85 | #### Reply with Image 86 | ``` 87 | res.reply({ 88 | type: "image", 89 | content: { 90 | mediaId: 'mediaId' 91 | } 92 | }); 93 | ``` 94 | #### Reply with voice 95 | ``` 96 | res.reply({ 97 | type: "voice", 98 | content: { 99 | mediaId: 'mediaId' 100 | } 101 | }); 102 | ``` 103 | #### Reply with Video 104 | ``` 105 | res.reply({ 106 | type: "video", 107 | content: { 108 | mediaId: 'mediaId', 109 | thumbMediaId: 'thumbMediaId' 110 | } 111 | }); 112 | ``` 113 | #### Reply with Music 114 | ``` 115 | res.reply({ 116 | title: "Just some music", 117 | description: "I have nothing to lose", 118 | musicUrl: "http://mp3.com/xx.mp3", 119 | hqMusicUrl: "http://mp3.com/xx.mp3" 120 | }); 121 | ``` 122 | #### Reply with Thumbnails posts 123 | ``` 124 | res.reply([ 125 | { 126 | title: 'Come to fetch me', 127 | description: 'or you want to play in another way ?', 128 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 129 | url: 'http://nodeapi.cloudfoundry.com/' 130 | } 131 | ]); 132 | ``` 133 | #### Reply with social function messages 134 | ```js 135 | res.reply({ 136 | type: 'hardware', 137 | HardWare:{ 138 | MessageView: 'myrank', 139 | MessageAction: 'ranklist' 140 | } 141 | }); 142 | ``` 143 | ### transfer user message to wechat customer service 144 | transfer the message sent from wechat users to the Wechat Multi Customer Service 145 | ```js 146 | res.transfer2CustomerService(); 147 | ``` 148 | 149 | ### Reply with device messages 150 | Specific responses will be made as the message type is device_text or device_event. 151 | ```js 152 | var wechat = require('wechat'); 153 | var config = { 154 | token: 'token', 155 | appid: 'appid', 156 | encodingAESKey: 'encodinAESKey', 157 | checkSignature: true // optional, default value is true. Because WeChat open platform debug tool does not send signature in plaintext mode, if you want to use this debug tool, please set to false 158 | }; 159 | 160 | app.use(express.query()); 161 | app.use('/wechat', wechat(config, function (req, res, next) { 162 | // message is located in req.weixin 163 | var message = req.weixin; 164 | if (message.MsgType === 'device_text') { 165 | // device text 166 | res.reply('This message will be pushed onto the device.'); 167 | } else if (message.MsgType === 'device_event') { 168 | if (message.Event === 'subscribe_status' || 169 | message.Event === 'unsubscribe_status') { 170 | //subscribe or unsubscribe the WIFI device status,the reply should be 1 or 0 171 | res.reply(1); 172 | } else { 173 | res.reply('This message will be pushed onto the device.') 174 | } 175 | } 176 | })); 177 | ``` 178 | ### WXSession 179 | 180 | Wechat messages are not communicate like traditional C/S model, therefore nothing Cookies will be store in Wechat client. this WXSession is designed to support access user's infomation via `req.wxsession`, with `connect.session` backed. 181 | 182 | It's a simple demo: 183 | 184 | ``` 185 | app.use(connect.cookieParser()); 186 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 187 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 188 | if (info.Content === '=') { 189 | var exp = req.wxsession.text.join(''); 190 | req.wxsession.text = ''; 191 | res.reply(exp); 192 | } else { 193 | req.wxsession.text = req.wxsession.text || []; 194 | req.wxsession.text.push(info.Content); 195 | res.reply('Message got ' + info.Content); 196 | } 197 | }))); 198 | ``` 199 | 200 | `req.wxsession` and `req.session` shares same store. width `redis` as persistence database, across processes sharing are supportd. 201 | 202 | ### Reply Waiter 203 | 204 | a reply waiter is seems like a telephone menu system. it must be setup before activation. this function is supported upon WXSession. 205 | 206 | ``` 207 | var List = require('wechat').List; 208 | List.add('view', [ 209 | ['reply {a}', function (info, req, res) { 210 | res.reply('Im Answer A'); 211 | }], 212 | ['reply {b}', function (info, req, res) { 213 | res.reply('Im Answer B'); 214 | }], 215 | ['reply {c}', 'Im Answer C (the shorthand method)'] 216 | ]); 217 | ``` 218 | 219 | active the reply waiter we setuped before: 220 | 221 | ``` 222 | var app = connect(); 223 | app.use(connect.query()); 224 | app.use(connect.cookieParser()); 225 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 226 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 227 | if (info.Content === 'list') { 228 | res.wait('view'); // view is the very waiter we setuped before. 229 | } else { 230 | res.reply('hehe'); 231 | // or stop the waiter and quit. 232 | // res.nowait('hehe'); 233 | } 234 | }))); 235 | ``` 236 | 237 | if waiter `view` actived, user will receive messages below: 238 | 239 | ``` 240 | reply a 241 | reply b 242 | reply c 243 | ``` 244 | 245 | reply waiter acquires both function and text as a `callback` action 246 | 247 | ``` 248 | List.add('view', [ 249 | ['reply {a}', function (info, req, res, next) { 250 | // we callback as a function 251 | res.reply('Answer A'); 252 | }], 253 | // or text as shorthand 254 | ['reply {c}', 'Answer C'] 255 | ]); 256 | ``` 257 | 258 | if user's message is not in waiter's trigger texts. this message will be processd in the `else` way and can be stoped by `res.nowait()`, `res.nowait` method actions like `reply` method. 259 | 260 | ## Show cases 261 | ### Auto-reply robot based on Node.js 262 | 263 | ![Node.js API Auto-reply robot](http://nodeapi.diveintonode.org/assets/qrcode.jpg) 264 | 265 | Codes here 266 | 267 | robots can be setup in PAASs like [CloudFoundry](http://www.cloudfoundry.com/), [appfog](https://www.appfog.com/) or [BAE](http://developer.baidu.com/wiki/index.php?title=docs/cplat/rt/node.js). 268 | 269 | ## API details 270 | official document locates here [Messages API Guide (in Chinese)](http://mp.weixin.qq.com/wiki/index.php?title=消息接口指南)。 271 | 272 | wachat 0.6.x supports shorthand methods below: 273 | 274 | ``` 275 | app.use('/wechat', wechat('some token', wechat.text(function (message, req, res, next) { 276 | // reply with text 277 | // { ToUserName: 'gh_d3e07d51b513', 278 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 279 | // CreateTime: '1359125035', 280 | // MsgType: 'text', 281 | // Content: 'http', 282 | // MsgId: '5837397576500011341' } 283 | }).image(function (message, req, res, next) { 284 | // message为图片内容 285 | // { ToUserName: 'gh_d3e07d51b513', 286 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 287 | // CreateTime: '1359124971', 288 | // MsgType: 'image', 289 | // PicUrl: 'http://mmsns.qpic.cn/mmsns/bfc815ygvIWcaaZlEXJV7NzhmA3Y2fc4eBOxLjpPI60Q1Q6ibYicwg/0', 290 | // MediaId: 'media_id', 291 | // MsgId: '5837397301622104395' } 292 | }).voice(function (message, req, res, next) { 293 | // Reply with Voice 294 | // { ToUserName: 'gh_d3e07d51b513', 295 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 296 | // CreateTime: '1359125022', 297 | // MsgType: 'voice', 298 | // MediaId: 'OMYnpghh8fRfzHL8obuboDN9rmLig4s0xdpoNT6a5BoFZWufbE6srbCKc_bxduzS', 299 | // Format: 'amr', 300 | // MsgId: '5837397520665436492' } 301 | }).video(function (message, req, res, next) { 302 | // Reply with Video 303 | // { ToUserName: 'gh_d3e07d51b513', 304 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 305 | // CreateTime: '1359125022', 306 | // MsgType: 'video', 307 | // MediaId: 'OMYnpghh8fRfzHL8obuboDN9rmLig4s0xdpoNT6a5BoFZWufbE6srbCKc_bxduzS', 308 | // ThumbMediaId: 'media_id', 309 | // MsgId: '5837397520665436492' } 310 | }).location(function (message, req, res, next) { 311 | // Reply with Location (geo) 312 | // { ToUserName: 'gh_d3e07d51b513', 313 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 314 | // CreateTime: '1359125311', 315 | // MsgType: 'location', 316 | // Location_X: '30.283950', 317 | // Location_Y: '120.063139', 318 | // Scale: '15', 319 | // Label: {}, 320 | // MsgId: '5837398761910985062' } 321 | }).link(function (message, req, res, next) { 322 | // Reply with Link 323 | // { ToUserName: 'gh_d3e07d51b513', 324 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 325 | // CreateTime: '1359125022', 326 | // MsgType: 'link', 327 | // Title: 'A link', 328 | // Description: 'A link has its desc', 329 | // Url: 'http://1024.com/', 330 | // MsgId: '5837397520665436492' } 331 | }).event(function (message, req, res, next) { 332 | // Reply with Event 333 | // { ToUserName: 'gh_d3e07d51b513', 334 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 335 | // CreateTime: '1359125022', 336 | // MsgType: 'event', 337 | // Event: 'LOCATION', 338 | // Latitude: '23.137466', 339 | // Longitude: '113.352425', 340 | // Precision: '119.385040', 341 | // MsgId: '5837397520665436492' } 342 | }).device_text(function (message, req, res, next) { 343 | // Reply with device text. 344 | // { ToUserName: 'gh_d3e07d51b513', 345 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 346 | // CreateTime: '1359125022', 347 | // MsgType: 'device_text', 348 | // DeviceType: 'gh_d3e07d51b513' 349 | // DeviceID: 'dev1234abcd', 350 | // Content: 'd2hvc3lvdXJkYWRkeQ==', 351 | // SessionID: '9394', 352 | // MsgId: '5837397520665436492', 353 | // OpenID: 'oPKu7jgOibOA-De4u8J2RuNKpZRw' } 354 | }).device_event(function (message, req, res, next) { 355 | // Reply with device event. 356 | // { ToUserName: 'gh_d3e07d51b513', 357 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 358 | // CreateTime: '1359125022', 359 | // MsgType: 'device_event', 360 | // Event: 'bind' 361 | // DeviceType: 'gh_d3e07d51b513' 362 | // DeviceID: 'dev1234abcd', 363 | // OpType : 0, //Available as Event is subscribe_status or unsubscribe_status. 364 | // Content: 'd2hvc3lvdXJkYWRkeQ==', //Available as Event is not subscribe_status and unsubscribe_status. 365 | // SessionID: '9394', 366 | // MsgId: '5837397520665436492', 367 | // OpenID: 'oPKu7jgOibOA-De4u8J2RuNKpZRw' } 368 | }))); 369 | ``` 370 | 371 | *Tips*: `text`, `image`, `voice`, `video`, `location`, `link`, `event`, `device_text`, `device_event` must be set at least one. 372 | 373 | ### More simple APIs 374 | 375 | Supported in 0.3.x and above. 376 | 377 | ``` 378 | app.use('/wechat', wechat('some token').text(function (message, req, res, next) { 379 | // TODO 380 | }).image(function (message, req, res, next) { 381 | // TODO 382 | }).voice(function (message, req, res, next) { 383 | // TODO 384 | }).video(function (message, req, res, next) { 385 | // TODO 386 | }).location(function (message, req, res, next) { 387 | // TODO 388 | }).link(function (message, req, res, next) { 389 | // TODO 390 | }).event(function (message, req, res, next) { 391 | // TODO 392 | }).device_text(function (message, req, res, next) { 393 | // TODO 394 | }).device_event(function (message, req, res, next) { 395 | // TODO 396 | }).middlewarify()); 397 | ``` 398 | 399 | ### Functions Graph 400 | ![graph](https://raw.github.com/node-webot/wechat/master/figures/wechat.png) 401 | 402 | *Tips*: Business logic in blue lines. 403 | 404 | ## License 405 | The MIT license. 406 | 407 | ## Donation 408 | buy me a cup of coffee please. 409 | 410 | [![donate wechat](https://img.alipay.com/sys/personalprod/style/mc/btn-index.png)](https://me.alipay.com/jacksontian) 411 | 412 | 413 | Or: 414 | 415 | [![](http://img.shields.io/gratipay/JacksonTian.svg)](https://www.gittip.com/JacksonTian/) 416 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wechat 2 | ====== 3 | 4 | 微信公共平台自动回复消息接口服务中间件 5 | 6 | [Wechat document in English](./README.en.md) 7 | 8 | ## 模块状态 9 | - [![NPM version](https://badge.fury.io/js/wechat.png)](http://badge.fury.io/js/wechat) 10 | - [![Build Status](https://travis-ci.org/node-webot/wechat.png?branch=master)](https://travis-ci.org/node-webot/wechat) 11 | - [![Dependencies Status](https://david-dm.org/node-webot/wechat.png)](https://david-dm.org/node-webot/wechat) 12 | - [![Coverage Status](https://coveralls.io/repos/node-webot/wechat/badge.png)](https://coveralls.io/r/node-webot/wechat) 13 | 14 | ## 功能列表 15 | - 自动回复(文本、图片、语音、视频、音乐、图文) 16 | - 等待回复(用于调查问卷、问答等场景) 17 | - 会话支持(创新功能) 18 | 19 | 详细参见[API文档](http://doxmate.cool/node-webot/wechat/api.html) 20 | 21 | - 自动回复部分的Koa/Co版本: 22 | - 更多功能请前往:,Koa/Co版本: 23 | - 企业功能请前往: 24 | - OAuth功能请前往: 25 | - 微信支付功能请前往: 26 | 27 | ## Installation 28 | 29 | ```sh 30 | $ npm install wechat 31 | ``` 32 | 33 | ## Use with Connect/Express 34 | 35 | ```js 36 | var wechat = require('wechat'); 37 | var config = { 38 | token: 'token', 39 | appid: 'appid', 40 | encodingAESKey: 'encodinAESKey', 41 | checkSignature: true // 可选,默认为true。由于微信公众平台接口调试工具在明文模式下不发送签名,所以如要使用该测试工具,请将其设置为false 42 | }; 43 | 44 | app.use(express.query()); 45 | app.use('/wechat', wechat(config, function (req, res, next) { 46 | // 微信输入信息都在req.weixin上 47 | var message = req.weixin; 48 | if (message.FromUserName === 'diaosi') { 49 | // 回复屌丝(普通回复) 50 | res.reply('hehe'); 51 | } else if (message.FromUserName === 'text') { 52 | //你也可以这样回复text类型的信息 53 | res.reply({ 54 | content: 'text object', 55 | type: 'text' 56 | }); 57 | } else if (message.FromUserName === 'hehe') { 58 | // 回复一段音乐 59 | res.reply({ 60 | type: "music", 61 | content: { 62 | title: "来段音乐吧", 63 | description: "一无所有", 64 | musicUrl: "http://mp3.com/xx.mp3", 65 | hqMusicUrl: "http://mp3.com/xx.mp3", 66 | thumbMediaId: "thisThumbMediaId" 67 | } 68 | }); 69 | } else { 70 | // 回复高富帅(图文回复) 71 | res.reply([ 72 | { 73 | title: '你来我家接我吧', 74 | description: '这是女神与高富帅之间的对话', 75 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 76 | url: 'http://nodeapi.cloudfoundry.com/' 77 | } 78 | ]); 79 | } 80 | })); 81 | ``` 82 | 备注:token在微信平台的开发者中心申请 83 | 84 | ### 回复消息 85 | 当用户发送消息到微信公众账号,自动回复一条消息。这条消息可以是文本、图片、语音、视频、音乐、图文。详见:[官方文档](http://mp.weixin.qq.com/wiki/index.php?title=发送被动响应消息) 86 | 87 | #### 回复文本 88 | ```js 89 | res.reply('Hello world!'); 90 | // 或者 91 | res.reply({type: "text", content: 'Hello world!'}); 92 | ``` 93 | #### 回复图片 94 | ```js 95 | res.reply({ 96 | type: "image", 97 | content: { 98 | mediaId: 'mediaId' 99 | } 100 | }); 101 | ``` 102 | #### 回复语音 103 | ```js 104 | res.reply({ 105 | type: "voice", 106 | content: { 107 | mediaId: 'mediaId' 108 | } 109 | }); 110 | ``` 111 | #### 回复视频 112 | ```js 113 | res.reply({ 114 | type: "video", 115 | content: { 116 | title: '来段视频吧', 117 | description: '女神与高富帅', 118 | mediaId: 'mediaId' 119 | } 120 | }); 121 | ``` 122 | #### 回复音乐 123 | ```js 124 | res.reply({ 125 | title: "来段音乐吧", 126 | description: "一无所有", 127 | musicUrl: "http://mp3.com/xx.mp3", 128 | hqMusicUrl: "http://mp3.com/xx.mp3", 129 | thumbMediaId: "thisThumbMediaId" 130 | }); 131 | ``` 132 | #### 回复图文 133 | ```js 134 | res.reply([ 135 | { 136 | title: '你来我家接我吧', 137 | description: '这是女神与高富帅之间的对话', 138 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 139 | url: 'http://nodeapi.cloudfoundry.com/' 140 | } 141 | ]); 142 | ``` 143 | #### 回复设备社交功能消息 144 | ```js 145 | res.reply({ 146 | type: 'hardware', 147 | HardWare:{ 148 | MessageView: 'myrank', 149 | MessageAction: 'ranklist' 150 | } 151 | }); 152 | 153 | ``` 154 | ### 将用户消息转发到多客服 155 | 将普通微信用户向公众号发的消息,转发到多客服系统 156 | ```js 157 | res.transfer2CustomerService(); 158 | ``` 159 | 160 | ### 回复设备消息 161 | 模块可以对类型为device_text或device_event的消息作出特定格式的响应. 162 | ```js 163 | var wechat = require('wechat'); 164 | var config = { 165 | token: 'token', 166 | appid: 'appid', 167 | encodingAESKey: 'encodinAESKey', 168 | checkSignature: true // 可选,默认为true。由于微信公众平台接口调试工具在明文模式下不发送签名,所以如要使用该测试工具,请将其设置为false 169 | }; 170 | 171 | app.use(express.query()); 172 | app.use('/wechat', wechat(config, function (req, res, next) { 173 | // 微信输入信息都在req.weixin上 174 | var message = req.weixin; 175 | if (message.MsgType === 'device_text') { 176 | // 设备文本消息 177 | res.reply('这条回复会推到设备里去.'); 178 | } else if (message.MsgType === 'device_event') { 179 | if (message.Event === 'subscribe_status' || 180 | message.Event === 'unsubscribe_status') { 181 | //WIFI设备状态订阅,回复设备状态(1或0) 182 | res.reply(1); 183 | } else { 184 | res.reply('这条回复会推到设备里去.') 185 | } 186 | } 187 | })); 188 | ``` 189 | 190 | ### OAuth 191 | OAuth功能请前往: 192 | 193 | ### WXSession支持 194 | 由于公共平台应用的客户端实际上是微信,所以采用传统的Cookie来实现会话并不现实,为此中间件模块在openid的基础上添加了Session支持。一旦服务端启用了`connect.session`中间件,在业务中就可以访问`req.wxsession`属性。这个属性与`req.session`行为类似。 195 | 196 | ```js 197 | app.use(connect.cookieParser()); 198 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 199 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 200 | if (info.Content === '=') { 201 | var exp = req.wxsession.text.join(''); 202 | req.wxsession.text = ''; 203 | res.reply(exp); 204 | } else { 205 | req.wxsession.text = req.wxsession.text || []; 206 | req.wxsession.text.push(info.Content); 207 | res.reply('收到' + info.Content); 208 | } 209 | }))); 210 | ``` 211 | 212 | `req.wxsession`与`req.session`采用相同的存储引擎,这意味着如果采用redis作为存储,这样`wxsession`可以实现跨进程共享。 213 | 214 | ### 等待回复 215 | 等待回复,类似于电话拨号业务。该功能在WXSession的基础上提供。需要为等待回复预置操作,中间件将其抽象为`List`对象,在提供服务前需要添加服务。 216 | 217 | ```js 218 | var List = require('wechat').List; 219 | List.add('view', [ 220 | ['回复{a}查看我的性别', function (info, req, res) { 221 | res.reply('我是个妹纸哟'); 222 | }], 223 | ['回复{b}查看我的年龄', function (info, req, res) { 224 | res.reply('我今年18岁'); 225 | }], 226 | ['回复{c}查看我的性取向', '这样的事情怎么好意思告诉你啦- -'] 227 | ]); 228 | ``` 229 | 230 | 然后在业务中触发等待回复事务,如下示例,当收到用户发送`list`后,调用`res.wait('view')`进入事务`view`中。 231 | 232 | ```js 233 | var app = connect(); 234 | app.use(connect.query()); 235 | app.use(connect.cookieParser()); 236 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 237 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 238 | if (info.Content === 'list') { 239 | res.wait('view'); 240 | } else { 241 | res.reply('hehe'); 242 | // 或者中断等待回复事务 243 | // res.nowait('hehe'); 244 | } 245 | }))); 246 | ``` 247 | 用户将收到如下回复: 248 | 249 | ``` 250 | 回复a查看我的性别 251 | 回复b查看我的年龄 252 | 回复c查看我的性取向 253 | ``` 254 | 255 | 用户回复其中的`a`、`b`、`c`将会由注册的方法接管回复。回复可以是一个函数,也可以是一个字符串: 256 | 257 | ```js 258 | List.add('view', [ 259 | ['回复{a}查看我的性别', function (info, req, res, next) { 260 | res.reply('我是个妹纸哟'); 261 | }], 262 | // 或者字符串 263 | ['回复{c}查看我的性取向', '这样的事情怎么好意思告诉你啦- -'] 264 | ]); 265 | ``` 266 | 267 | 如果用户触发等待回复事务后,没有按照`{}`中的进行回复,那么将会由原有的默认函数进行处理。在原有函数中,可以选择调用`res.nowait()`中断事务。`nowait()`除了能中断事务外,与`reply`的行为一致。 268 | 269 | ## Show cases 270 | ### Node.js API自动回复 271 | 272 | ![Node.js API自动回复机器人](http://nodeapi.diveintonode.org/assets/qrcode.jpg) 273 | 274 | 欢迎关注。 275 | 276 | 代码: 277 | 278 | 你可以在[CloudFoundry](http://www.cloudfoundry.com/)、[appfog](https://www.appfog.com/)、[BAE](http://developer.baidu.com/wiki/index.php?title=docs/cplat/rt/node.js)等搭建自己的机器人。 279 | 280 | ## 详细API 281 | 原始API文档请参见:[消息接口指南](http://mp.weixin.qq.com/wiki/index.php?title=消息接口指南)。 282 | 283 | 目前微信公共平台能接收到7种内容:文字、图片、音频、视频、位置、链接、事件。支持6种回复:纯文本、图文、音乐、音频、图片、视频。 284 | 针对目前的业务形态,发布了0.6.x版本,该版本支持六种内容分别处理,以保持业务逻辑的简洁性。 285 | 286 | ```js 287 | app.use('/wechat', wechat('some token', wechat.text(function (message, req, res, next) { 288 | // message为文本内容 289 | // { ToUserName: 'gh_d3e07d51b513', 290 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 291 | // CreateTime: '1359125035', 292 | // MsgType: 'text', 293 | // Content: 'http', 294 | // MsgId: '5837397576500011341' } 295 | }).image(function (message, req, res, next) { 296 | // message为图片内容 297 | // { ToUserName: 'gh_d3e07d51b513', 298 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 299 | // CreateTime: '1359124971', 300 | // MsgType: 'image', 301 | // PicUrl: 'http://mmsns.qpic.cn/mmsns/bfc815ygvIWcaaZlEXJV7NzhmA3Y2fc4eBOxLjpPI60Q1Q6ibYicwg/0', 302 | // MediaId: 'media_id', 303 | // MsgId: '5837397301622104395' } 304 | }).voice(function (message, req, res, next) { 305 | // message为音频内容 306 | // { ToUserName: 'gh_d3e07d51b513', 307 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 308 | // CreateTime: '1359125022', 309 | // MsgType: 'voice', 310 | // MediaId: 'OMYnpghh8fRfzHL8obuboDN9rmLig4s0xdpoNT6a5BoFZWufbE6srbCKc_bxduzS', 311 | // Format: 'amr', 312 | // MsgId: '5837397520665436492' } 313 | }).video(function (message, req, res, next) { 314 | // message为视频内容 315 | // { ToUserName: 'gh_d3e07d51b513', 316 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 317 | // CreateTime: '1359125022', 318 | // MsgType: 'video', 319 | // MediaId: 'OMYnpghh8fRfzHL8obuboDN9rmLig4s0xdpoNT6a5BoFZWufbE6srbCKc_bxduzS', 320 | // ThumbMediaId: 'media_id', 321 | // MsgId: '5837397520665436492' } 322 | }).shortvideo(function (message, req, res, next) { 323 | // message为短视频内容 324 | // { ToUserName: 'gh_d3e07d51b513', 325 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 326 | // CreateTime: '1359125022', 327 | // MsgType: 'shortvideo', 328 | // MediaId: 'OMYnpghh8fRfzHL8obuboDN9rmLig4s0xdpoNT6a5BoFZWufbE6srbCKc_bxduzS', 329 | // ThumbMediaId: 'media_id', 330 | // MsgId: '5837397520665436492' } 331 | }).location(function (message, req, res, next) { 332 | // message为位置内容 333 | // { ToUserName: 'gh_d3e07d51b513', 334 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 335 | // CreateTime: '1359125311', 336 | // MsgType: 'location', 337 | // Location_X: '30.283950', 338 | // Location_Y: '120.063139', 339 | // Scale: '15', 340 | // Label: {}, 341 | // MsgId: '5837398761910985062' } 342 | }).link(function (message, req, res, next) { 343 | // message为链接内容 344 | // { ToUserName: 'gh_d3e07d51b513', 345 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 346 | // CreateTime: '1359125022', 347 | // MsgType: 'link', 348 | // Title: '公众平台官网链接', 349 | // Description: '公众平台官网链接', 350 | // Url: 'http://1024.com/', 351 | // MsgId: '5837397520665436492' } 352 | }).event(function (message, req, res, next) { 353 | // message为事件内容 354 | // { ToUserName: 'gh_d3e07d51b513', 355 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 356 | // CreateTime: '1359125022', 357 | // MsgType: 'event', 358 | // Event: 'LOCATION', 359 | // Latitude: '23.137466', 360 | // Longitude: '113.352425', 361 | // Precision: '119.385040', 362 | // MsgId: '5837397520665436492' } 363 | }).device_text(function (message, req, res, next) { 364 | // message为设备文本消息内容 365 | // { ToUserName: 'gh_d3e07d51b513', 366 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 367 | // CreateTime: '1359125022', 368 | // MsgType: 'device_text', 369 | // DeviceType: 'gh_d3e07d51b513' 370 | // DeviceID: 'dev1234abcd', 371 | // Content: 'd2hvc3lvdXJkYWRkeQ==', 372 | // SessionID: '9394', 373 | // MsgId: '5837397520665436492', 374 | // OpenID: 'oPKu7jgOibOA-De4u8J2RuNKpZRw' } 375 | }).device_event(function (message, req, res, next) { 376 | // message为设备事件内容 377 | // { ToUserName: 'gh_d3e07d51b513', 378 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 379 | // CreateTime: '1359125022', 380 | // MsgType: 'device_event', 381 | // Event: 'bind' 382 | // DeviceType: 'gh_d3e07d51b513' 383 | // DeviceID: 'dev1234abcd', 384 | // OpType : 0, //Event为subscribe_status/unsubscribe_status时存在 385 | // Content: 'd2hvc3lvdXJkYWRkeQ==', //Event不为subscribe_status/unsubscribe_status时存在 386 | // SessionID: '9394', 387 | // MsgId: '5837397520665436492', 388 | // OpenID: 'oPKu7jgOibOA-De4u8J2RuNKpZRw' } 389 | }))); 390 | ``` 391 | 392 | 注意: `text`, `image`, `voice`, `video`, `location`, `link`, `event`, `device_text`, `device_event`方法请至少指定一个。 393 | 这六个方法的设计适用于按内容类型区分处理的场景。如果需要更复杂的场景,请使用第一个例子中的API。 394 | 395 | ### 更简化的API设计 396 | 示例如下: 397 | 398 | ```js 399 | app.use('/wechat', wechat('some token').text(function (message, req, res, next) { 400 | // TODO 401 | }).image(function (message, req, res, next) { 402 | // TODO 403 | }).voice(function (message, req, res, next) { 404 | // TODO 405 | }).video(function (message, req, res, next) { 406 | // TODO 407 | }).location(function (message, req, res, next) { 408 | // TODO 409 | }).link(function (message, req, res, next) { 410 | // TODO 411 | }).event(function (message, req, res, next) { 412 | // TODO 413 | }).device_text(function (message, req, res, next) { 414 | // TODO 415 | }).device_event(function (message, req, res, next) { 416 | // TODO 417 | }).middlewarify()); 418 | ``` 419 | 该接口从0.3.x提供。 420 | 421 | ### 流程图 422 | ![graph](https://raw.github.com/node-webot/wechat/master/figures/wechat.png) 423 | 424 | 诸多细节由wechat中间件提供,用户只要关注蓝色部分的业务逻辑即可。 425 | 426 | ## 交流群 427 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 428 | 429 | ## 感谢 430 | 感谢以下贡献者: 431 | 432 | ``` 433 | $ git summary 434 | 435 | project : wechat 436 | repo age : 2 years, 5 months 437 | active : 136 days 438 | commits : 318 439 | files : 32 440 | authors : 441 | 265 Jackson Tian 83.3% 442 | 10 ifeiteng 3.1% 443 | 10 yelo 3.1% 444 | 4 realdog 1.3% 445 | 4 Bruce Lee 1.3% 446 | 3 Guo Yu 0.9% 447 | 2 zhongao 0.6% 448 | 2 Jesse Yang 0.6% 449 | 2 Lu Jun 0.6% 450 | 2 dan 0.6% 451 | 2 wxhuang 0.6% 452 | 1 Rogerz Zhang 0.3% 453 | 1 Foghost 0.3% 454 | 1 feichang.wyl 0.3% 455 | 1 feit 0.3% 456 | 1 feitian124 0.3% 457 | 1 LiSheep 0.3% 458 | 1 p13766 0.3% 459 | 1 Lance Li 0.3% 460 | 1 Chen Wei 0.3% 461 | 1 xianda 0.3% 462 | 1 Qun Lin 0.3% 463 | 1 TooBug 0.3% 464 | 465 | ``` 466 | 467 | ## 捐赠 468 | 如果您觉得Wechat对您有帮助,欢迎请作者一杯咖啡 469 | 470 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 471 | 472 | 或者[![](http://img.shields.io/gratipay/JacksonTian.svg)](https://www.gittip.com/JacksonTian/) 473 | 474 | ## License 475 | The MIT license. 476 | -------------------------------------------------------------------------------- /figures/wechat.graffle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplicationVersion 6 | 7 | com.omnigroup.OmniGrafflePro 8 | 139.18.0.187838 9 | 10 | CreationDate 11 | 2013-05-15 14:09:04 +0000 12 | Creator 13 | JacksonTian 14 | GraphDocumentVersion 15 | 8 16 | GuidesLocked 17 | NO 18 | GuidesVisible 19 | YES 20 | ImageCounter 21 | 1 22 | LinksVisible 23 | NO 24 | MagnetsVisible 25 | NO 26 | MasterSheets 27 | 28 | ModificationDate 29 | 2014-06-25 02:36:28 +0000 30 | Modifier 31 | JacksonTian 32 | NotesVisible 33 | NO 34 | OriginVisible 35 | NO 36 | PageBreaks 37 | YES 38 | PrintInfo 39 | 40 | NSBottomMargin 41 | 42 | float 43 | 41 44 | 45 | NSHorizonalPagination 46 | 47 | coded 48 | BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG 49 | 50 | NSLeftMargin 51 | 52 | float 53 | 18 54 | 55 | NSPaperSize 56 | 57 | size 58 | {595, 842} 59 | 60 | NSPrintReverseOrientation 61 | 62 | int 63 | 0 64 | 65 | NSRightMargin 66 | 67 | float 68 | 18 69 | 70 | NSTopMargin 71 | 72 | float 73 | 18 74 | 75 | 76 | ReadOnly 77 | NO 78 | Sheets 79 | 80 | 81 | ActiveLayerIndex 82 | 0 83 | AutoAdjust 84 | 85 | BackgroundGraphic 86 | 87 | Bounds 88 | {{0, 0}, {1118, 783}} 89 | Class 90 | SolidGraphic 91 | ID 92 | 2 93 | Style 94 | 95 | shadow 96 | 97 | Draws 98 | NO 99 | 100 | stroke 101 | 102 | Draws 103 | NO 104 | 105 | 106 | 107 | BaseZoom 108 | 0 109 | CanvasOrigin 110 | {0, 0} 111 | ColumnAlign 112 | 1 113 | ColumnSpacing 114 | 36 115 | DisplayScale 116 | 1 0/72 in = 1.0000 in 117 | GraphicsList 118 | 119 | 120 | Bounds 121 | {{174.01490783691406, 631}, {521.68653869628906, 72}} 122 | Class 123 | ShapedGraphic 124 | ID 125 | 92 126 | Magnets 127 | 128 | {0, 1} 129 | {0, -1} 130 | {1, 0} 131 | {-1, 0} 132 | 133 | Shape 134 | Rectangle 135 | Style 136 | 137 | fill 138 | 139 | Color 140 | 141 | b 142 | 0 143 | g 144 | 0.423462 145 | r 146 | 1 147 | 148 | 149 | stroke 150 | 151 | CornerRadius 152 | 5 153 | 154 | 155 | Text 156 | 157 | Text 158 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 159 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 STHeitiSC-Light;} 160 | {\colortbl;\red255\green255\blue255;} 161 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 162 | 163 | \f0\fs24 \cf0 Wechat API\ 164 | User 165 | \f1 , 166 | \f0 Shop} 167 | 168 | 169 | 170 | Class 171 | LineGraphic 172 | ID 173 | 91 174 | Points 175 | 176 | {391.67912007897462, 162.5} 177 | {355.00748872756958, 162.5} 178 | 179 | Style 180 | 181 | stroke 182 | 183 | HeadArrow 184 | StickArrow 185 | Legacy 186 | 187 | TailArrow 188 | 0 189 | 190 | 191 | 192 | 193 | Class 194 | LineGraphic 195 | ID 196 | 90 197 | Points 198 | 199 | {357.33585689682189, 149.5} 200 | {394.00748872756964, 149.5} 201 | 202 | Style 203 | 204 | stroke 205 | 206 | HeadArrow 207 | StickArrow 208 | Legacy 209 | 210 | TailArrow 211 | 0 212 | 213 | 214 | 215 | 216 | Bounds 217 | {{266.02982616424561, 118}, {78.970176696777344, 72.5}} 218 | Class 219 | ShapedGraphic 220 | ID 221 | 87 222 | Magnets 223 | 224 | {0, 1} 225 | {0, -1} 226 | {1, 0} 227 | {-1, 0} 228 | 229 | Shape 230 | Circle 231 | Style 232 | 233 | fill 234 | 235 | Color 236 | 237 | b 238 | 1 239 | g 240 | 0.525182 241 | r 242 | 0.724013 243 | 244 | 245 | stroke 246 | 247 | CornerRadius 248 | 5 249 | 250 | 251 | Text 252 | 253 | Text 254 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 255 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 256 | {\colortbl;\red255\green255\blue255;} 257 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 258 | 259 | \f0\fs24 \cf0 Connect\ 260 | Express} 261 | 262 | 263 | 264 | Class 265 | LineGraphic 266 | ID 267 | 86 268 | OrthogonalBarAutomatic 269 | 270 | OrthogonalBarPoint 271 | {0, 0} 272 | OrthogonalBarPosition 273 | -1 274 | Points 275 | 276 | {560.43656921386719, 230.99999075000005} 277 | {478.68653869628906, 164} 278 | 279 | Style 280 | 281 | stroke 282 | 283 | HeadArrow 284 | StickArrow 285 | Legacy 286 | 287 | LineType 288 | 2 289 | TailArrow 290 | 0 291 | 292 | 293 | Tail 294 | 295 | ID 296 | 54 297 | Info 298 | 2 299 | 300 | 301 | 302 | Class 303 | LineGraphic 304 | ID 305 | 85 306 | OrthogonalBarAutomatic 307 | 308 | OrthogonalBarPoint 309 | {0, 0} 310 | OrthogonalBarPosition 311 | -1 312 | Points 313 | 314 | {669.68653869628906, 308} 315 | {478.68653869628906, 151} 316 | 317 | Style 318 | 319 | stroke 320 | 321 | HeadArrow 322 | StickArrow 323 | Legacy 324 | 325 | LineType 326 | 2 327 | TailArrow 328 | 0 329 | 330 | 331 | Tail 332 | 333 | ID 334 | 83 335 | Info 336 | 2 337 | 338 | 339 | 340 | Class 341 | LineGraphic 342 | Head 343 | 344 | ID 345 | 83 346 | 347 | ID 348 | 84 349 | Points 350 | 351 | {669.68653869628906, 377} 352 | {669.68653869628906, 345} 353 | 354 | Style 355 | 356 | stroke 357 | 358 | HeadArrow 359 | StickArrow 360 | Legacy 361 | 362 | TailArrow 363 | 0 364 | 365 | 366 | Tail 367 | 368 | ID 369 | 81 370 | 371 | 372 | 373 | Bounds 374 | {{642.68653869628906, 308}, {54, 37}} 375 | Class 376 | ShapedGraphic 377 | ID 378 | 83 379 | Magnets 380 | 381 | {0, 1} 382 | {0, -1} 383 | {1, 0} 384 | {-1, 0} 385 | 386 | Shape 387 | Rectangle 388 | Style 389 | 390 | fill 391 | 392 | Color 393 | 394 | b 395 | 0 396 | g 397 | 0.941483 398 | r 399 | 1 400 | 401 | 402 | stroke 403 | 404 | CornerRadius 405 | 5 406 | 407 | 408 | Text 409 | 410 | Text 411 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 412 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 413 | {\colortbl;\red255\green255\blue255;} 414 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 415 | 416 | \f0\fs24 \cf0 toXML} 417 | 418 | 419 | 420 | Class 421 | LineGraphic 422 | Head 423 | 424 | ID 425 | 81 426 | Info 427 | 1 428 | 429 | ID 430 | 82 431 | OrthogonalBarAutomatic 432 | 433 | OrthogonalBarPoint 434 | {0, 0} 435 | OrthogonalBarPosition 436 | -1 437 | Points 438 | 439 | {644.01496887207031, 560} 440 | {669.68653869628906, 414} 441 | 442 | Style 443 | 444 | stroke 445 | 446 | HeadArrow 447 | StickArrow 448 | Legacy 449 | 450 | LineType 451 | 2 452 | TailArrow 453 | 0 454 | 455 | 456 | Tail 457 | 458 | ID 459 | 79 460 | Info 461 | 3 462 | 463 | 464 | 465 | Bounds 466 | {{642.68653869628906, 377}, {54, 37}} 467 | Class 468 | ShapedGraphic 469 | ID 470 | 81 471 | Magnets 472 | 473 | {0, 1} 474 | {0, -1} 475 | {1, 0} 476 | {-1, 0} 477 | 478 | Shape 479 | Rectangle 480 | Style 481 | 482 | fill 483 | 484 | Color 485 | 486 | b 487 | 0 488 | g 489 | 0.941483 490 | r 491 | 1 492 | 493 | 494 | stroke 495 | 496 | CornerRadius 497 | 5 498 | 499 | 500 | Text 501 | 502 | Text 503 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 504 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 505 | {\colortbl;\red255\green255\blue255;} 506 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 507 | 508 | \f0\fs24 \cf0 reply} 509 | 510 | 511 | 512 | Class 513 | LineGraphic 514 | Head 515 | 516 | ID 517 | 79 518 | 519 | ID 520 | 80 521 | Points 522 | 523 | {435.51496887207031, 507} 524 | {435.51496887207031, 541.5} 525 | 526 | Style 527 | 528 | stroke 529 | 530 | HeadArrow 531 | StickArrow 532 | Legacy 533 | 534 | TailArrow 535 | 0 536 | 537 | 538 | Tail 539 | 540 | ID 541 | 66 542 | Info 543 | 1 544 | 545 | 546 | 547 | Bounds 548 | {{227.01496887207031, 541.5}, {417, 37}} 549 | Class 550 | ShapedGraphic 551 | ID 552 | 79 553 | Magnets 554 | 555 | {0, 1} 556 | {0, -1} 557 | {1, 0} 558 | {-1, 0} 559 | 560 | Shape 561 | Rectangle 562 | Style 563 | 564 | fill 565 | 566 | Color 567 | 568 | b 569 | 1 570 | g 571 | 0.787332 572 | r 573 | 0.358313 574 | 575 | 576 | stroke 577 | 578 | CornerRadius 579 | 5 580 | 581 | 582 | Text 583 | 584 | Text 585 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 586 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 587 | {\colortbl;\red255\green255\blue255;} 588 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 589 | 590 | \f0\fs24 \cf0 User business logic} 591 | 592 | 593 | 594 | Class 595 | LineGraphic 596 | Head 597 | 598 | ID 599 | 66 600 | Info 601 | 2 602 | 603 | ID 604 | 67 605 | Points 606 | 607 | {435.51496887207031, 414} 608 | {435.51496887207031, 448.5} 609 | 610 | Style 611 | 612 | stroke 613 | 614 | HeadArrow 615 | StickArrow 616 | Legacy 617 | 618 | TailArrow 619 | 0 620 | 621 | 622 | Tail 623 | 624 | ID 625 | 57 626 | Info 627 | 1 628 | 629 | 630 | 631 | Bounds 632 | {{577.0150146484375, 459}, {54, 37}} 633 | Class 634 | ShapedGraphic 635 | ID 636 | 65 637 | Magnets 638 | 639 | {0, 1} 640 | {0, -1} 641 | {1, 0} 642 | {-1, 0} 643 | 644 | Shape 645 | Rectangle 646 | Style 647 | 648 | fill 649 | 650 | Color 651 | 652 | b 653 | 0.486347 654 | g 655 | 0 656 | r 657 | 1 658 | 659 | 660 | stroke 661 | 662 | CornerRadius 663 | 5 664 | 665 | 666 | Text 667 | 668 | Text 669 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 670 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 671 | {\colortbl;\red255\green255\blue255;} 672 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 673 | 674 | \f0\fs24 \cf0 event} 675 | 676 | 677 | 678 | Bounds 679 | {{509.26499938964844, 459}, {54, 37}} 680 | Class 681 | ShapedGraphic 682 | ID 683 | 64 684 | Magnets 685 | 686 | {0, 1} 687 | {0, -1} 688 | {1, 0} 689 | {-1, 0} 690 | 691 | Shape 692 | Rectangle 693 | Style 694 | 695 | fill 696 | 697 | Color 698 | 699 | b 700 | 0.486347 701 | g 702 | 0 703 | r 704 | 1 705 | 706 | 707 | stroke 708 | 709 | CornerRadius 710 | 5 711 | 712 | 713 | Text 714 | 715 | Text 716 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 717 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 718 | {\colortbl;\red255\green255\blue255;} 719 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 720 | 721 | \f0\fs24 \cf0 link} 722 | 723 | 724 | 725 | Bounds 726 | {{441.51498413085938, 459}, {54, 37}} 727 | Class 728 | ShapedGraphic 729 | ID 730 | 63 731 | Magnets 732 | 733 | {0, 1} 734 | {0, -1} 735 | {1, 0} 736 | {-1, 0} 737 | 738 | Shape 739 | Rectangle 740 | Style 741 | 742 | fill 743 | 744 | Color 745 | 746 | b 747 | 0.486347 748 | g 749 | 0 750 | r 751 | 1 752 | 753 | 754 | stroke 755 | 756 | CornerRadius 757 | 5 758 | 759 | 760 | Text 761 | 762 | Text 763 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 764 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 765 | {\colortbl;\red255\green255\blue255;} 766 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 767 | 768 | \f0\fs24 \cf0 voice} 769 | 770 | 771 | 772 | Bounds 773 | {{373.76496887207031, 459}, {54, 37}} 774 | Class 775 | ShapedGraphic 776 | ID 777 | 62 778 | Magnets 779 | 780 | {0, 1} 781 | {0, -1} 782 | {1, 0} 783 | {-1, 0} 784 | 785 | Shape 786 | Rectangle 787 | Style 788 | 789 | fill 790 | 791 | Color 792 | 793 | b 794 | 0.486347 795 | g 796 | 0 797 | r 798 | 1 799 | 800 | 801 | stroke 802 | 803 | CornerRadius 804 | 5 805 | 806 | 807 | Text 808 | 809 | Text 810 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 811 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 812 | {\colortbl;\red255\green255\blue255;} 813 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 814 | 815 | \f0\fs24 \cf0 location} 816 | 817 | 818 | 819 | Bounds 820 | {{174.0149040222168, 470.5}, {39, 14}} 821 | Class 822 | ShapedGraphic 823 | FitText 824 | YES 825 | Flow 826 | Resize 827 | ID 828 | 61 829 | Shape 830 | Rectangle 831 | Style 832 | 833 | fill 834 | 835 | Draws 836 | NO 837 | 838 | shadow 839 | 840 | Draws 841 | NO 842 | 843 | stroke 844 | 845 | Draws 846 | NO 847 | 848 | 849 | Text 850 | 851 | Align 852 | 0 853 | Pad 854 | 0 855 | Text 856 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 857 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 858 | {\colortbl;\red255\green255\blue255;} 859 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 860 | 861 | \f0\fs24 \cf0 Handle} 862 | VerticalPad 863 | 0 864 | 865 | Wrap 866 | NO 867 | 868 | 869 | Bounds 870 | {{308.80415344238281, 459}, {54, 37}} 871 | Class 872 | ShapedGraphic 873 | ID 874 | 60 875 | Magnets 876 | 877 | {0, 1} 878 | {0, -1} 879 | {1, 0} 880 | {-1, 0} 881 | 882 | Shape 883 | Rectangle 884 | Style 885 | 886 | fill 887 | 888 | Color 889 | 890 | b 891 | 0.486347 892 | g 893 | 0 894 | r 895 | 1 896 | 897 | 898 | stroke 899 | 900 | CornerRadius 901 | 5 902 | 903 | 904 | Text 905 | 906 | Text 907 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 908 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 909 | {\colortbl;\red255\green255\blue255;} 910 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 911 | 912 | \f0\fs24 \cf0 image} 913 | 914 | 915 | 916 | Bounds 917 | {{243.84333801269531, 459}, {54, 37}} 918 | Class 919 | ShapedGraphic 920 | ID 921 | 59 922 | Magnets 923 | 924 | {0, 1} 925 | {0, -1} 926 | {1, 0} 927 | {-1, 0} 928 | 929 | Shape 930 | Rectangle 931 | Style 932 | 933 | fill 934 | 935 | Color 936 | 937 | b 938 | 0.486347 939 | g 940 | 0 941 | r 942 | 1 943 | 944 | 945 | stroke 946 | 947 | CornerRadius 948 | 5 949 | 950 | 951 | Text 952 | 953 | Text 954 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 955 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 956 | {\colortbl;\red255\green255\blue255;} 957 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 958 | 959 | \f0\fs24 \cf0 text} 960 | 961 | 962 | 963 | Class 964 | LineGraphic 965 | Head 966 | 967 | ID 968 | 57 969 | Info 970 | 2 971 | 972 | ID 973 | 58 974 | Points 975 | 976 | {435.51496887207031, 345} 977 | {435.51496887207031, 377} 978 | 979 | Style 980 | 981 | stroke 982 | 983 | HeadArrow 984 | StickArrow 985 | Legacy 986 | 987 | TailArrow 988 | 0 989 | 990 | 991 | Tail 992 | 993 | ID 994 | 51 995 | Info 996 | 1 997 | 998 | 999 | 1000 | Bounds 1001 | {{408.51496887207031, 377}, {54, 37}} 1002 | Class 1003 | ShapedGraphic 1004 | ID 1005 | 57 1006 | Magnets 1007 | 1008 | {0, 1} 1009 | {0, -1} 1010 | {1, 0} 1011 | {-1, 0} 1012 | 1013 | Shape 1014 | Rectangle 1015 | Style 1016 | 1017 | fill 1018 | 1019 | Color 1020 | 1021 | b 1022 | 0 1023 | g 1024 | 0.941483 1025 | r 1026 | 1 1027 | 1028 | 1029 | stroke 1030 | 1031 | CornerRadius 1032 | 5 1033 | Pattern 1034 | 1 1035 | 1036 | 1037 | Text 1038 | 1039 | Text 1040 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1041 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 1042 | {\colortbl;\red255\green255\blue255;} 1043 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1044 | 1045 | \f0\fs24 \cf0 Session} 1046 | 1047 | 1048 | 1049 | Class 1050 | LineGraphic 1051 | Head 1052 | 1053 | ID 1054 | 51 1055 | 1056 | ID 1057 | 56 1058 | Points 1059 | 1060 | {435.51496887207031, 275.89721106790671} 1061 | {435.51496887207031, 308} 1062 | 1063 | Style 1064 | 1065 | stroke 1066 | 1067 | HeadArrow 1068 | StickArrow 1069 | Legacy 1070 | 1071 | TailArrow 1072 | 0 1073 | 1074 | 1075 | Tail 1076 | 1077 | ID 1078 | 52 1079 | Info 1080 | 1 1081 | 1082 | 1083 | 1084 | Class 1085 | LineGraphic 1086 | Head 1087 | 1088 | ID 1089 | 54 1090 | Info 1091 | 4 1092 | 1093 | ID 1094 | 55 1095 | Points 1096 | 1097 | {491.14317147606727, 249.5} 1098 | {540.93655946386718, 249.5} 1099 | 1100 | Style 1101 | 1102 | stroke 1103 | 1104 | HeadArrow 1105 | StickArrow 1106 | Legacy 1107 | 1108 | TailArrow 1109 | 0 1110 | 1111 | 1112 | Tail 1113 | 1114 | ID 1115 | 52 1116 | Info 1117 | 3 1118 | 1119 | 1120 | 1121 | Bounds 1122 | {{540.93656921386719, 231}, {39, 37}} 1123 | Class 1124 | ShapedGraphic 1125 | ID 1126 | 54 1127 | Magnets 1128 | 1129 | {0, 1} 1130 | {0, -1} 1131 | {1, 0} 1132 | {-1, 0} 1133 | 1134 | Shape 1135 | Circle 1136 | Style 1137 | 1138 | fill 1139 | 1140 | Color 1141 | 1142 | b 1143 | 0 1144 | g 1145 | 0.941483 1146 | r 1147 | 1 1148 | 1149 | 1150 | stroke 1151 | 1152 | CornerRadius 1153 | 5 1154 | 1155 | 1156 | Text 1157 | 1158 | Text 1159 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1160 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 1161 | {\colortbl;\red255\green255\blue255;} 1162 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1163 | 1164 | \f0\fs24 \cf0 401} 1165 | 1166 | 1167 | 1168 | Class 1169 | LineGraphic 1170 | Head 1171 | 1172 | ID 1173 | 52 1174 | Info 1175 | 2 1176 | 1177 | ID 1178 | 53 1179 | Points 1180 | 1181 | {435.51497459411621, 191} 1182 | {435.51496887207031, 223.1027889320932} 1183 | 1184 | Style 1185 | 1186 | stroke 1187 | 1188 | HeadArrow 1189 | StickArrow 1190 | Legacy 1191 | 1192 | TailArrow 1193 | 0 1194 | 1195 | 1196 | Tail 1197 | 1198 | ID 1199 | 6 1200 | 1201 | 1202 | 1203 | Bounds 1204 | {{378.51496887207031, 223}, {114, 53}} 1205 | Class 1206 | ShapedGraphic 1207 | ID 1208 | 52 1209 | Magnets 1210 | 1211 | {0, 1} 1212 | {0, -1} 1213 | {1, 0} 1214 | {-1, 0} 1215 | 1216 | Shape 1217 | Diamond 1218 | Style 1219 | 1220 | fill 1221 | 1222 | Color 1223 | 1224 | b 1225 | 0 1226 | g 1227 | 0.941483 1228 | r 1229 | 1 1230 | 1231 | 1232 | stroke 1233 | 1234 | Cap 1235 | 0 1236 | CornerRadius 1237 | 1 1238 | Join 1239 | 0 1240 | 1241 | 1242 | Text 1243 | 1244 | Text 1245 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1246 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1247 | {\colortbl;\red255\green255\blue255;} 1248 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1249 | 1250 | \f0\fs24 \cf0 check\ 1251 | Signature} 1252 | 1253 | 1254 | 1255 | Bounds 1256 | {{408.51496887207031, 308}, {54, 37}} 1257 | Class 1258 | ShapedGraphic 1259 | ID 1260 | 51 1261 | Magnets 1262 | 1263 | {0, 1} 1264 | {0, -1} 1265 | {1, 0} 1266 | {-1, 0} 1267 | 1268 | Shape 1269 | Rectangle 1270 | Style 1271 | 1272 | fill 1273 | 1274 | Color 1275 | 1276 | b 1277 | 0 1278 | g 1279 | 0.941483 1280 | r 1281 | 1 1282 | 1283 | 1284 | stroke 1285 | 1286 | CornerRadius 1287 | 5 1288 | 1289 | 1290 | Text 1291 | 1292 | Text 1293 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1294 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;\f1\fswiss\fcharset0 Helvetica;} 1295 | {\colortbl;\red255\green255\blue255;} 1296 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1297 | 1298 | \f0\fs24 \cf0 Parse\ 1299 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1300 | 1301 | \f1 \cf0 XML} 1302 | 1303 | 1304 | 1305 | Class 1306 | LineGraphic 1307 | ID 1308 | 50 1309 | Points 1310 | 1311 | {119.3655371817884, 162.5} 1312 | {82.693905830383301, 162.5} 1313 | 1314 | Style 1315 | 1316 | stroke 1317 | 1318 | HeadArrow 1319 | StickArrow 1320 | Legacy 1321 | 1322 | TailArrow 1323 | 0 1324 | 1325 | 1326 | 1327 | 1328 | Class 1329 | LineGraphic 1330 | ID 1331 | 49 1332 | Points 1333 | 1334 | {85.022273999635502, 149.5} 1335 | {121.6939058303833, 149.5} 1336 | 1337 | Style 1338 | 1339 | stroke 1340 | 1341 | HeadArrow 1342 | StickArrow 1343 | Legacy 1344 | 1345 | TailArrow 1346 | 0 1347 | 1348 | 1349 | 1350 | 1351 | Class 1352 | LineGraphic 1353 | ID 1354 | 48 1355 | Points 1356 | 1357 | {250.03723956673701, 162.5} 1358 | {213.36560821533197, 162.5} 1359 | 1360 | Style 1361 | 1362 | stroke 1363 | 1364 | HeadArrow 1365 | StickArrow 1366 | Legacy 1367 | 1368 | TailArrow 1369 | 0 1370 | 1371 | 1372 | 1373 | 1374 | Class 1375 | LineGraphic 1376 | ID 1377 | 46 1378 | Points 1379 | 1380 | {215.69397638458429, 149.5} 1381 | {252.36560821533203, 149.5} 1382 | 1383 | Style 1384 | 1385 | stroke 1386 | 1387 | HeadArrow 1388 | StickArrow 1389 | Legacy 1390 | 1391 | TailArrow 1392 | 0 1393 | 1394 | 1395 | 1396 | 1397 | Bounds 1398 | {{404.01497459411621, 128}, {63, 63}} 1399 | Class 1400 | ShapedGraphic 1401 | ID 1402 | 6 1403 | Magnets 1404 | 1405 | {0, 1} 1406 | {0, -1} 1407 | {1, 0} 1408 | {-1, 0} 1409 | 1410 | Shape 1411 | Rectangle 1412 | Style 1413 | 1414 | fill 1415 | 1416 | Color 1417 | 1418 | b 1419 | 1 1420 | g 1421 | 0.179831 1422 | r 1423 | 0.863737 1424 | 1425 | 1426 | stroke 1427 | 1428 | CornerRadius 1429 | 5 1430 | 1431 | 1432 | Text 1433 | 1434 | Text 1435 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1436 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1437 | {\colortbl;\red255\green255\blue255;} 1438 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1439 | 1440 | \f0\fs24 \cf0 Wechat\ 1441 | Robot} 1442 | 1443 | 1444 | 1445 | Bounds 1446 | {{130.02975845336914, 118.5}, {72, 72}} 1447 | Class 1448 | ShapedGraphic 1449 | ID 1450 | 5 1451 | Magnets 1452 | 1453 | {0, 1} 1454 | {0, -1} 1455 | {1, 0} 1456 | {-1, 0} 1457 | 1458 | Shape 1459 | Circle 1460 | Style 1461 | 1462 | fill 1463 | 1464 | Color 1465 | 1466 | b 1467 | 0 1468 | g 1469 | 0.423462 1470 | r 1471 | 1 1472 | 1473 | 1474 | stroke 1475 | 1476 | CornerRadius 1477 | 5 1478 | 1479 | 1480 | Text 1481 | 1482 | Text 1483 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1484 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;\f1\fswiss\fcharset0 Helvetica;} 1485 | {\colortbl;\red255\green255\blue255;} 1486 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1487 | 1488 | \f0\fs24 \cf0 Ten 1489 | \f1 cent\ 1490 | Server} 1491 | 1492 | 1493 | 1494 | Bounds 1495 | {{19.693939208984375, 127.5}, {53, 53}} 1496 | Class 1497 | ShapedGraphic 1498 | ID 1499 | 4 1500 | Magnets 1501 | 1502 | {0, 1} 1503 | {0, -1} 1504 | {1, 0} 1505 | {-1, 0} 1506 | 1507 | Shape 1508 | Rectangle 1509 | Style 1510 | 1511 | fill 1512 | 1513 | Color 1514 | 1515 | b 1516 | 0.217809 1517 | g 1518 | 1 1519 | r 1520 | 0.303537 1521 | 1522 | 1523 | stroke 1524 | 1525 | CornerRadius 1526 | 5 1527 | 1528 | 1529 | Text 1530 | 1531 | Text 1532 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1533 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 1534 | {\colortbl;\red255\green255\blue255;} 1535 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1536 | 1537 | \f0\fs24 \cf0 User\ 1538 | Phone} 1539 | 1540 | 1541 | 1542 | Bounds 1543 | {{227.01496887207031, 448.5}, {417, 58.5}} 1544 | Class 1545 | ShapedGraphic 1546 | ID 1547 | 66 1548 | Magnets 1549 | 1550 | {0, 1} 1551 | {0, -1} 1552 | {1, 0} 1553 | {-1, 0} 1554 | 1555 | Shape 1556 | Rectangle 1557 | Style 1558 | 1559 | fill 1560 | 1561 | Draws 1562 | NO 1563 | 1564 | stroke 1565 | 1566 | CornerRadius 1567 | 5 1568 | 1569 | 1570 | 1571 | 1572 | GridInfo 1573 | 1574 | HPages 1575 | 2 1576 | KeepToScale 1577 | 1578 | Layers 1579 | 1580 | 1581 | Lock 1582 | NO 1583 | Name 1584 | 图层 1 1585 | Print 1586 | YES 1587 | View 1588 | YES 1589 | 1590 | 1591 | LayoutInfo 1592 | 1593 | Animate 1594 | NO 1595 | circoMinDist 1596 | 18 1597 | circoSeparation 1598 | 0.0 1599 | layoutEngine 1600 | dot 1601 | neatoSeparation 1602 | 0.0 1603 | twopiSeparation 1604 | 0.0 1605 | 1606 | Orientation 1607 | 2 1608 | PrintOnePage 1609 | 1610 | RowAlign 1611 | 1 1612 | RowSpacing 1613 | 36 1614 | SheetTitle 1615 | 版面 1 1616 | UniqueID 1617 | 1 1618 | VPages 1619 | 1 1620 | 1621 | 1622 | ActiveLayerIndex 1623 | 0 1624 | AutoAdjust 1625 | 1626 | BackgroundGraphic 1627 | 1628 | Bounds 1629 | {{0, 0}, {559, 783}} 1630 | Class 1631 | SolidGraphic 1632 | ID 1633 | 2 1634 | Style 1635 | 1636 | shadow 1637 | 1638 | Draws 1639 | NO 1640 | 1641 | stroke 1642 | 1643 | Draws 1644 | NO 1645 | 1646 | 1647 | 1648 | BaseZoom 1649 | 0 1650 | CanvasOrigin 1651 | {0, 0} 1652 | ColumnAlign 1653 | 1 1654 | ColumnSpacing 1655 | 36 1656 | DisplayScale 1657 | 1 0/72 in = 1.0000 in 1658 | GraphicsList 1659 | 1660 | 1661 | Bounds 1662 | {{93, 270}, {73, 54}} 1663 | Class 1664 | ShapedGraphic 1665 | ID 1666 | 4 1667 | Magnets 1668 | 1669 | {0, 1} 1670 | {0, -1} 1671 | {1, 0} 1672 | {-1, 0} 1673 | 1674 | Shape 1675 | Rectangle 1676 | Style 1677 | 1678 | stroke 1679 | 1680 | CornerRadius 1681 | 5 1682 | 1683 | 1684 | Text 1685 | 1686 | Text 1687 | {\rtf1\ansi\ansicpg936\cocoartf1265\cocoasubrtf200 1688 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset134 STHeitiSC-Light;} 1689 | {\colortbl;\red255\green255\blue255;} 1690 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 1691 | 1692 | \f0\fs24 \cf0 \'d7\'d4\'b6\'af\'bb\'d8\'b8\'b4} 1693 | 1694 | 1695 | 1696 | GridInfo 1697 | 1698 | HPages 1699 | 1 1700 | KeepToScale 1701 | 1702 | Layers 1703 | 1704 | 1705 | Lock 1706 | NO 1707 | Name 1708 | 图层 1 1709 | Print 1710 | YES 1711 | View 1712 | YES 1713 | 1714 | 1715 | LayoutInfo 1716 | 1717 | Animate 1718 | NO 1719 | circoMinDist 1720 | 18 1721 | circoSeparation 1722 | 0.0 1723 | layoutEngine 1724 | dot 1725 | neatoSeparation 1726 | 0.0 1727 | twopiSeparation 1728 | 0.0 1729 | 1730 | Orientation 1731 | 2 1732 | PrintOnePage 1733 | 1734 | RowAlign 1735 | 1 1736 | RowSpacing 1737 | 36 1738 | SheetTitle 1739 | 版面 2 1740 | UniqueID 1741 | 2 1742 | VPages 1743 | 1 1744 | 1745 | 1746 | ActiveLayerIndex 1747 | 0 1748 | AutoAdjust 1749 | 1750 | BackgroundGraphic 1751 | 1752 | Bounds 1753 | {{0, 0}, {559, 783}} 1754 | Class 1755 | SolidGraphic 1756 | ID 1757 | 2 1758 | Style 1759 | 1760 | shadow 1761 | 1762 | Draws 1763 | NO 1764 | 1765 | stroke 1766 | 1767 | Draws 1768 | NO 1769 | 1770 | 1771 | 1772 | BaseZoom 1773 | 0 1774 | CanvasOrigin 1775 | {0, 0} 1776 | ColumnAlign 1777 | 1 1778 | ColumnSpacing 1779 | 36 1780 | DisplayScale 1781 | 1 0/72 in = 1.0000 in 1782 | GraphicsList 1783 | 1784 | GridInfo 1785 | 1786 | HPages 1787 | 1 1788 | KeepToScale 1789 | 1790 | Layers 1791 | 1792 | 1793 | Lock 1794 | NO 1795 | Name 1796 | 图层 1 1797 | Print 1798 | YES 1799 | View 1800 | YES 1801 | 1802 | 1803 | LayoutInfo 1804 | 1805 | Animate 1806 | NO 1807 | circoMinDist 1808 | 18 1809 | circoSeparation 1810 | 0.0 1811 | layoutEngine 1812 | dot 1813 | neatoSeparation 1814 | 0.0 1815 | twopiSeparation 1816 | 0.0 1817 | 1818 | Orientation 1819 | 2 1820 | PrintOnePage 1821 | 1822 | RowAlign 1823 | 1 1824 | RowSpacing 1825 | 36 1826 | SheetTitle 1827 | 版面 3 1828 | UniqueID 1829 | 3 1830 | VPages 1831 | 1 1832 | 1833 | 1834 | SmartAlignmentGuidesActive 1835 | YES 1836 | SmartDistanceGuidesActive 1837 | YES 1838 | UseEntirePage 1839 | 1840 | WindowInfo 1841 | 1842 | CurrentSheet 1843 | 2 1844 | ExpandedCanvases 1845 | 1846 | Frame 1847 | {{311, -3}, {836, 746}} 1848 | ListView 1849 | 1850 | OutlineWidth 1851 | 142 1852 | RightSidebar 1853 | 1854 | ShowRuler 1855 | 1856 | Sidebar 1857 | 1858 | SidebarWidth 1859 | 120 1860 | VisibleRegion 1861 | {{-71, 0}, {701, 591}} 1862 | Zoom 1863 | 1 1864 | ZoomValues 1865 | 1866 | 1867 | 版面 1 1868 | 1 1869 | 1.3799999952316284 1870 | 1871 | 1872 | 版面 2 1873 | 1 1874 | 1 1875 | 1876 | 1877 | 版面 3 1878 | 1 1879 | 1 1880 | 1881 | 1882 | 1883 | 1884 | 1885 | -------------------------------------------------------------------------------- /figures/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/wechat/cb03e912fdc2d21178f6c748b537e809b4345048/figures/wechat.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var wechat = require('./lib/wechat'); 2 | // 等待回复 3 | wechat.List = require('./lib/list'); 4 | // 事件 5 | wechat.Event = require('./lib/events'); 6 | 7 | module.exports = wechat; 8 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | var Event = function () { 2 | this.events = {}; 3 | }; 4 | 5 | Event.prototype.add = function (event, callback) { 6 | this.events[event] = callback; 7 | return this; 8 | }; 9 | 10 | Event.prototype._dispatch = function (message, req, res, next) { 11 | if (this.events[message.Event]) { 12 | this.events[message.Event](message, req, res, next); 13 | } else { 14 | next(); 15 | } 16 | }; 17 | 18 | /** 19 | * 分发消息 20 | * ``` 21 | * var Event = require('wechat').Event; 22 | * var events = new Event(); 23 | * events.add('pic_weixin', function (message, req, res, next) { 24 | * // 弹出微信相册发图器的事件推送 25 | * }); 26 | * var handle = Event.dispatch(events); 27 | * app.use('/wechat', wechat(config).event(handle).middlewarify()); 28 | * ``` 29 | */ 30 | Event.dispatch = function (event) { 31 | return function (message, req, res, next) { 32 | // message为事件内容 33 | // { ToUserName: 'gh_d3e07d51b513', 34 | // FromUserName: 'oPKu7jgOibOA-De4u8J2RuNKpZRw', 35 | // CreateTime: '1359125022', 36 | // MsgType: 'event', 37 | // Event: 'LOCATION', 38 | // Latitude: '23.137466', 39 | // Longitude: '113.352425', 40 | // Precision: '119.385040', 41 | // MsgId: '5837397520665436492' } 42 | event._dispatch(message, req, res, next); 43 | }; 44 | }; 45 | 46 | module.exports = Event; 47 | -------------------------------------------------------------------------------- /lib/list.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | /*! 3 | * 缓存列表 4 | */ 5 | var listCache = {}; 6 | 7 | /** 8 | * 回复列表类型 9 | */ 10 | var List = function () { 11 | this.map = {}; 12 | }; 13 | 14 | /** 15 | * 从List对象中根据key取出对应的handler 16 | * @param {String} key 列表中的关键词 17 | */ 18 | List.prototype.get = function (key) { 19 | return this.map[key]; 20 | }; 21 | 22 | /** 23 | * 静态方法,根据items生成List对象,并放置到缓存中 24 | * @param {String} name 列表名字 25 | * @param {Array} items 元素列表 26 | * @param {String} head 回复开头 27 | * @param {String} delimiter 回复分隔符 28 | * @param {String} foot 回复底部 29 | */ 30 | List.add = function (name, items, head, delimiter, foot) { 31 | var description = []; 32 | var list = new List(); 33 | list.name = name; 34 | items.forEach(function (item) { 35 | var text = item[0]; 36 | // 抽取出key,并关联上对应的handle 37 | var replaced = text.replace(/\{(.*)\}/, function (match, key) { 38 | list.map[key] = item[1]; 39 | return key; 40 | }); 41 | description.push(replaced); 42 | }); 43 | 44 | if (delimiter) { 45 | var lists = description.join('\n' + delimiter + '\n'); 46 | list.description = util.format('%s\n%s\n%s', head || '', lists, (foot || '')); 47 | } else { 48 | list.description = description.join('\n'); 49 | } 50 | listCache[name] = list; 51 | }; 52 | 53 | /** 54 | * 静态方法,从缓存中根据名字取出List对象 55 | * @param {String} name 列表名字 56 | */ 57 | List.get = function (name) { 58 | return listCache[name]; 59 | }; 60 | 61 | /** 62 | * 静态方法,清空缓存的所有的List对象 63 | * @param {String} name 列表名字 64 | */ 65 | List.clear = function () { 66 | listCache = {}; 67 | }; 68 | 69 | module.exports = List; 70 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session构造函数,用于与Connect的Session中间件集成的会话脚本 3 | * @param {String} id Session ID 4 | * @param {Object} req Connect中的请求对象 5 | * @param {Object} data 可选的其余数据,将被合并进Session对象中 6 | */ 7 | var Session = function (id, req, data) { 8 | Object.defineProperty(this, 'id', { value: id }); 9 | Object.defineProperty(this, 'req', { value: req }); 10 | if (data) { 11 | for (var key in data) { 12 | this[key] = data[key]; 13 | } 14 | } 15 | }; 16 | 17 | /** 18 | * 保存Session对象到实际的存储中 19 | * 20 | * Callback: 21 | * 22 | * - `err`, 错误对象,保存发生错误时传入 23 | * @param {Function} callback 保存Session的回调函数 24 | */ 25 | Session.prototype.save = function (callback) { 26 | this.req.sessionStore.set(this.id, this, callback || function(){}); 27 | return this; 28 | }; 29 | 30 | /** 31 | * 销毁Session对象 32 | * 33 | * Callback: 34 | * 35 | * - `err`, 错误对象,删除发生错误时传入 36 | * @param {Function} callback 从存储中删除Session数据后的回调函数 37 | */ 38 | Session.prototype.destroy = function (callback) { 39 | delete this.req.wxsession; 40 | this.req.sessionStore.destroy(this.id, callback); 41 | return this; 42 | }; 43 | 44 | module.exports = Session; 45 | -------------------------------------------------------------------------------- /lib/wechat.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var xml2js = require('xml2js'); 3 | var ejs = require('ejs'); 4 | var Session = require('./session'); 5 | var List = require('./list'); 6 | var WXBizMsgCrypt = require('wechat-crypto'); 7 | 8 | /** 9 | * 检查签名 10 | */ 11 | var checkSignature = function (query, token) { 12 | var signature = query.signature; 13 | var timestamp = query.timestamp; 14 | var nonce = query.nonce; 15 | 16 | var shasum = crypto.createHash('sha1'); 17 | var arr = [token, timestamp, nonce].sort(); 18 | shasum.update(arr.join('')); 19 | 20 | return shasum.digest('hex') === signature; 21 | }; 22 | 23 | /*! 24 | * 响应模版 25 | */ 26 | var tpl = ['', 27 | ']]>', 28 | ']]>', 29 | '<%=createTime%>', 30 | '<% if (msgType === "device_event" && (Event === "subscribe_status" || Event === "unsubscribe_status")) { %>', 31 | '<% if (Event === "subscribe_status" || Event === "unsubscribe_status") { %>', 32 | '', 33 | '<%=DeviceStatus%>', 34 | '<% } else { %>', 35 | ']]>', 36 | ']]>', 37 | '<% } %>', 38 | '<% } else { %>', 39 | ']]>', 40 | '<% } %>', 41 | '<% if (msgType === "news") { %>', 42 | '<%=content.length%>', 43 | '', 44 | '<% content.forEach(function(item){ %>', 45 | '', 46 | '<![CDATA[<%-item.title%>]]>', 47 | ']]>', 48 | ']]>', 49 | ']]>', 50 | '', 51 | '<% }); %>', 52 | '', 53 | '<% } else if (msgType === "music") { %>', 54 | '', 55 | '<![CDATA[<%-content.title%>]]>', 56 | ']]>', 57 | ']]>', 58 | ']]>', 59 | '<% if (content.thumbMediaId) { %> ', 60 | ']]>', 61 | '<% } %>', 62 | '', 63 | '<% } else if (msgType === "voice") { %>', 64 | '', 65 | ']]>', 66 | '', 67 | '<% } else if (msgType === "image") { %>', 68 | '', 69 | ']]>', 70 | '', 71 | '<% } else if (msgType === "video") { %>', 72 | '', 77 | '<% } else if (msgType === "hardware") { %>', 78 | '', 79 | ']]>', 80 | ']]>', 81 | '', 82 | '0', 83 | '<% } else if (msgType === "device_text" || msgType === "device_event") { %>', 84 | ']]>', 85 | ']]>', 86 | '<% if (msgType === "device_text") { %>', 87 | ']]>', 88 | '<% } else if ((msgType === "device_event" && Event != "subscribe_status" && Event != "unsubscribe_status")) { %>', 89 | ']]>', 90 | ']]>', 91 | '<% } %>', 92 | '<%=SessionID%>', 93 | '<% } else if (msgType === "transfer_customer_service") { %>', 94 | '<% if (content && content.kfAccount) { %>', 95 | '', 96 | ']]>', 97 | '', 98 | '<% } %>', 99 | '<% } else { %>', 100 | ']]>', 101 | '<% } %>', 102 | ''].join(''); 103 | 104 | /*! 105 | * 编译过后的模版 106 | */ 107 | var compiled = ejs.compile(tpl); 108 | 109 | var wrapTpl = '' + 110 | ']]>' + 111 | ']]>' + 112 | '<%-timestamp%>' + 113 | ']]>' + 114 | ''; 115 | 116 | var encryptWrap = ejs.compile(wrapTpl); 117 | 118 | var load = function (stream, callback) { 119 | // support content-type 'text/xml' using 'express-xml-bodyparser', which set raw xml string 120 | // to 'req.rawBody'(while latest body-parser no longer set req.rawBody), see 121 | // https://github.com/macedigital/express-xml-bodyparser/blob/master/lib/types/xml.js#L79 122 | if (stream.rawBody) { 123 | callback(null, stream.rawBody); 124 | return; 125 | } 126 | 127 | var buffers = []; 128 | stream.on('data', function (trunk) { 129 | buffers.push(trunk); 130 | }); 131 | stream.on('end', function () { 132 | callback(null, Buffer.concat(buffers)); 133 | }); 134 | stream.once('error', callback); 135 | }; 136 | 137 | /*! 138 | * 从微信的提交中提取XML文件 139 | */ 140 | var getMessage = function (stream, callback) { 141 | load(stream, function (err, buf) { 142 | if (err) { 143 | return callback(err); 144 | } 145 | var xml = buf.toString('utf-8'); 146 | stream.weixin_xml = xml; 147 | xml2js.parseString(xml, {trim: true}, callback); 148 | }); 149 | }; 150 | 151 | /*! 152 | * 将xml2js解析出来的对象转换成直接可访问的对象 153 | */ 154 | var formatMessage = function (result) { 155 | var message = {}; 156 | if (typeof result === 'object') { 157 | for (var key in result) { 158 | if (!(result[key] instanceof Array) || result[key].length === 0) { 159 | continue; 160 | } 161 | if (result[key].length === 1) { 162 | var val = result[key][0]; 163 | if (typeof val === 'object') { 164 | message[key] = formatMessage(val); 165 | } else { 166 | message[key] = (val || '').trim(); 167 | } 168 | } else { 169 | message[key] = []; 170 | result[key].forEach(function (item) { 171 | message[key].push(formatMessage(item)); 172 | }); 173 | } 174 | } 175 | return message; 176 | } else { 177 | return result; 178 | } 179 | }; 180 | 181 | /*! 182 | * 将内容回复给微信的封装方法 183 | */ 184 | var reply = function (content, fromUsername, toUsername, message) { 185 | var info = {}; 186 | var type = 'text'; 187 | info.content = content || ''; 188 | info.createTime = new Date().getTime(); 189 | if (message && (message.MsgType === 'device_text' || message.MsgType === 'device_event')) { 190 | info.DeviceType = message.DeviceType; 191 | info.DeviceID = message.DeviceID; 192 | info.SessionID = isNaN(message.SessionID) ? 0 : message.SessionID; 193 | info.createTime = Math.floor(info.createTime / 1000); 194 | if (message['Event'] === 'subscribe_status' || message['Event'] === 'unsubscribe_status') { 195 | delete info.content; 196 | info.DeviceStatus = isNaN(content) ? 0 : content; 197 | } else { 198 | if (!(content instanceof Buffer)) { 199 | content = String(content); 200 | } 201 | info.content = new Buffer(content).toString('base64'); 202 | } 203 | type = message.MsgType; 204 | if (message.MsgType === 'device_event') { 205 | info['Event'] = message['Event']; 206 | } 207 | } else if (Array.isArray(content)) { 208 | type = 'news'; 209 | } else if (typeof content === 'object') { 210 | if (content.hasOwnProperty('type')) { 211 | type = content.type; 212 | if (content.content) { 213 | info.content = content.content; 214 | } 215 | if (content.HardWare) { 216 | info.HardWare = content.HardWare; 217 | } 218 | } else { 219 | type = 'music'; 220 | } 221 | } 222 | info.msgType = type; 223 | info.toUsername = toUsername; 224 | info.fromUsername = fromUsername; 225 | return compiled(info); 226 | }; 227 | 228 | var reply2CustomerService = function (fromUsername, toUsername, kfAccount) { 229 | var info = {}; 230 | info.msgType = 'transfer_customer_service'; 231 | info.createTime = new Date().getTime(); 232 | info.toUsername = toUsername; 233 | info.fromUsername = fromUsername; 234 | info.content = {}; 235 | if (typeof kfAccount === 'string') { 236 | info.content.kfAccount = kfAccount; 237 | } 238 | return compiled(info); 239 | }; 240 | 241 | var respond = function (handler) { 242 | return function (req, res, next) { 243 | var message = req.weixin; 244 | var callback = handler.getHandler(message.MsgType); 245 | 246 | /** 247 | * 根据条件对返回的XML数据加密 248 | * @param xml 249 | */ 250 | function encryptXml(xml) { 251 | if (!req.query.encrypt_type || req.query.encrypt_type === 'raw') { 252 | return xml; 253 | } else { 254 | // 判断是否已有前置cryptor 255 | var cryptor = req.cryptor || handler.cryptor; 256 | var wrap = {}; 257 | wrap.encrypt = cryptor.encrypt(xml); 258 | wrap.nonce = parseInt((Math.random() * 100000000000), 10); 259 | wrap.timestamp = new Date().getTime(); 260 | wrap.signature = cryptor.getSignature(wrap.timestamp, wrap.nonce, wrap.encrypt); 261 | return encryptWrap(wrap); 262 | } 263 | } 264 | 265 | res.reply = function (content) { 266 | res.writeHead(200); 267 | // 响应空字符串,用于响应慢的情况,避免微信重试 268 | if (!content) { 269 | return res.end(''); 270 | } 271 | 272 | res.end(encryptXml(reply(content, message.ToUserName, message.FromUserName, message))); 273 | }; 274 | 275 | // 响应消息,转到客服模式 276 | res.transfer2CustomerService = function (kfAccount) { 277 | res.writeHead(200); 278 | 279 | res.end(encryptXml(reply2CustomerService(message.ToUserName, message.FromUserName, kfAccount))); 280 | }; 281 | 282 | var done = function () { 283 | // 如果session中有_wait标记 284 | if (message.MsgType === 'text' && req.wxsession && req.wxsession._wait) { 285 | var list = List.get(req.wxsession._wait); 286 | var handle = list.get(message.Content); 287 | var wrapper = function (message) { 288 | return handler.handle ? function(req, res) { 289 | res.reply(message); 290 | } : function (info, req, res) { 291 | res.reply(message); 292 | }; 293 | }; 294 | 295 | // 如果回复命中规则,则用预置的方法回复 296 | if (handle) { 297 | callback = typeof handle === 'string' ? wrapper(handle) : handle; 298 | } 299 | } 300 | 301 | // 兼容旧API 302 | if (handler.handle) { 303 | callback(req, res, next); 304 | } else { 305 | callback(message, req, res, next); 306 | } 307 | }; 308 | 309 | if (req.sessionStore) { 310 | var storage = req.sessionStore; 311 | var _end = res.end; 312 | var openid = message.FromUserName + ':' + message.ToUserName; 313 | res.end = function () { 314 | _end.apply(res, arguments); 315 | if (req.wxsession) { 316 | req.wxsession.save(); 317 | } 318 | }; 319 | // 等待列表 320 | res.wait = function (name, callback) { 321 | var list = List.get(name); 322 | if (list) { 323 | req.wxsession._wait = name; 324 | res.reply(list.description); 325 | } else { 326 | var err = new Error('Undefined list: ' + name); 327 | err.name = 'UndefinedListError'; 328 | res.writeHead(500); 329 | res.end(err.name); 330 | callback && callback(err); 331 | } 332 | }; 333 | 334 | // 清除等待列表 335 | res.nowait = function () { 336 | delete req.wxsession._wait; 337 | res.reply.apply(res, arguments); 338 | }; 339 | 340 | storage.get(openid, function (err, session) { 341 | if (!session) { 342 | req.wxsession = new Session(openid, req); 343 | req.wxsession.cookie = req.session.cookie; 344 | } else { 345 | // add by xjmalm, convert string type to Date in case some of the session storage only stores the simple data types 346 | if (session.cookie && 'string' === typeof session.cookie.expires) { 347 | session.cookie.expires = new Date(session.cookie.expires); 348 | } 349 | 350 | req.wxsession = new Session(openid, req, session); 351 | } 352 | done(); 353 | }); 354 | } else { 355 | done(); 356 | } 357 | }; 358 | }; 359 | 360 | /** 361 | * 微信自动回复平台的内部的Handler对象 362 | * @param {String|Object} config 配置 363 | * @param {Function} handle handle对象 364 | */ 365 | var Handler = function (token, handle) { 366 | if (token) { 367 | this.setToken(token); 368 | } 369 | this.handlers = {}; 370 | this.handle = handle; 371 | }; 372 | 373 | Handler.prototype.setToken = function (token) { 374 | if (typeof token === 'string') { 375 | this.token = token; 376 | this.checkSignature = true; 377 | } else { 378 | this.token = token.token; 379 | this.appid = token.appid; 380 | this.encodingAESKey = token.encodingAESKey; 381 | if (typeof token.checkSignature === 'undefined' || typeof token.checkSignature !== 'boolean') { 382 | this.checkSignature = true; 383 | } else { 384 | this.checkSignature = token.checkSignature 385 | } 386 | } 387 | }; 388 | 389 | /** 390 | * 设置handler对象 391 | * 按消息设置handler对象的快捷方式 392 | * 393 | * - `text(fn)` 394 | * - `image(fn)` 395 | * - `voice(fn)` 396 | * - `video(fn)` 397 | * - `location(fn)` 398 | * - `link(fn)` 399 | * - `event(fn)` 400 | * @param {String} type handler处理的消息类型 401 | * @param {Function} handle handle对象 402 | */ 403 | Handler.prototype.setHandler = function (type, fn) { 404 | this.handlers[type] = fn; 405 | return this; 406 | }; 407 | 408 | ['text', 'image', 'voice', 'video', 'location', 'link', 'event', 'shortvideo', 'hardware', 'device_text', 'device_event'].forEach(function (method) { 409 | Handler.prototype[method] = function (fn) { 410 | return this.setHandler(method, fn); 411 | }; 412 | }); 413 | 414 | /** 415 | * 根据消息类型取出handler对象 416 | * @param {String} type 消息类型 417 | */ 418 | Handler.prototype.getHandler = function (type) { 419 | return this.handle || this.handlers[type] || function (info, req, res, next) { 420 | next(); 421 | }; 422 | }; 423 | 424 | var serveEncrypt = function (that, req, res, next, _respond) { 425 | var method = req.method; 426 | // 加密模式 427 | var signature = req.query.msg_signature; 428 | var timestamp = req.query.timestamp; 429 | var nonce = req.query.nonce; 430 | 431 | // 判断是否已有前置cryptor 432 | var cryptor = req.cryptor || that.cryptor; 433 | 434 | if (method === 'GET') { 435 | var echostr = req.query.echostr; 436 | if (signature !== cryptor.getSignature(timestamp, nonce, echostr)) { 437 | res.writeHead(401); 438 | res.end('Invalid signature'); 439 | return; 440 | } 441 | var result = cryptor.decrypt(echostr); 442 | // TODO 检查appId的正确性 443 | res.writeHead(200); 444 | res.end(result.message); 445 | } else if (method === 'POST') { 446 | load(req, function (err, buf) { 447 | if (err) { 448 | return next(err); 449 | } 450 | var xml = buf.toString('utf-8'); 451 | if (!xml) { 452 | var emptyErr = new Error('body is empty'); 453 | emptyErr.name = 'Wechat'; 454 | return next(emptyErr); 455 | } 456 | xml2js.parseString(xml, {trim: true}, function (err, result) { 457 | if (err) { 458 | err.name = 'BadMessage' + err.name; 459 | return next(err); 460 | } 461 | var xml = formatMessage(result.xml); 462 | var encryptMessage = xml.Encrypt; 463 | if (signature !== cryptor.getSignature(timestamp, nonce, encryptMessage)) { 464 | res.writeHead(401); 465 | res.end('Invalid signature'); 466 | return; 467 | } 468 | var decrypted = cryptor.decrypt(encryptMessage); 469 | var messageWrapXml = decrypted.message; 470 | if (messageWrapXml === '') { 471 | res.writeHead(401); 472 | res.end('Invalid appid'); 473 | return; 474 | } 475 | req.weixin_xml = messageWrapXml; 476 | xml2js.parseString(messageWrapXml, {trim: true}, function (err, result) { 477 | if (err) { 478 | err.name = 'BadMessage' + err.name; 479 | return next(err); 480 | } 481 | req.weixin = formatMessage(result.xml); 482 | _respond(req, res, next); 483 | }); 484 | }); 485 | }); 486 | } else { 487 | res.writeHead(501); 488 | res.end('Not Implemented'); 489 | } 490 | }; 491 | 492 | /** 493 | * 根据Handler对象生成响应方法,并最终生成中间件函数 494 | */ 495 | Handler.prototype.middlewarify = function () { 496 | var that = this; 497 | if (this.encodingAESKey) { 498 | that.cryptor = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid); 499 | } 500 | var token = this.token; 501 | var _respond = respond(this); 502 | return function (req, res, next) { 503 | // 如果已经解析过了,调用相关handle处理 504 | if (req.weixin) { 505 | _respond(req, res, next); 506 | return; 507 | } 508 | if (req.query.encrypt_type && req.query.msg_signature) { 509 | serveEncrypt(that, req, res, next, _respond); 510 | } else { 511 | var method = req.method; 512 | // 动态token,在前置中间件中设置该值req.wechat_token,优先选用 513 | if (that.checkSignature && !checkSignature(req.query, req.wechat_token || token)) { 514 | res.writeHead(401); 515 | res.end('Invalid signature'); 516 | return; 517 | } 518 | if (method === 'GET') { 519 | res.writeHead(200); 520 | res.end(req.query.echostr); 521 | } else if (method === 'POST') { 522 | getMessage(req, function (err, result) { 523 | if (err) { 524 | err.name = 'BadMessage' + err.name; 525 | return next(err); 526 | } 527 | req.weixin = formatMessage(result.xml); 528 | _respond(req, res, next); 529 | }); 530 | } else { 531 | res.writeHead(501); 532 | res.end('Not Implemented'); 533 | } 534 | } 535 | }; 536 | }; 537 | 538 | /** 539 | * 根据口令 540 | * 541 | * Examples: 542 | * 使用wechat作为自动回复中间件的三种方式 543 | * ``` 544 | * wechat(token, function (req, res, next) {}); 545 | * 546 | * wechat(token, wechat.text(function (message, req, res, next) { 547 | * // TODO 548 | * }).location(function (message, req, res, next) { 549 | * // TODO 550 | * })); 551 | * 552 | * wechat(token) 553 | * .text(function (message, req, res, next) { 554 | * // TODO 555 | * }).location(function (message, req, res, next) { 556 | * // TODO 557 | * }).middlewarify(); 558 | * ``` 559 | * 加密模式下token为config 560 | * 561 | * ``` 562 | * var config = { 563 | * token: 'token', 564 | * appid: 'appid', 565 | * encodingAESKey: 'encodinAESKey' 566 | * }; 567 | * wechat(config, function (req, res, next) {}); 568 | * ``` 569 | * 570 | * 静态方法 571 | * 572 | * - `text`,处理文字推送的回调函数,接受参数为(text, req, res, next)。 573 | * - `image`,处理图片推送的回调函数,接受参数为(image, req, res, next)。 574 | * - `voice`,处理声音推送的回调函数,接受参数为(voice, req, res, next)。 575 | * - `video`,处理视频推送的回调函数,接受参数为(video, req, res, next)。 576 | * - `location`,处理位置推送的回调函数,接受参数为(location, req, res, next)。 577 | * - `link`,处理链接推送的回调函数,接受参数为(link, req, res, next)。 578 | * - `event`,处理事件推送的回调函数,接受参数为(event, req, res, next)。 579 | * - `shortvideo`,处理短视频推送的回调函数,接受参数为(event, req, res, next)。 580 | * @param {String} token 在微信平台填写的口令 581 | * @param {Function} handle 生成的回调函数,参见示例 582 | */ 583 | var middleware = function (token, handle) { 584 | if (arguments.length === 1) { 585 | return new Handler(token); 586 | } 587 | 588 | if (handle instanceof Handler) { 589 | handle.setToken(token); 590 | return handle.middlewarify(); 591 | } else { 592 | return new Handler(token, handle).middlewarify(); 593 | } 594 | }; 595 | 596 | ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link', 'event'].forEach(function (method) { 597 | middleware[method] = function (fn) { 598 | return (new Handler())[method](fn); 599 | }; 600 | }); 601 | 602 | middleware.toXML = compiled; 603 | middleware.reply = reply; 604 | middleware.reply2CustomerService = reply2CustomerService; 605 | middleware.checkSignature = checkSignature; 606 | 607 | module.exports = middleware; 608 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat", 3 | "version": "2.1.0", 4 | "description": "微信公共平台Node库", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-all" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/node-webot/wechat.git" 12 | }, 13 | "keywords": [ 14 | "weixin", 15 | "wechat" 16 | ], 17 | "dependencies": { 18 | "xml2js": "0.4.17", 19 | "ejs": ">=1.0.0", 20 | "wechat-crypto": "0.0.2" 21 | }, 22 | "devDependencies": { 23 | "supertest": "*", 24 | "mocha": "*", 25 | "should": "^3", 26 | "expect.js": "*", 27 | "connect": "^2", 28 | "travis-cov": "*", 29 | "coveralls": "*", 30 | "mocha-lcov-reporter": "*", 31 | "muk": "*", 32 | "rewire": "*", 33 | "istanbul": "*" 34 | }, 35 | "author": "Jackson Tian", 36 | "license": "MIT", 37 | "readmeFilename": "README.md", 38 | "directories": { 39 | "test": "test" 40 | }, 41 | "files": [ 42 | "lib", 43 | "index.js" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | appid: 'wxc9135aade4e81d57', 3 | appsecret: '0461795e98b8ffde5a212b5098f1b9b6' 4 | }; 5 | -------------------------------------------------------------------------------- /test/events.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var Event = require('../').Event; 3 | 4 | describe('events.js', function () { 5 | describe('event', function () { 6 | it('add event should ok', function () { 7 | var emitter = new Event(); 8 | emitter.add('pic_weixin', function (message, req, res, next) { 9 | // 弹出微信相册发图器的事件推送 10 | }); 11 | emitter.events.should.have.property('pic_weixin'); 12 | }); 13 | 14 | it('_dispatch should ok', function (done) { 15 | var emitter = new Event(); 16 | var count = 0; 17 | emitter.add('pic_weixin', function (message, req, res, next) { 18 | // 弹出微信相册发图器的事件推送 19 | count++; 20 | next(); 21 | }); 22 | var message = {Event: 'pic_weixin'}; 23 | emitter._dispatch(message, {}, {}, function () { 24 | count.should.have.be.equal(1); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('_dispatch miss should ok', function (done) { 30 | var emitter = new Event(); 31 | var count = 0; 32 | var message = {Event: 'pic_weixin'}; 33 | emitter.add('hehe', function (message, req, res, next) { 34 | count++; 35 | next(); 36 | }); 37 | emitter._dispatch(message, {}, {}, function () { 38 | count.should.have.be.equal(0); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('dispatch should ok', function (done) { 44 | var emitter = new Event(); 45 | var count = 0; 46 | var message = {Event: 'pic_weixin'}; 47 | emitter.add('pic_weixin', function (message, req, res, next) { 48 | count++; 49 | next(); 50 | }); 51 | var handle = Event.dispatch(emitter); 52 | handle(message, {}, {}, function () { 53 | count.should.have.be.equal(1); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/get_message.test.js: -------------------------------------------------------------------------------- 1 | var rewire = require('rewire'); 2 | var wechat = rewire('../lib/wechat'); 3 | var Readable = require('stream').Readable; 4 | var util = require('util'); 5 | var should = require('should'); 6 | 7 | function Counter(opt) { 8 | Readable.call(this, opt); 9 | } 10 | util.inherits(Counter, Readable); 11 | 12 | Counter.prototype._read = function() {}; 13 | 14 | var getMessage = wechat.__get__('getMessage'); 15 | 16 | describe('getMessage', function () { 17 | it('should not error', function (done) { 18 | var stream = new Counter(); 19 | getMessage(stream, function (err, xml) { 20 | should.not.exist(err); 21 | done(); 22 | }); 23 | stream.push(new Buffer('')); 24 | stream.push(new Buffer('')); 25 | stream.push(null); 26 | }); 27 | 28 | it('should exist error', function (done) { 29 | var stream = new Counter(); 30 | getMessage(stream, function (err, xml) { 31 | should.exist(err); 32 | done(); 33 | }); 34 | stream.emit('error', new Error('some error')); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/list.test.js: -------------------------------------------------------------------------------- 1 | 2 | var should = require('should'); 3 | var List = require('../').List; 4 | 5 | describe('list.js', function () { 6 | it('should ok', function () { 7 | var common = [ 8 | ['选择{a}查看啥', function () {}], 9 | ['选择{b}查看啥', function () {}], 10 | ['选择{c}查看啥', function () {}] 11 | ]; 12 | List.add('common', common); 13 | var list = List.get('common'); 14 | list.description.should.be.equal('选择a查看啥\n选择b查看啥\n选择c查看啥'); 15 | list.get('a').should.be.equal(common[0][1]); 16 | list.get('b').should.be.equal(common[1][1]); 17 | list.get('c').should.be.equal(common[2][1]); 18 | }); 19 | 20 | it('should ok when clear', function () { 21 | var common = [ 22 | ['选择{a}查看啥', function () {}], 23 | ['选择{b}查看啥', function () {}], 24 | ['选择{c}查看啥', function () {}] 25 | ]; 26 | List.add('common', common); 27 | var list = List.get('common'); 28 | should.exist(list); 29 | List.clear(); 30 | list = List.get('common'); 31 | should.not.exist(list); 32 | }); 33 | 34 | it('should ok with string', function () { 35 | var common = [ 36 | ['welcome'], 37 | ['选择{a}查看啥', function () {}], 38 | ['选择{b}查看啥', function () {}], 39 | ['选择{c}查看啥', function () {}] 40 | ]; 41 | List.add('welcome', common); 42 | var list = List.get('welcome'); 43 | list.description.should.be.equal('welcome\n选择a查看啥\n选择b查看啥\n选择c查看啥'); 44 | list.get('a').should.be.equal(common[1][1]); 45 | list.get('b').should.be.equal(common[2][1]); 46 | list.get('c').should.be.equal(common[3][1]); 47 | }); 48 | 49 | it('should ok with body, foot, delimiter', function() { 50 | var common = [ 51 | ['选择{a}查看啥', function () {}], 52 | ['选择{b}查看啥', function () {}], 53 | ['选择{c}查看啥', function () {}] 54 | ]; 55 | var head = '我是头'; 56 | var delimiter = '-------'; 57 | var foot = '我是小尾巴'; 58 | 59 | List.add('common', common, head, delimiter, foot); 60 | var list = List.get('common'); 61 | list.description.should.be.equal(head + '\n选择a查看啥\n' + delimiter + '\n选择b查看啥\n' + delimiter + '\n选择c查看啥\n' + foot); 62 | list.get('a').should.be.equal(common[0][1]); 63 | list.get('b').should.be.equal(common[1][1]); 64 | list.get('c').should.be.equal(common[2][1]); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/list2.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var List = require('../').List; 3 | 4 | var request = require('supertest'); 5 | var template = require('./support').template; 6 | var tail = require('./support').tail; 7 | 8 | var connect = require('connect'); 9 | var wechat = require('../'); 10 | 11 | var app = connect(); 12 | app.use(connect.query()); 13 | app.use(connect.cookieParser()); 14 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 15 | app.use('/wechat', wechat('some token', function (req, res, next) { 16 | // 微信输入信息都在req.weixin上 17 | var info = req.weixin; 18 | if (info.Content === 'list') { 19 | res.wait('view'); 20 | } 21 | })); 22 | 23 | describe('list', function() { 24 | it('should ok with list', function (done) { 25 | List.add('view', [ 26 | ['回复{c}查看我的性取向', '这样的事情怎么好意思告诉你啦- -'] 27 | ]); 28 | var info = { 29 | sp: 'test', 30 | user: 'test', 31 | type: 'text', 32 | text: 'list' 33 | }; 34 | 35 | request(app) 36 | .post('/wechat' + tail()) 37 | .send(template(info)) 38 | .expect(200) 39 | .end(function (err, res) { 40 | if (err) { 41 | return done(err); 42 | } 43 | 44 | info = { 45 | sp: 'test', 46 | user: 'test', 47 | type: 'text', 48 | text: 'c' 49 | }; 50 | 51 | request(app) 52 | .post('/wechat' + tail()) 53 | .send(template(info)) 54 | .expect(200) 55 | .end(function(err, res){ 56 | if (err) { 57 | return done(err); 58 | } 59 | var body = res.text.toString(); 60 | body.should.include('这样的事情怎么好意思告诉你啦'); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var request = require('supertest'); 4 | var tail = require('./support').tail; 5 | 6 | var connect = require('connect'); 7 | var wechat = require('../'); 8 | var assert = require('assert'); 9 | var xml2js = require('xml2js'); 10 | var rewire = require("rewire"); 11 | var wechatModule = rewire('../lib/wechat.js'); 12 | 13 | var formatMessage = wechatModule.__get__('formatMessage') 14 | 15 | var app = connect(); 16 | app.use(connect.query()); 17 | app.use('/wechat', wechat('some token', function (req, res, next) { 18 | res.writeHead(200); 19 | res.end('hehe'); 20 | })); 21 | app.use(function (err, req, res, next) { 22 | res.statusCode = err.status || 500; 23 | res.end(err.name); 24 | }); 25 | 26 | describe('parse_xml.js', function () { 27 | it('should ok', function (done) { 28 | var xml = '13613748915847060634540564918'; 29 | 30 | request(app) 31 | .post('/wechat' + tail()) 32 | .send(xml) 33 | .expect(200) 34 | .expect('hehe') 35 | .end(done); 36 | }); 37 | 38 | it('should not ok when bad xml', function (done) { 39 | var xml = '13613748915847060634540564918'; 40 | 41 | request(app) 42 | .post('/wechat' + tail()) 43 | .send(xml) 44 | .expect(500) 45 | .expect('BadMessageError') 46 | .end(done); 47 | }); 48 | 49 | it('should return array when xml include repeat item', function(done) { 50 | var xml = ''; 51 | xml2js.parseString(xml, {trim: true}, function(err, result) { 52 | var xml = formatMessage(result.xml); 53 | var items = xml['arraytest']['item']; 54 | 55 | assert((items instanceof Array) == true); 56 | for(var i = 0; i < items.length; i++) { 57 | assert(items[i] == ("item"+i)); 58 | } 59 | 60 | done() 61 | }); 62 | }) 63 | }); 64 | -------------------------------------------------------------------------------- /test/reply.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var reply = require('../').reply; 3 | var reply2CustomerService = require('../').reply2CustomerService; 4 | 5 | describe('wechat.js', function () { 6 | describe('reply text', function () { 7 | it('reply("text") should ok', function () { 8 | var result = reply('hello world', 'from', 'to'); 9 | result.should.be.include(''); 10 | result.should.be.include(''); 11 | result.should.be.include(''); 12 | result.should.be.include(''); 13 | }); 14 | 15 | it('reply({type: "text", content: content}) should ok', function () { 16 | var result = reply({type: 'text', content: 'hello world'}, 'from', 'to'); 17 | result.should.be.include(''); 18 | result.should.be.include(''); 19 | result.should.be.include(''); 20 | result.should.be.include(''); 21 | }); 22 | }); 23 | 24 | describe('reply music', function () { 25 | it('reply(object) should ok', function () { 26 | var result = reply({ 27 | title: "来段音乐吧", 28 | description: "一无所有", 29 | musicUrl: "http://mp3.com/xx.mp3", 30 | hqMusicUrl: "http://mp3.com/xx.mp3" 31 | }, 'from', 'to'); 32 | result.should.be.include('<![CDATA[来段音乐吧]]>'); 33 | result.should.be.include(''); 34 | result.should.be.include(''); 35 | result.should.be.include(''); 36 | result.should.be.include(''); 37 | result.should.be.include(''); 38 | result.should.be.include(''); 39 | }); 40 | 41 | it('reply(object) with type should ok', function () { 42 | var result = reply({ 43 | type: "music", 44 | content: { 45 | title: "来段音乐吧", 46 | description: "一无所有", 47 | musicUrl: "http://mp3.com/xx.mp3", 48 | hqMusicUrl: "http://mp3.com/xx.mp3" 49 | } 50 | }, 'from', 'to'); 51 | result.should.be.include('<![CDATA[来段音乐吧]]>'); 52 | result.should.be.include(''); 53 | result.should.be.include(''); 54 | result.should.be.include(''); 55 | result.should.be.include(''); 56 | result.should.be.include(''); 57 | result.should.be.include(''); 58 | }); 59 | }); 60 | 61 | describe('reply news', function () { 62 | var news = [ 63 | { 64 | title: '你来我家接我吧', 65 | description: '这是女神与高富帅之间的对话', 66 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 67 | url: 'http://nodeapi.cloudfoundry.com/' 68 | } 69 | ]; 70 | 71 | it('reply(Array) should ok', function () { 72 | var result = reply(news, 'from', 'to'); 73 | result.should.be.include('1'); 74 | result.should.be.include('<![CDATA[你来我家接我吧]]>'); 75 | result.should.be.include(''); 76 | result.should.be.include(''); 77 | result.should.be.include(''); 78 | result.should.be.include(''); 79 | result.should.be.include(''); 80 | result.should.be.include(''); 81 | }); 82 | 83 | it('reply({type: "news", content: news}) should ok', function () { 84 | var result = reply({type: 'news', content: news}, 'from', 'to'); 85 | result.should.be.include('1'); 86 | result.should.be.include('<![CDATA[你来我家接我吧]]>'); 87 | result.should.be.include(''); 88 | result.should.be.include(''); 89 | result.should.be.include(''); 90 | result.should.be.include(''); 91 | result.should.be.include(''); 92 | result.should.be.include(''); 93 | }); 94 | }); 95 | 96 | describe('reply image', function () { 97 | var image = { 98 | mediaId: 'mediaId' 99 | }; 100 | 101 | it('reply({type: "image", content: image}) should ok', function () { 102 | var result = reply({type: 'image', content: image}, 'from', 'to'); 103 | result.should.be.include(''); 104 | result.should.be.include(''); 105 | result.should.be.include(''); 106 | result.should.be.include(''); 107 | }); 108 | }); 109 | 110 | describe('reply voice', function () { 111 | var voice = { 112 | mediaId: 'mediaId' 113 | }; 114 | 115 | it('reply({type: "voice", content: voice}) should ok', function () { 116 | var result = reply({type: 'voice', content: voice}, 'from', 'to'); 117 | result.should.be.include(''); 118 | result.should.be.include(''); 119 | result.should.be.include(''); 120 | result.should.be.include(''); 121 | }); 122 | }); 123 | 124 | describe('reply video', function () { 125 | var video = { 126 | mediaId: 'mediaId', 127 | thumbMediaId: 'thumbMediaId' 128 | }; 129 | 130 | it('reply({type: "video", content: video}) should ok', function () { 131 | var result = reply({type: 'video', content: video}, 'from', 'to'); 132 | result.should.be.include(''); 133 | result.should.be.include(''); 134 | result.should.be.include(''); 135 | result.should.be.include(''); 136 | }); 137 | }); 138 | 139 | describe('reply2CustomerService', function () { 140 | it('reply2CustomerService', function () { 141 | var result = reply2CustomerService('from', 'to'); 142 | result.should.be.include(''); 143 | result.should.be.include(''); 144 | result.should.be.include(''); 145 | result.should.be.not.include(''); 146 | }); 147 | 148 | it('reply2CustomerService with kfAccount', function () { 149 | var result = reply2CustomerService('from', 'to', 'kf'); 150 | result.should.be.include(''); 151 | result.should.be.include(''); 152 | result.should.be.include(''); 153 | result.should.be.include(''); 154 | }); 155 | }); 156 | 157 | describe('reply device text', function () { 158 | it('device text reply("hello from device") should ok', function () { 159 | var MsgType = 'device_text'; 160 | var DeviceType = 'to_user'; 161 | var DeviceID = 'device_id'; 162 | var SessionID = 709394; 163 | var content = 'hello from device'; 164 | var message = { 165 | MsgType: MsgType, 166 | DeviceType: DeviceType, 167 | DeviceID: DeviceID, 168 | SessionID: SessionID, 169 | }; 170 | var result = reply(content, 'from', 'to', message); 171 | result.should.be.include(''); 172 | result.should.be.include(''); 173 | result.should.be.include(''); 174 | result.should.be.include(''); 175 | result.should.be.include(''); 176 | result.should.be.include('' + SessionID + ''); 177 | result.should.be.include(''); 178 | }); 179 | }); 180 | 181 | describe('reply device binding event', function () { 182 | it('device binding event reply("bind") should ok', function () { 183 | var MsgType = 'device_event'; 184 | var Event = 'bind'; 185 | var DeviceType = 'to_user'; 186 | var DeviceID = 'device_id'; 187 | var SessionID = 709394; 188 | var content = 'bind'; 189 | var message = { 190 | MsgType: MsgType, 191 | Event: Event, 192 | DeviceType: DeviceType, 193 | DeviceID: DeviceID, 194 | SessionID: SessionID, 195 | }; 196 | var result = reply(content, 'from', 'to', message); 197 | result.should.be.include(''); 198 | result.should.be.include(''); 199 | result.should.be.include(''); 200 | result.should.be.include(''); 201 | result.should.be.include(''); 202 | result.should.be.include(''); 203 | result.should.be.include('' + SessionID + ''); 204 | result.should.be.include(''); 205 | }); 206 | }); 207 | 208 | describe('reply WIFI device status', function () { 209 | it('WIFI device status reply("bind") should ok', function () { 210 | var MsgType = 'device_event'; 211 | var Event = 'subscribe_status'; 212 | var DeviceType = 'to_user'; 213 | var DeviceID = 'device_id'; 214 | var OpType = 0; 215 | var OpenID = 'open_id'; 216 | var content = 0; 217 | var message = { 218 | MsgType: MsgType, 219 | Event: Event, 220 | DeviceType: DeviceType, 221 | DeviceID: DeviceID, 222 | OpType: OpType, 223 | OpenID: OpenID 224 | }; 225 | var result = reply(content, 'from', 'to', message); 226 | result.should.be.include(''); 227 | result.should.be.include(''); 228 | result.should.be.include(''); 229 | result.should.be.include('' + content + ''); 230 | result.should.be.include(''); 231 | result.should.be.include(''); 232 | }); 233 | }); 234 | 235 | describe('reply social function', function () { 236 | it('social function reply({type: "hardware", HardWare: {MessageView: "myrank", MessageAction: "ranklist"}}) should ok', function () { 237 | var MsgType = 'hardware'; 238 | var view = 'myrank'; 239 | var action = 'ranklist'; 240 | var content = { 241 | type: MsgType, 242 | HardWare:{ 243 | MessageView: view, 244 | MessageAction: action 245 | } 246 | }; 247 | var result = reply(content, 'from', 'to'); 248 | result.should.be.include(''); 249 | result.should.be.include(''); 250 | result.should.be.include(''); 251 | result.should.be.include(''); 252 | result.should.be.include('0'); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /test/session.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var request = require('supertest'); 4 | var template = require('./support').template; 5 | var tail = require('./support').tail; 6 | 7 | var connect = require('connect'); 8 | var wechat = require('../'); 9 | 10 | var app = connect(); 11 | app.use(connect.query()); 12 | app.use(connect.cookieParser()); 13 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 14 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 15 | if (info.Content === '=') { 16 | req.wxsession.text = req.wxsession.text || []; 17 | var exp = req.wxsession.text.join(''); 18 | res.reply('result: ' + eval(exp)); 19 | } else if (info.Content === 'destroy') { 20 | req.wxsession.destroy(); 21 | res.reply('销毁会话'); 22 | } else { 23 | req.wxsession.text = req.wxsession.text || []; 24 | req.wxsession.text.push(info.Content); 25 | res.reply('收到' + info.Content); 26 | } 27 | }))); 28 | 29 | describe('wechat.js', function () { 30 | describe('session', function () { 31 | it('should ok', function (done) { 32 | var info = { 33 | sp: 'nvshen', 34 | user: 'diaosi', 35 | type: 'text', 36 | text: '1' 37 | }; 38 | 39 | request(app) 40 | .post('/wechat' + tail()) 41 | .send(template(info)) 42 | .expect(200) 43 | .end(function(err, res){ 44 | if (err) return done(err); 45 | var body = res.text.toString(); 46 | body.should.include(''); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should ok', function (done) { 52 | var info = { 53 | sp: 'nvshen', 54 | user: 'diaosi', 55 | type: 'text', 56 | text: '+' 57 | }; 58 | 59 | request(app) 60 | .post('/wechat' + tail()) 61 | .send(template(info)) 62 | .expect(200) 63 | .end(function(err, res){ 64 | if (err) return done(err); 65 | var body = res.text.toString(); 66 | body.should.include(''); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should ok', function (done) { 72 | var info = { 73 | sp: 'nvshen', 74 | user: 'diaosi', 75 | type: 'text', 76 | text: '1' 77 | }; 78 | 79 | request(app) 80 | .post('/wechat' + tail()) 81 | .send(template(info)) 82 | .expect(200) 83 | .end(function(err, res){ 84 | if (err) return done(err); 85 | var body = res.text.toString(); 86 | body.should.include(''); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should ok', function (done) { 92 | var info = { 93 | sp: 'nvshen', 94 | user: 'diaosi', 95 | type: 'text', 96 | text: '=' 97 | }; 98 | 99 | request(app) 100 | .post('/wechat' + tail()) 101 | .send(template(info)) 102 | .expect(200) 103 | .end(function(err, res){ 104 | if (err) return done(err); 105 | var body = res.text.toString(); 106 | body.should.include(''); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('should destroy session', function (done) { 112 | var info = { 113 | sp: 'nvshen', 114 | user: 'diaosi', 115 | type: 'text', 116 | text: 'destroy' 117 | }; 118 | 119 | request(app) 120 | .post('/wechat' + tail()) 121 | .send(template(info)) 122 | .expect(200) 123 | .end(function(err, res){ 124 | if (err) return done(err); 125 | var body = res.text.toString(); 126 | body.should.include(''); 127 | var info = { 128 | sp: 'nvshen', 129 | user: 'diaosi', 130 | type: 'text', 131 | text: '=' 132 | }; 133 | 134 | request(app) 135 | .post('/wechat' + tail()) 136 | .send(template(info)) 137 | .expect(200) 138 | .end(function(err, res){ 139 | if (err) return done(err); 140 | var body = res.text.toString(); 141 | body.should.include(''); 142 | done(); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | 3 | var tpl = [ 4 | '', 5 | ']]>', 6 | ']]>', 7 | '<%=(new Date().getTime())%>', 8 | ']]>', 9 | '<% if (type === "text") { %>', 10 | ']]>', 11 | '<% } else if (type === "location") { %>', 12 | '<%=xPos%>', 13 | '<%=yPos%>', 14 | '<%=scale%>', 15 | '', 16 | '<% } else if (type === "image") { %>', 17 | ']]>', 18 | '<% } else if (type === "voice") { %>', 19 | '<%=mediaId%>', 20 | '<%=format%>', 21 | '<% } else if (type === "link") { %>', 22 | '<![CDATA[<%=title%>]]>', 23 | ']]>', 24 | ']]>', 25 | '<% } else if (type === "event") { %>', 26 | ']]>', 27 | '<% if (event === "LOCATION") { %>', 28 | '<%=latitude%>', 29 | '<%=longitude%>', 30 | '<%=precision%>', 31 | '<% } %>', 32 | '<% if (event === "location_select") { %>', 33 | '', 34 | '', 35 | ']]>', 36 | ']]>', 37 | '', 38 | '', 39 | '', 40 | ']]>', 41 | '', 42 | '<% } %>', 43 | '<% if (event === "pic_weixin") { %>', 44 | '', 45 | '', 46 | '1', 47 | '', 48 | '', 49 | ' ', 50 | '', 51 | '', 52 | ']]>', 53 | '', 54 | '<% } %>', 55 | '<% } %>', 56 | '<% if (user === "web") { %>', 57 | 'webwx_msg_cli_ver_0x1', 58 | '<% } %>', 59 | '' 60 | ].join(''); 61 | 62 | exports.tail = function (checkSignature) { 63 | var q = { 64 | timestamp: new Date().getTime(), 65 | nonce: parseInt((Math.random() * 100000000000), 10) 66 | }; 67 | if (checkSignature !== false) { 68 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 69 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 70 | } 71 | q.echostr = 'hehe'; 72 | return '?' + querystring.stringify(q); 73 | }; 74 | 75 | exports.template = require('ejs').compile(tpl); 76 | -------------------------------------------------------------------------------- /test/talk.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var request = require('supertest'); 4 | var template = require('./support').template; 5 | var tail = require('./support').tail; 6 | 7 | var connect = require('connect'); 8 | var wechat = require('../'); 9 | 10 | var List = require('../').List; 11 | 12 | var app = connect(); 13 | app.use(connect.query()); 14 | app.use(connect.cookieParser()); 15 | app.use(connect.session({secret: 'keyboard cat', cookie: {maxAge: 60000}})); 16 | app.use('/wechat', wechat('some token', wechat.text(function (info, req, res, next) { 17 | if (info.Content === 'list') { 18 | res.wait('view', function (err) { 19 | should.not.exist(err); 20 | }); 21 | } else if (info.Content === 'undefinedlist') { 22 | res.wait('undefined', function (err) { 23 | should.exist(err); 24 | }); 25 | } else { 26 | res.reply('hehe'); 27 | } 28 | }))); 29 | 30 | describe('wechat.js', function () { 31 | before(function () { 32 | List.add('view', [ 33 | ['回复{a}查看我的性别', function (info, req, res) { 34 | res.reply('我是个妹纸哟'); 35 | }], 36 | ['回复{b}查看我的年龄', function (info, req, res) { 37 | res.reply('我今年18岁'); 38 | }], 39 | ['回复{c}查看我的性取向', '这样的事情怎么好意思告诉你啦- -'], 40 | ['回复{nowait}退出问答', function (info, req, res) { 41 | res.nowait('thanks'); 42 | }] 43 | ]); 44 | }); 45 | 46 | describe('talk', function () { 47 | it('should reply hehe when not trigger the list', function (done) { 48 | var info = { 49 | sp: 'nvshen', 50 | user: 'diaosi', 51 | type: 'text', 52 | text: 'a' 53 | }; 54 | 55 | request(app) 56 | .post('/wechat' + tail()) 57 | .send(template(info)) 58 | .expect(200) 59 | .end(function (err, res) { 60 | if (err) return done(err); 61 | var body = res.text.toString(); 62 | body.should.include(''); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should reply the list when trigger the list', function (done) { 68 | var info = { 69 | sp: 'nvshen', 70 | user: 'diaosi', 71 | type: 'text', 72 | text: 'list' 73 | }; 74 | 75 | request(app) 76 | .post('/wechat' + tail()) 77 | .send(template(info)) 78 | .expect(200) 79 | .end(function (err, res) { 80 | if (err) return done(err); 81 | var body = res.text.toString(); 82 | body.should.include(''); 83 | done(); 84 | }); 85 | }); 86 | 87 | it('should reply with list', function (done) { 88 | var info = { 89 | sp: 'nvshen', 90 | user: 'diaosi', 91 | type: 'text', 92 | text: 'a' 93 | }; 94 | 95 | request(app) 96 | .post('/wechat' + tail()) 97 | .send(template(info)) 98 | .expect(200) 99 | .end(function (err, res) { 100 | if (err) return done(err); 101 | var body = res.text.toString(); 102 | body.should.include(''); 103 | done(); 104 | }); 105 | }); 106 | 107 | it('should reply with list also', function (done) { 108 | var info = { 109 | sp: 'nvshen', 110 | user: 'diaosi', 111 | type: 'text', 112 | text: 'b' 113 | }; 114 | 115 | request(app) 116 | .post('/wechat' + tail()) 117 | .send(template(info)) 118 | .expect(200) 119 | .end(function(err, res){ 120 | if (err) return done(err); 121 | var body = res.text.toString(); 122 | body.should.include(''); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('should reply with text', function (done) { 128 | var info = { 129 | sp: 'nvshen', 130 | user: 'diaosi', 131 | type: 'text', 132 | text: 'c' 133 | }; 134 | 135 | request(app) 136 | .post('/wechat' + tail()) 137 | .send(template(info)) 138 | .expect(200) 139 | .end(function(err, res){ 140 | if (err) return done(err); 141 | var body = res.text.toString(); 142 | body.should.include(''); 143 | done(); 144 | }); 145 | }); 146 | 147 | it('should reply with default handle', function (done) { 148 | var info = { 149 | sp: 'nvshen', 150 | user: 'diaosi', 151 | type: 'text', 152 | text: 'd' 153 | }; 154 | 155 | request(app) 156 | .post('/wechat' + tail()) 157 | .send(template(info)) 158 | .expect(200) 159 | .end(function(err, res){ 160 | if (err) return done(err); 161 | var body = res.text.toString(); 162 | body.should.include(''); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should reply 500 when undefined list', function (done) { 168 | var info = { 169 | sp: 'nvshen', 170 | user: 'diaosi', 171 | type: 'text', 172 | text: 'undefinedlist' 173 | }; 174 | 175 | request(app) 176 | .post('/wechat' + tail()) 177 | .send(template(info)) 178 | .expect(500) 179 | .end(function(err, res){ 180 | if (err) return done(err); 181 | var body = res.text.toString(); 182 | body.should.include('UndefinedListError'); 183 | done(); 184 | }); 185 | }); 186 | 187 | it('should reply thanks when nowait', function (done) { 188 | var info = { 189 | sp: 'nvshen', 190 | user: 'diaosi', 191 | type: 'text', 192 | text: 'nowait' 193 | }; 194 | 195 | request(app) 196 | .post('/wechat' + tail()) 197 | .send(template(info)) 198 | .expect(200) 199 | .end(function(err, res){ 200 | if (err) return done(err); 201 | var body = res.text.toString(); 202 | body.should.include('thanks'); 203 | done(); 204 | }); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /test/wechat.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var querystring = require('querystring'); 4 | var request = require('supertest'); 5 | var template = require('./support').template; 6 | var tail = require('./support').tail; 7 | 8 | var connect = require('connect'); 9 | var wechat = require('../'); 10 | 11 | var app = connect(); 12 | app.use(connect.query()); 13 | app.use(function (req, res, next) { 14 | if (req.query.rawBody) { 15 | req.rawBody = "\ 16 | \ 17 | 1362161914\ 18 | \ 19 | 30.283878\ 20 | 120.063370\ 21 | 15\ 22 | \ 23 | 5850440872586764820\ 24 | "; 25 | } 26 | next(); 27 | }); 28 | app.use('/wechat', wechat('some token', function (req, res, next) { 29 | // 微信输入信息都在req.weixin上 30 | var info = req.weixin; 31 | // 回复屌丝(普通回复) 32 | if (info.FromUserName === 'diaosi') { 33 | res.reply('hehe'); 34 | } else if (info.FromUserName === 'test') { 35 | res.reply({ 36 | content: 'text object', 37 | type: 'text' 38 | }); 39 | } else if (info.FromUserName === 'hehe') { 40 | res.reply({ 41 | title: "来段音乐吧<", 42 | description: "一无所有>", 43 | musicUrl: "http://mp3.com/xx.mp3?a=b&c=d", 44 | hqMusicUrl: "http://mp3.com/xx.mp3?foo=bar" 45 | }); 46 | } else if (info.FromUserName === 'cs') { 47 | res.transfer2CustomerService(); 48 | } else if (info.FromUserName === 'kf') { 49 | res.transfer2CustomerService('test1@test'); 50 | } else if (info.FromUserName === 'ls') { 51 | res.reply(info.SendLocationInfo.EventKey); 52 | } else if (info.FromUserName === 'pic_weixin') { 53 | res.reply(info.SendPicsInfo.EventKey); 54 | } else if (info.FromUserName === 'web') { 55 | res.reply('web message ok'); 56 | } else if (info.FromUserName === 'empty') { 57 | res.reply(''); 58 | } else { 59 | // 回复高富帅(图文回复) 60 | res.reply([ 61 | { 62 | title: '你来我家接我吧', 63 | description: '这是女神与高富帅之间的对话', 64 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 65 | url: 'http://nodeapi.cloudfoundry.com/' 66 | } 67 | ]); 68 | } 69 | })); 70 | 71 | describe('wechat.js', function () { 72 | 73 | describe('valid GET', function () { 74 | it('should 401', function (done) { 75 | request(app) 76 | .get('/wechat') 77 | .expect(401) 78 | .expect('Invalid signature', done); 79 | }); 80 | 81 | it('should 200', function (done) { 82 | var q = { 83 | timestamp: new Date().getTime(), 84 | nonce: parseInt((Math.random() * 10e10), 10) 85 | }; 86 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 87 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 88 | q.echostr = 'hehe'; 89 | request(app) 90 | .get('/wechat?' + querystring.stringify(q)) 91 | .expect(200) 92 | .expect('hehe', done); 93 | }); 94 | 95 | it('should 401 invalid signature', function (done) { 96 | var q = { 97 | timestamp: new Date().getTime(), 98 | nonce: parseInt((Math.random() * 10e10), 10) 99 | }; 100 | q.signature = 'invalid_signature'; 101 | q.echostr = 'hehe'; 102 | request(app) 103 | .get('/wechat?' + querystring.stringify(q)) 104 | .expect(401) 105 | .expect('Invalid signature', done); 106 | }); 107 | }); 108 | 109 | describe('valid POST', function () { 110 | it('should 401', function (done) { 111 | request(app) 112 | .post('/wechat') 113 | .expect(401) 114 | .expect('Invalid signature', done); 115 | }); 116 | 117 | it('should 401 invalid signature', function (done) { 118 | var q = { 119 | timestamp: new Date().getTime(), 120 | nonce: parseInt((Math.random() * 10e10), 10) 121 | }; 122 | q.signature = 'invalid_signature'; 123 | q.echostr = 'hehe'; 124 | request(app) 125 | .post('/wechat?' + querystring.stringify(q)) 126 | .expect(401) 127 | .expect('Invalid signature', done); 128 | }); 129 | }); 130 | 131 | describe('valid other method', function () { 132 | it('should 200', function (done) { 133 | var q = { 134 | timestamp: new Date().getTime(), 135 | nonce: parseInt((Math.random() * 10e10), 10) 136 | }; 137 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 138 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 139 | q.echostr = 'hehe'; 140 | request(app) 141 | .head('/wechat?' + querystring.stringify(q)) 142 | .expect(501, done); 143 | }); 144 | }); 145 | 146 | describe('respond', function () { 147 | it('should ok', function (done) { 148 | var info = { 149 | sp: 'nvshen', 150 | user: 'diaosi', 151 | type: 'text', 152 | text: '测试中' 153 | }; 154 | 155 | request(app) 156 | .post('/wechat' + tail()) 157 | .send(template(info)) 158 | .expect(200) 159 | .end(function(err, res){ 160 | if (err) return done(err); 161 | var body = res.text.toString(); 162 | body.should.include(''); 163 | body.should.include(''); 164 | body.should.match(/\d{13}<\/CreateTime>/); 165 | body.should.include(''); 166 | body.should.include(''); 167 | done(); 168 | }); 169 | }); 170 | 171 | it('should ok with req.rawBody', function (done) { 172 | request(app) 173 | .post('/wechat' + tail() + "&rawBody=true") 174 | .send('') 175 | .expect(200) 176 | .end(function (err, res){ 177 | if (err) return done(err); 178 | var body = res.text.toString(); 179 | body.should.include(''); 180 | body.should.include(''); 181 | body.should.match(/\d{13}<\/CreateTime>/); 182 | body.should.include(''); 183 | body.should.include(''); 184 | done(); 185 | }); 186 | }); 187 | 188 | it('should ok with text type object', function (done) { 189 | var info = { 190 | sp: 'nvshen', 191 | user: 'test', 192 | type: 'text', 193 | text: '测试中' 194 | }; 195 | 196 | request(app) 197 | .post('/wechat' + tail()) 198 | .send(template(info)) 199 | .expect(200) 200 | .end(function(err, res){ 201 | if (err) return done(err); 202 | var body = res.text.toString(); 203 | body.should.include(''); 204 | body.should.include(''); 205 | body.should.match(/\d{13}<\/CreateTime>/); 206 | body.should.include(''); 207 | body.should.include(''); 208 | done(); 209 | }); 210 | }); 211 | 212 | it('should ok with news', function (done) { 213 | var info = { 214 | sp: 'nvshen', 215 | user: 'gaofushuai', 216 | type: 'text', 217 | text: '测试中' 218 | }; 219 | 220 | request(app) 221 | .post('/wechat' + tail()) 222 | .send(template(info)) 223 | .expect(200) 224 | .end(function(err, res){ 225 | if (err) return done(err); 226 | var body = res.text.toString(); 227 | body.should.include(''); 228 | body.should.include(''); 229 | body.should.match(/\d{13}<\/CreateTime>/); 230 | body.should.include(''); 231 | body.should.include('1'); 232 | body.should.include('<![CDATA[你来我家接我吧]]>'); 233 | body.should.include(''); 234 | body.should.include(''); 235 | body.should.include(''); 236 | done(); 237 | }); 238 | }); 239 | 240 | it('should ok with music', function (done) { 241 | var info = { 242 | sp: 'nvshen', 243 | user: 'hehe', 244 | type: 'text', 245 | text: '测试中' 246 | }; 247 | 248 | request(app) 249 | .post('/wechat' + tail()) 250 | .send(template(info)) 251 | .expect(200) 252 | .end(function(err, res){ 253 | if (err) return done(err); 254 | var body = res.text.toString(); 255 | body.should.include(''); 256 | body.should.include(''); 257 | body.should.match(/\d{13}<\/CreateTime>/); 258 | body.should.include(''); 259 | body.should.include(''); 260 | body.should.include(''); 261 | body.should.include('<![CDATA[来段音乐吧<]]>'); 262 | body.should.include(']]>'); 263 | body.should.include(''); 264 | body.should.include(''); 265 | done(); 266 | }); 267 | }); 268 | 269 | it('should ok with event location_select', function (done) { 270 | var info = { 271 | sp: 'nvshen', 272 | user: 'ls', 273 | type: 'event', 274 | xPos: '80', 275 | yPos: '70', 276 | label: 'alibaba', 277 | event: 'location_select', 278 | eventKey: 'sendLocation', 279 | text: '测试中' 280 | }; 281 | 282 | request(app) 283 | .post('/wechat' + tail()) 284 | .send(template(info)) 285 | .expect(200) 286 | .end(function(err, res){ 287 | if (err) return done(err); 288 | var body = res.text.toString(); 289 | body.should.include(''); 290 | body.should.include(''); 291 | body.should.match(/\d{13}<\/CreateTime>/); 292 | body.should.include(''); 293 | body.should.include(''); 294 | done(); 295 | }); 296 | }); 297 | 298 | it('should ok with event pic_weixin', function (done) { 299 | var info = { 300 | sp: 'nvshen', 301 | user: 'pic_weixin', 302 | type: 'event', 303 | event: 'pic_weixin', 304 | eventKey: 'sendPic', 305 | text: '测试中' 306 | }; 307 | 308 | request(app) 309 | .post('/wechat' + tail()) 310 | .send(template(info)) 311 | .expect(200) 312 | .end(function(err, res){ 313 | if (err) return done(err); 314 | var body = res.text.toString(); 315 | body.should.include(''); 316 | body.should.include(''); 317 | body.should.match(/\d{13}<\/CreateTime>/); 318 | body.should.include(''); 319 | body.should.include(''); 320 | done(); 321 | }); 322 | }); 323 | 324 | it('should ok with customer service', function (done) { 325 | var info = { 326 | sp: 'gaofushuai', 327 | user: 'cs', 328 | type: 'text', 329 | text: '测试中' 330 | }; 331 | 332 | request(app) 333 | .post('/wechat' + tail()) 334 | .send(template(info)) 335 | .expect(200) 336 | .end(function(err, res){ 337 | if (err) return done(err); 338 | var body = res.text.toString(); 339 | body.should.include(''); 340 | body.should.include(''); 341 | body.should.match(/\d{13}<\/CreateTime>/); 342 | body.should.include(''); 343 | done(); 344 | }); 345 | }); 346 | 347 | 348 | it('should ok with transfer info to kfAccount', function(done) { 349 | var info = { 350 | sp: 'zhong', 351 | user: 'kf', 352 | type: 'text', 353 | text: '测试中' 354 | }; 355 | request(app) 356 | .post('/wechat' + tail()) 357 | .send(template(info)) 358 | .expect(200) 359 | .end(function(err, res) { 360 | if (err) return done(err); 361 | var body = res.text.toString(); 362 | body.should.include(''); 363 | body.should.include(''); 364 | body.should.match(/\d{13}<\/CreateTime>/); 365 | body.should.include(''); 366 | body.should.include(''); 367 | done(); 368 | }); 369 | }); 370 | 371 | it('should ok with empty message', function (done) { 372 | var info = { 373 | sp: 'nvshen', 374 | user: 'empty', 375 | type: 'text', 376 | text: '测试中' 377 | }; 378 | 379 | request(app) 380 | .post('/wechat' + tail()) 381 | .send(template(info)) 382 | .expect(200) 383 | .end(function (err, res) { 384 | if (err) return done(err); 385 | var body = res.text.toString(); 386 | body.should.match(''); 387 | done(); 388 | }); 389 | }); 390 | 391 | it('should ok with web wechat message', function (done) { 392 | var info = { 393 | sp: 'nvshen', 394 | user: 'web', 395 | type: 'text', 396 | text: '测试中' 397 | }; 398 | 399 | request(app) 400 | .post('/wechat' + tail()) 401 | .send(template(info)) 402 | .expect(200) 403 | .end(function(err, res){ 404 | if (err) return done(err); 405 | var body = res.text.toString(); 406 | body.should.include(''); 407 | body.should.include(''); 408 | body.should.match(/\d{13}<\/CreateTime>/); 409 | body.should.include(''); 410 | body.should.include(''); 411 | done(); 412 | }); 413 | }); 414 | 415 | it('should pass to next', function (done) { 416 | var info = { 417 | sp: 'nvshen', 418 | user: 'hehe', 419 | type: 'next', 420 | text: '测试中' 421 | }; 422 | 423 | request(app) 424 | .post('/wechat' + tail()) 425 | .send(template(info)) 426 | .expect(200) 427 | .end(function(err, res){ 428 | if (err) return done(err); 429 | var body = res.text.toString(); 430 | body.should.include(''); 431 | body.should.include(''); 432 | body.should.match(/\d{13}<\/CreateTime>/); 433 | body.should.include(''); 434 | body.should.include(''); 435 | body.should.include(''); 436 | body.should.include('<![CDATA[来段音乐吧<]]>'); 437 | body.should.include(']]>'); 438 | body.should.include(''); 439 | body.should.include(''); 440 | done(); 441 | }); 442 | }); 443 | }); 444 | 445 | describe('exception', function () { 446 | var xml = '\ 447 | \ 448 | 1362161914\ 449 | \ 450 | 30.283878\ 451 | 120.063370\ 452 | 15\ 453 | \ 454 | 5850440872586764820\ 455 | '; 456 | it('should ok', function () { 457 | request(app) 458 | .post('/wechat' + tail()) 459 | .send(xml) 460 | .expect(200) 461 | .end(function(err, res){ 462 | if (err) return done(err); 463 | }); 464 | }); 465 | }); 466 | }); 467 | -------------------------------------------------------------------------------- /test/wechat2.test.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'); 2 | var wechat = require('../'); 3 | 4 | var app = connect(); 5 | app.use(connect.query()); 6 | app.use('/wechat', wechat('some token', wechat.text(function (message, req, res, next) { 7 | // 微信输入信息都在message上 8 | // 回复屌丝(普通回复) 9 | if (message.FromUserName === 'diaosi') { 10 | res.reply('hehe'); 11 | } else if (message.FromUserName === 'hehe') { 12 | res.reply({ 13 | title: "来段音乐吧", 14 | description: "一无所有", 15 | musicUrl: "http://mp3.com/xx.mp3", 16 | hqMusicUrl: "http://mp3.com/xx.mp3" 17 | }); 18 | } else { 19 | // 回复高富帅(图文回复) 20 | res.reply([ 21 | { 22 | title: '你来我家接我吧', 23 | description: '这是女神与高富帅之间的对话', 24 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 25 | url: 'http://nodeapi.cloudfoundry.com/' 26 | } 27 | ]); 28 | } 29 | }).location(function (message, req, res, next) { 30 | res.reply('location'); 31 | }).image(function (message, req, res, next) { 32 | res.reply('image'); 33 | }).voice(function (message, req, res, next) { 34 | res.reply('voice'); 35 | }).link(function (message, req, res, next) { 36 | res.reply('link'); 37 | }).event(function (message, req, res, next) { 38 | res.reply('event'); 39 | }))); 40 | 41 | require('should'); 42 | 43 | var querystring = require('querystring'); 44 | var request = require('supertest'); 45 | var template = require('./support').template; 46 | var tail = require('./support').tail; 47 | 48 | describe('wechat.js 0.2.0', function () { 49 | 50 | describe('valid GET', function () { 51 | it('should 401', function (done) { 52 | request(app) 53 | .get('/wechat') 54 | .expect(401) 55 | .expect('Invalid signature', done); 56 | }); 57 | 58 | it('should 200', function (done) { 59 | var q = { 60 | timestamp: new Date().getTime(), 61 | nonce: parseInt((Math.random() * 10e10), 10) 62 | }; 63 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 64 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 65 | q.echostr = 'hehe'; 66 | request(app) 67 | .get('/wechat?' + querystring.stringify(q)) 68 | .expect(200) 69 | .expect('hehe', done); 70 | }); 71 | 72 | it('should 401 invalid signature', function (done) { 73 | var q = { 74 | timestamp: new Date().getTime(), 75 | nonce: parseInt((Math.random() * 10e10), 10) 76 | }; 77 | q.signature = 'invalid_signature'; 78 | q.echostr = 'hehe'; 79 | request(app) 80 | .get('/wechat?' + querystring.stringify(q)) 81 | .expect(401) 82 | .expect('Invalid signature', done); 83 | }); 84 | }); 85 | 86 | describe('valid POST', function () { 87 | it('should 401', function (done) { 88 | request(app) 89 | .post('/wechat') 90 | .expect(401) 91 | .expect('Invalid signature', done); 92 | }); 93 | 94 | it('should 401 invalid signature', function (done) { 95 | var q = { 96 | timestamp: new Date().getTime(), 97 | nonce: parseInt((Math.random() * 10e10), 10) 98 | }; 99 | q.signature = 'invalid_signature'; 100 | q.echostr = 'hehe'; 101 | request(app) 102 | .post('/wechat?' + querystring.stringify(q)) 103 | .expect(401) 104 | .expect('Invalid signature', done); 105 | }); 106 | }); 107 | 108 | describe('respond', function () { 109 | it('should ok', function (done) { 110 | var info = { 111 | sp: 'nvshen', 112 | user: 'diaosi', 113 | type: 'text', 114 | text: '测试中' 115 | }; 116 | 117 | request(app) 118 | .post('/wechat' + tail()) 119 | .send(template(info)) 120 | .expect(200) 121 | .end(function(err, res){ 122 | if (err) return done(err); 123 | var body = res.text.toString(); 124 | body.should.include(''); 125 | body.should.include(''); 126 | body.should.match(/\d{13}<\/CreateTime>/); 127 | body.should.include(''); 128 | body.should.include(''); 129 | done(); 130 | }); 131 | }); 132 | 133 | it('should ok with news', function (done) { 134 | var info = { 135 | sp: 'nvshen', 136 | user: 'gaofushuai', 137 | type: 'text', 138 | text: '测试中' 139 | }; 140 | 141 | request(app) 142 | .post('/wechat' + tail()) 143 | .send(template(info)) 144 | .expect(200) 145 | .end(function(err, res){ 146 | if (err) return done(err); 147 | var body = res.text.toString(); 148 | body.should.include(''); 149 | body.should.include(''); 150 | body.should.match(/\d{13}<\/CreateTime>/); 151 | body.should.include(''); 152 | body.should.include('1'); 153 | body.should.include('<![CDATA[你来我家接我吧]]>'); 154 | body.should.include(''); 155 | body.should.include(''); 156 | body.should.include(''); 157 | done(); 158 | }); 159 | }); 160 | 161 | it('should ok when image', function (done) { 162 | var info = { 163 | sp: 'nvshen', 164 | user: 'diaosi', 165 | type: 'image', 166 | pic: 'http://mmsns.qpic.cn/mmsns/bfc815ygvIWcaaZlEXJV7NzhmA3Y2fc4eBOxLjpPI60Q1Q6ibYicwg/0' 167 | }; 168 | 169 | request(app) 170 | .post('/wechat' + tail()) 171 | .send(template(info)) 172 | .expect(200) 173 | .end(function(err, res){ 174 | if (err) return done(err); 175 | var body = res.text.toString(); 176 | body.should.include(''); 177 | body.should.include(''); 178 | body.should.match(/\d{13}<\/CreateTime>/); 179 | body.should.include(''); 180 | body.should.include(''); 181 | done(); 182 | }); 183 | }); 184 | 185 | it('should ok when location', function (done) { 186 | var info = { 187 | sp: 'nvshen', 188 | user: 'diaosi', 189 | type: 'location', 190 | xPos: 'xPos', 191 | yPos: 'yPos', 192 | scale: '100', 193 | label: 'label' 194 | }; 195 | 196 | request(app) 197 | .post('/wechat' + tail()) 198 | .send(template(info)) 199 | .expect(200) 200 | .end(function(err, res){ 201 | if (err) return done(err); 202 | var body = res.text.toString(); 203 | body.should.include(''); 204 | body.should.include(''); 205 | body.should.match(/\d{13}<\/CreateTime>/); 206 | body.should.include(''); 207 | body.should.include(''); 208 | done(); 209 | }); 210 | }); 211 | 212 | it('should ok when voice', function (done) { 213 | var info = { 214 | sp: 'nvshen', 215 | user: 'diaosi', 216 | type: 'voice', 217 | mediaId: 'id', 218 | format: 'format' 219 | }; 220 | 221 | request(app) 222 | .post('/wechat' + tail()) 223 | .send(template(info)) 224 | .expect(200) 225 | .end(function(err, res){ 226 | if (err) return done(err); 227 | var body = res.text.toString(); 228 | body.should.include(''); 229 | body.should.include(''); 230 | body.should.match(/\d{13}<\/CreateTime>/); 231 | body.should.include(''); 232 | body.should.include(''); 233 | done(); 234 | }); 235 | }); 236 | 237 | it('should ok when link', function (done) { 238 | var info = { 239 | sp: 'nvshen', 240 | user: 'diaosi', 241 | type: 'link', 242 | title: 'good link', 243 | description: '1024', 244 | url: 'http://where.is.caoliu/' 245 | }; 246 | 247 | request(app) 248 | .post('/wechat' + tail()) 249 | .send(template(info)) 250 | .expect(200) 251 | .end(function(err, res){ 252 | if (err) return done(err); 253 | var body = res.text.toString(); 254 | body.should.include(''); 255 | body.should.include(''); 256 | body.should.match(/\d{13}<\/CreateTime>/); 257 | body.should.include(''); 258 | body.should.include(''); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('should ok when event location', function (done) { 264 | var info = { 265 | sp: 'nvshen', 266 | user: 'diaosi', 267 | type: 'event', 268 | event: 'LOCATION', 269 | latitude: '23.137466', 270 | longitude: '113.352425', 271 | precision: '119.385040' 272 | }; 273 | 274 | request(app) 275 | .post('/wechat' + tail()) 276 | .send(template(info)) 277 | .expect(200) 278 | .end(function(err, res){ 279 | if (err) return done(err); 280 | var body = res.text.toString(); 281 | body.should.include(''); 282 | body.should.include(''); 283 | body.should.match(/\d{13}<\/CreateTime>/); 284 | body.should.include(''); 285 | body.should.include(''); 286 | done(); 287 | }); 288 | }); 289 | 290 | it('should ok when event enter', function (done) { 291 | var info = { 292 | sp: 'nvshen', 293 | user: 'diaosi', 294 | type: 'event', 295 | event: 'ENTER' 296 | }; 297 | 298 | request(app) 299 | .post('/wechat' + tail()) 300 | .send(template(info)) 301 | .expect(200) 302 | .end(function(err, res){ 303 | if (err) return done(err); 304 | var body = res.text.toString(); 305 | body.should.include(''); 306 | body.should.include(''); 307 | body.should.match(/\d{13}<\/CreateTime>/); 308 | body.should.include(''); 309 | body.should.include(''); 310 | done(); 311 | }); 312 | }); 313 | 314 | it('should ok reply text', function (done) { 315 | var info = { 316 | sp: 'nvshen', 317 | user: 'diaosi', 318 | type: 'text', 319 | text: '测试中' 320 | }; 321 | 322 | request(app) 323 | .post('/wechat' + tail()) 324 | .send(template(info)) 325 | .expect(200) 326 | .end(function(err, res){ 327 | if (err) return done(err); 328 | var body = res.text.toString(); 329 | body.should.include(''); 330 | body.should.include(''); 331 | done(); 332 | }); 333 | }); 334 | 335 | it('should ok reply news', function (done) { 336 | var info = { 337 | sp: 'nvshen', 338 | user: 'gaofushuai', 339 | type: 'text', 340 | text: '测试中' 341 | }; 342 | 343 | request(app) 344 | .post('/wechat' + tail()) 345 | .send(template(info)) 346 | .expect(200) 347 | .end(function(err, res){ 348 | if (err) return done(err); 349 | var body = res.text.toString(); 350 | body.should.include(''); 351 | body.should.include('1'); 352 | body.should.include('<![CDATA[你来我家接我吧]]>'); 353 | body.should.include(''); 354 | body.should.include(''); 355 | body.should.include(''); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('should ok reply music', function (done) { 361 | var info = { 362 | sp: 'nvshen', 363 | user: 'hehe', 364 | type: 'text', 365 | text: '测试中' 366 | }; 367 | 368 | request(app) 369 | .post('/wechat' + tail()) 370 | .send(template(info)) 371 | .expect(200) 372 | .end(function(err, res){ 373 | if (err) return done(err); 374 | var body = res.text.toString(); 375 | body.should.include(''); 376 | body.should.include('<![CDATA[来段音乐吧]]>'); 377 | body.should.include(''); 378 | body.should.include(''); 379 | body.should.include(''); 380 | done(); 381 | }); 382 | }); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /test/wechat3.test.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'); 2 | var wechat = require('../'); 3 | var querystring = require('querystring'); 4 | var request = require('supertest'); 5 | var template = require('./support').template; 6 | var tail = require('./support').tail; 7 | require('should'); 8 | 9 | var app = connect(); 10 | app.use(connect.query()); 11 | app.use('/wechat', wechat('some token').text(function (message, req, res, next) { 12 | // 微信输入信息都在message上 13 | // 回复屌丝(普通回复) 14 | if (message.FromUserName === 'diaosi') { 15 | res.reply('hehe'); 16 | } else if (message.FromUserName === 'hehe') { 17 | res.reply({ 18 | title: "来段音乐吧", 19 | description: "一无所有", 20 | musicUrl: "http://mp3.com/xx.mp3", 21 | hqMusicUrl: "http://mp3.com/xx.mp3" 22 | }); 23 | } else { 24 | // 回复高富帅(图文回复) 25 | res.reply([ 26 | { 27 | title: '你来我家接我吧', 28 | description: '这是女神与高富帅之间的对话', 29 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 30 | url: 'http://nodeapi.cloudfoundry.com/' 31 | } 32 | ]); 33 | } 34 | }).location(function (message, req, res, next) { 35 | res.reply('location'); 36 | }).image(function (message, req, res, next) { 37 | res.reply('image'); 38 | }).voice(function (message, req, res, next) { 39 | res.reply('voice'); 40 | }).link(function (message, req, res, next) { 41 | res.reply('link'); 42 | }).event(function (message, req, res, next) { 43 | res.reply('event'); 44 | }).middlewarify()); 45 | 46 | describe('wechat.js 0.3.0', function () { 47 | 48 | describe('valid GET', function () { 49 | it('should 401', function (done) { 50 | request(app) 51 | .get('/wechat') 52 | .expect(401) 53 | .expect('Invalid signature', done); 54 | }); 55 | 56 | it('should 200', function (done) { 57 | var q = { 58 | timestamp: new Date().getTime(), 59 | nonce: parseInt((Math.random() * 10e10), 10) 60 | }; 61 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 62 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 63 | q.echostr = 'hehe'; 64 | request(app) 65 | .get('/wechat?' + querystring.stringify(q)) 66 | .expect(200) 67 | .expect('hehe', done); 68 | }); 69 | 70 | it('should 401 invalid signature', function (done) { 71 | var q = { 72 | timestamp: new Date().getTime(), 73 | nonce: parseInt((Math.random() * 10e10), 10) 74 | }; 75 | q.signature = 'invalid_signature'; 76 | q.echostr = 'hehe'; 77 | request(app) 78 | .get('/wechat?' + querystring.stringify(q)) 79 | .expect(401) 80 | .expect('Invalid signature', done); 81 | }); 82 | }); 83 | 84 | describe('valid POST', function () { 85 | it('should 401', function (done) { 86 | request(app) 87 | .post('/wechat') 88 | .expect(401) 89 | .expect('Invalid signature', done); 90 | }); 91 | 92 | it('should 401 invalid signature', function (done) { 93 | var q = { 94 | timestamp: new Date().getTime(), 95 | nonce: parseInt((Math.random() * 10e10), 10) 96 | }; 97 | q.signature = 'invalid_signature'; 98 | q.echostr = 'hehe'; 99 | request(app) 100 | .post('/wechat?' + querystring.stringify(q)) 101 | .expect(401) 102 | .expect('Invalid signature', done); 103 | }); 104 | }); 105 | 106 | describe('respond', function () { 107 | it('should ok', function (done) { 108 | var info = { 109 | sp: 'nvshen', 110 | user: 'diaosi', 111 | type: 'text', 112 | text: '测试中' 113 | }; 114 | 115 | request(app) 116 | .post('/wechat' + tail()) 117 | .send(template(info)) 118 | .expect(200) 119 | .end(function(err, res){ 120 | if (err) return done(err); 121 | var body = res.text.toString(); 122 | body.should.include(''); 123 | body.should.include(''); 124 | body.should.match(/\d{13}<\/CreateTime>/); 125 | body.should.include(''); 126 | body.should.include(''); 127 | done(); 128 | }); 129 | }); 130 | 131 | it('should ok with news', function (done) { 132 | var info = { 133 | sp: 'nvshen', 134 | user: 'gaofushuai', 135 | type: 'text', 136 | text: '测试中' 137 | }; 138 | 139 | request(app) 140 | .post('/wechat' + tail()) 141 | .send(template(info)) 142 | .expect(200) 143 | .end(function(err, res){ 144 | if (err) return done(err); 145 | var body = res.text.toString(); 146 | body.should.include(''); 147 | body.should.include(''); 148 | body.should.match(/\d{13}<\/CreateTime>/); 149 | body.should.include(''); 150 | body.should.include('1'); 151 | body.should.include('<![CDATA[你来我家接我吧]]>'); 152 | body.should.include(''); 153 | body.should.include(''); 154 | body.should.include(''); 155 | done(); 156 | }); 157 | }); 158 | 159 | it('should ok when image', function (done) { 160 | var info = { 161 | sp: 'nvshen', 162 | user: 'diaosi', 163 | type: 'image', 164 | pic: 'http://mmsns.qpic.cn/mmsns/bfc815ygvIWcaaZlEXJV7NzhmA3Y2fc4eBOxLjpPI60Q1Q6ibYicwg/0' 165 | }; 166 | 167 | request(app) 168 | .post('/wechat' + tail()) 169 | .send(template(info)) 170 | .expect(200) 171 | .end(function(err, res){ 172 | if (err) return done(err); 173 | var body = res.text.toString(); 174 | body.should.include(''); 175 | body.should.include(''); 176 | body.should.match(/\d{13}<\/CreateTime>/); 177 | body.should.include(''); 178 | body.should.include(''); 179 | done(); 180 | }); 181 | }); 182 | 183 | it('should ok when location', function (done) { 184 | var info = { 185 | sp: 'nvshen', 186 | user: 'diaosi', 187 | type: 'location', 188 | xPos: 'xPos', 189 | yPos: 'yPos', 190 | scale: '100', 191 | label: 'label' 192 | }; 193 | 194 | request(app) 195 | .post('/wechat' + tail()) 196 | .send(template(info)) 197 | .expect(200) 198 | .end(function(err, res){ 199 | if (err) return done(err); 200 | var body = res.text.toString(); 201 | body.should.include(''); 202 | body.should.include(''); 203 | body.should.match(/\d{13}<\/CreateTime>/); 204 | body.should.include(''); 205 | body.should.include(''); 206 | done(); 207 | }); 208 | }); 209 | 210 | it('should ok when voice', function (done) { 211 | var info = { 212 | sp: 'nvshen', 213 | user: 'diaosi', 214 | type: 'voice', 215 | mediaId: 'id', 216 | format: 'format' 217 | }; 218 | 219 | request(app) 220 | .post('/wechat' + tail()) 221 | .send(template(info)) 222 | .expect(200) 223 | .end(function(err, res){ 224 | if (err) return done(err); 225 | var body = res.text.toString(); 226 | body.should.include(''); 227 | body.should.include(''); 228 | body.should.match(/\d{13}<\/CreateTime>/); 229 | body.should.include(''); 230 | body.should.include(''); 231 | done(); 232 | }); 233 | }); 234 | 235 | it('should ok when link', function (done) { 236 | var info = { 237 | sp: 'nvshen', 238 | user: 'diaosi', 239 | type: 'link', 240 | title: 'good link', 241 | description: '1024', 242 | url: 'http://where.is.caoliu/' 243 | }; 244 | 245 | request(app) 246 | .post('/wechat' + tail()) 247 | .send(template(info)) 248 | .expect(200) 249 | .end(function(err, res){ 250 | if (err) return done(err); 251 | var body = res.text.toString(); 252 | body.should.include(''); 253 | body.should.include(''); 254 | body.should.match(/\d{13}<\/CreateTime>/); 255 | body.should.include(''); 256 | body.should.include(''); 257 | done(); 258 | }); 259 | }); 260 | 261 | it('should ok when event location', function (done) { 262 | var info = { 263 | sp: 'nvshen', 264 | user: 'diaosi', 265 | type: 'event', 266 | event: 'LOCATION', 267 | latitude: '23.137466', 268 | longitude: '113.352425', 269 | precision: '119.385040' 270 | }; 271 | 272 | request(app) 273 | .post('/wechat' + tail()) 274 | .send(template(info)) 275 | .expect(200) 276 | .end(function(err, res){ 277 | if (err) return done(err); 278 | var body = res.text.toString(); 279 | body.should.include(''); 280 | body.should.include(''); 281 | body.should.match(/\d{13}<\/CreateTime>/); 282 | body.should.include(''); 283 | body.should.include(''); 284 | done(); 285 | }); 286 | }); 287 | 288 | it('should ok when event enter', function (done) { 289 | var info = { 290 | sp: 'nvshen', 291 | user: 'diaosi', 292 | type: 'event', 293 | event: 'ENTER' 294 | }; 295 | 296 | request(app) 297 | .post('/wechat' + tail()) 298 | .send(template(info)) 299 | .expect(200) 300 | .end(function(err, res){ 301 | if (err) return done(err); 302 | var body = res.text.toString(); 303 | body.should.include(''); 304 | body.should.include(''); 305 | body.should.match(/\d{13}<\/CreateTime>/); 306 | body.should.include(''); 307 | body.should.include(''); 308 | done(); 309 | }); 310 | }); 311 | 312 | it('should ok reply text', function (done) { 313 | var info = { 314 | sp: 'nvshen', 315 | user: 'diaosi', 316 | type: 'text', 317 | text: '测试中' 318 | }; 319 | 320 | request(app) 321 | .post('/wechat' + tail()) 322 | .send(template(info)) 323 | .expect(200) 324 | .end(function(err, res){ 325 | if (err) return done(err); 326 | var body = res.text.toString(); 327 | body.should.include(''); 328 | body.should.include(''); 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should ok reply news', function (done) { 334 | var info = { 335 | sp: 'nvshen', 336 | user: 'gaofushuai', 337 | type: 'text', 338 | text: '测试中' 339 | }; 340 | 341 | request(app) 342 | .post('/wechat' + tail()) 343 | .send(template(info)) 344 | .expect(200) 345 | .end(function(err, res){ 346 | if (err) return done(err); 347 | var body = res.text.toString(); 348 | body.should.include(''); 349 | body.should.include('1'); 350 | body.should.include('<![CDATA[你来我家接我吧]]>'); 351 | body.should.include(''); 352 | body.should.include(''); 353 | body.should.include(''); 354 | done(); 355 | }); 356 | }); 357 | 358 | it('should ok reply music', function (done) { 359 | var info = { 360 | sp: 'nvshen', 361 | user: 'hehe', 362 | type: 'text', 363 | text: '测试中' 364 | }; 365 | 366 | request(app) 367 | .post('/wechat' + tail()) 368 | .send(template(info)) 369 | .expect(200) 370 | .end(function(err, res){ 371 | if (err) return done(err); 372 | var body = res.text.toString(); 373 | body.should.include(''); 374 | body.should.include('<![CDATA[来段音乐吧]]>'); 375 | body.should.include(''); 376 | body.should.include(''); 377 | body.should.include(''); 378 | done(); 379 | }); 380 | }); 381 | }); 382 | }); 383 | -------------------------------------------------------------------------------- /test/wechat_encrypted.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var querystring = require('querystring'); 3 | var request = require('supertest'); 4 | 5 | var tail = function (token, message, get) { 6 | var q = { 7 | timestamp: new Date().getTime(), 8 | encrypt_type: 'aes', 9 | nonce: parseInt((Math.random() * 100000000000), 10) 10 | }; 11 | if (get) { 12 | q.echostr = message; 13 | } 14 | var s = [token, q.timestamp, q.nonce, message].sort().join(''); 15 | q.msg_signature = require('crypto').createHash('sha1').update(s).digest('hex'); 16 | return querystring.stringify(q); 17 | }; 18 | 19 | var tpl = '' + 20 | ']]>' + 21 | ']]>' + 22 | ''; 23 | 24 | var template = require('ejs').compile(tpl); 25 | 26 | var postData = function (token, message) { 27 | var q = { 28 | timestamp: new Date().getTime(), 29 | nonce: parseInt((Math.random() * 100000000000), 10), 30 | encrypt_type: 'aes' 31 | }; 32 | 33 | var s = [token, q.timestamp, q.nonce, message].sort().join(''); 34 | var signature = require('crypto').createHash('sha1').update(s).digest('hex'); 35 | q.msg_signature = signature; 36 | 37 | 38 | var info = { 39 | msg_encrypt: message, 40 | toUser: 'user' 41 | }; 42 | 43 | return { 44 | xml: template(info), 45 | querystring: querystring.stringify(q) 46 | }; 47 | }; 48 | 49 | var connect = require('connect'); 50 | var wechat = require('../'); 51 | var WXBizMsgCrypt = require('wechat-crypto'); 52 | 53 | var app = connect(); 54 | app.use(connect.query()); 55 | var cfg = { 56 | token: 'some token', 57 | appid: 'appid', 58 | encodingAESKey: 'SvFHaQqrlAhRud3ye6f8ujJsR2LeYbxzPPIzNlei2FX' 59 | }; 60 | 61 | app.use('/wechat', wechat(cfg, function (req, res, next) { 62 | res.reply('hehe'); 63 | })); 64 | 65 | describe('wechat_encrypted.js', function () { 66 | var cryptor = new WXBizMsgCrypt(cfg.token, cfg.encodingAESKey, cfg.appid); 67 | 68 | describe('get', function () { 69 | it('should ok', function (done) { 70 | var echoStr = 'node rock'; 71 | var _tail = tail(cfg.token, cryptor.encrypt(echoStr), true); 72 | request(app) 73 | .get('/wechat?' + _tail) 74 | .expect(200) 75 | .expect(echoStr, done); 76 | }); 77 | 78 | it('should not ok', function (done) { 79 | var echoStr = 'node rock'; 80 | var _tail = tail('fake_token', cryptor.encrypt(echoStr), true); 81 | request(app) 82 | .get('/wechat?' + _tail) 83 | .expect(401) 84 | .expect('Invalid signature', done); 85 | }); 86 | }); 87 | 88 | describe('post', function () { 89 | it('should 500', function (done) { 90 | request(app) 91 | .post('/wechat?' + tail(cfg.token, cryptor.encrypt(''), false)) 92 | .expect(500) 93 | .expect(/body is empty/, done); 94 | }); 95 | 96 | it('should 401 invalid signature', function (done) { 97 | var xml = ''; 98 | var data = postData('fake_token', cryptor.encrypt(xml)); 99 | request(app) 100 | .post('/wechat?' + data.querystring) 101 | .send(data.xml) 102 | .expect(401) 103 | .expect('Invalid signature', done); 104 | }); 105 | 106 | it('should 200', function (done) { 107 | var xml = ''; 108 | var data = postData(cfg.token, cryptor.encrypt(xml)); 109 | request(app) 110 | .post('/wechat?' + data.querystring) 111 | .send(data.xml) 112 | .expect(200, done); 113 | }); 114 | }); 115 | 116 | describe('put', function () { 117 | it('should 500', function (done) { 118 | request(app) 119 | .put('/wechat?' + tail(cfg.token, cryptor.encrypt(''), false)) 120 | .expect(501) 121 | .expect(/Not Implemented/, done); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/wechat_multi.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var querystring = require('querystring'); 4 | var request = require('supertest'); 5 | 6 | var connect = require('connect'); 7 | var wechat = require('../'); 8 | 9 | var app = connect(); 10 | app.use(connect.query()); 11 | app.use('/wechat', wechat('some token', function (req, res, next) { 12 | next(); 13 | })); 14 | app.use('/wechat', wechat('some token', function (req, res, next) { 15 | // 微信输入信息都在req.weixin上 16 | res.reply('hehe'); 17 | })); 18 | 19 | describe('wechat.js', function () { 20 | 21 | it('multi wechat should ok', function (done) { 22 | var q = { 23 | timestamp: new Date().getTime(), 24 | nonce: parseInt((Math.random() * 10e10), 10) 25 | }; 26 | var s = ['some token', q.timestamp, q.nonce].sort().join(''); 27 | q.signature = require('crypto').createHash('sha1').update(s).digest('hex'); 28 | q.echostr = 'hehe'; 29 | request(app) 30 | .get('/wechat?' + querystring.stringify(q)) 31 | .expect(200) 32 | .expect('hehe', done); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/wechat_nohandle.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var request = require('supertest'); 4 | var template = require('./support').template; 5 | var tail = require('./support').tail; 6 | 7 | var connect = require('connect'); 8 | var wechat = require('../'); 9 | 10 | var app = connect(); 11 | app.use(connect.query()); 12 | app.use('/wechat', wechat('some token', wechat.text(function (message, req, res, next) { 13 | res.end('hehe'); 14 | }))); 15 | app.use('/wechat', function (req, res, next) { 16 | res.end('next'); 17 | }); 18 | 19 | describe('no handler', function () { 20 | describe('respond', function () { 21 | it('should ok', function (done) { 22 | var info = { 23 | sp: 'nvshen', 24 | user: 'diaosi', 25 | type: 'hehe', 26 | text: '测试中' 27 | }; 28 | 29 | request(app) 30 | .post('/wechat' + tail()) 31 | .send(template(info)) 32 | .expect(200) 33 | .end(function(err, res){ 34 | if (err) { 35 | return done(err); 36 | } 37 | var body = res.text.toString(); 38 | body.should.include('next'); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/wechat_nosignature.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | 3 | var querystring = require('querystring'); 4 | var request = require('supertest'); 5 | var template = require('./support').template; 6 | var tail = require('./support').tail; 7 | 8 | var connect = require('connect'); 9 | var wechat = require('../'); 10 | 11 | var app = connect(); 12 | app.use(connect.query()); 13 | app.use(function (req, res, next) { 14 | if (req.query.rawBody) { 15 | req.rawBody = "\ 16 | \ 17 | 1362161914\ 18 | \ 19 | 30.283878\ 20 | 120.063370\ 21 | 15\ 22 | \ 23 | 5850440872586764820\ 24 | "; 25 | } 26 | next(); 27 | }); 28 | var cfg = { 29 | token: 'some token', 30 | checkSignature: false 31 | }; 32 | app.use('/wechat', wechat(cfg, function (req, res, next) { 33 | // 微信输入信息都在req.weixin上 34 | var info = req.weixin; 35 | // 回复屌丝(普通回复) 36 | if (info.FromUserName === 'diaosi') { 37 | res.reply('hehe'); 38 | } else if (info.FromUserName === 'test') { 39 | res.reply({ 40 | content: 'text object', 41 | type: 'text' 42 | }); 43 | } else if (info.FromUserName === 'hehe') { 44 | res.reply({ 45 | title: "来段音乐吧<", 46 | description: "一无所有>", 47 | musicUrl: "http://mp3.com/xx.mp3?a=b&c=d", 48 | hqMusicUrl: "http://mp3.com/xx.mp3?foo=bar" 49 | }); 50 | } else if (info.FromUserName === 'cs') { 51 | res.transfer2CustomerService(); 52 | } else if (info.FromUserName === 'kf') { 53 | res.transfer2CustomerService('test1@test'); 54 | } else if (info.FromUserName === 'ls') { 55 | res.reply(info.SendLocationInfo.EventKey); 56 | } else if (info.FromUserName === 'pic_weixin') { 57 | res.reply(info.SendPicsInfo.EventKey); 58 | } else if (info.FromUserName === 'web') { 59 | res.reply('web message ok'); 60 | } else if (info.FromUserName === 'empty') { 61 | res.reply(''); 62 | } else { 63 | // 回复高富帅(图文回复) 64 | res.reply([ 65 | { 66 | title: '你来我家接我吧', 67 | description: '这是女神与高富帅之间的对话', 68 | picurl: 'http://nodeapi.cloudfoundry.com/qrcode.jpg', 69 | url: 'http://nodeapi.cloudfoundry.com/' 70 | } 71 | ]); 72 | } 73 | })); 74 | 75 | describe('wechat_nosignaturecheck.js', function () { 76 | 77 | describe('valid GET', function () { 78 | it('should 200', function (done) { 79 | var q = { 80 | timestamp: new Date().getTime(), 81 | nonce: parseInt((Math.random() * 10e10), 10) 82 | }; 83 | q.echostr = 'hehe'; 84 | request(app) 85 | .get('/wechat?' + querystring.stringify(q)) 86 | .expect(200) 87 | .expect('hehe', done); 88 | }); 89 | }); 90 | 91 | describe('respond', function () { 92 | it('should ok', function (done) { 93 | var info = { 94 | sp: 'nvshen', 95 | user: 'diaosi', 96 | type: 'text', 97 | text: '测试中' 98 | }; 99 | 100 | request(app) 101 | .post('/wechat' + tail(false)) 102 | .send(template(info)) 103 | .expect(200) 104 | .end(function(err, res){ 105 | if (err) return done(err); 106 | var body = res.text.toString(); 107 | body.should.include(''); 108 | body.should.include(''); 109 | body.should.match(/\d{13}<\/CreateTime>/); 110 | body.should.include(''); 111 | body.should.include(''); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('should ok with req.rawBody', function (done) { 117 | request(app) 118 | .post('/wechat' + tail(false) + "&rawBody=true") 119 | .send('') 120 | .expect(200) 121 | .end(function (err, res){ 122 | if (err) return done(err); 123 | var body = res.text.toString(); 124 | body.should.include(''); 125 | body.should.include(''); 126 | body.should.match(/\d{13}<\/CreateTime>/); 127 | body.should.include(''); 128 | body.should.include(''); 129 | done(); 130 | }); 131 | }); 132 | 133 | it('should ok with text type object', function (done) { 134 | var info = { 135 | sp: 'nvshen', 136 | user: 'test', 137 | type: 'text', 138 | text: '测试中' 139 | }; 140 | 141 | request(app) 142 | .post('/wechat' + tail(false)) 143 | .send(template(info)) 144 | .expect(200) 145 | .end(function(err, res){ 146 | if (err) return done(err); 147 | var body = res.text.toString(); 148 | body.should.include(''); 149 | body.should.include(''); 150 | body.should.match(/\d{13}<\/CreateTime>/); 151 | body.should.include(''); 152 | body.should.include(''); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should ok with news', function (done) { 158 | var info = { 159 | sp: 'nvshen', 160 | user: 'gaofushuai', 161 | type: 'text', 162 | text: '测试中' 163 | }; 164 | 165 | request(app) 166 | .post('/wechat' + tail(false)) 167 | .send(template(info)) 168 | .expect(200) 169 | .end(function(err, res){ 170 | if (err) return done(err); 171 | var body = res.text.toString(); 172 | body.should.include(''); 173 | body.should.include(''); 174 | body.should.match(/\d{13}<\/CreateTime>/); 175 | body.should.include(''); 176 | body.should.include('1'); 177 | body.should.include('<![CDATA[你来我家接我吧]]>'); 178 | body.should.include(''); 179 | body.should.include(''); 180 | body.should.include(''); 181 | done(); 182 | }); 183 | }); 184 | 185 | it('should ok with music', function (done) { 186 | var info = { 187 | sp: 'nvshen', 188 | user: 'hehe', 189 | type: 'text', 190 | text: '测试中' 191 | }; 192 | 193 | request(app) 194 | .post('/wechat' + tail(false)) 195 | .send(template(info)) 196 | .expect(200) 197 | .end(function(err, res){ 198 | if (err) return done(err); 199 | var body = res.text.toString(); 200 | body.should.include(''); 201 | body.should.include(''); 202 | body.should.match(/\d{13}<\/CreateTime>/); 203 | body.should.include(''); 204 | body.should.include(''); 205 | body.should.include(''); 206 | body.should.include('<![CDATA[来段音乐吧<]]>'); 207 | body.should.include(']]>'); 208 | body.should.include(''); 209 | body.should.include(''); 210 | done(); 211 | }); 212 | }); 213 | 214 | it('should ok with event location_select', function (done) { 215 | var info = { 216 | sp: 'nvshen', 217 | user: 'ls', 218 | type: 'event', 219 | xPos: '80', 220 | yPos: '70', 221 | label: 'alibaba', 222 | event: 'location_select', 223 | eventKey: 'sendLocation', 224 | text: '测试中' 225 | }; 226 | 227 | request(app) 228 | .post('/wechat' + tail(false)) 229 | .send(template(info)) 230 | .expect(200) 231 | .end(function(err, res){ 232 | if (err) return done(err); 233 | var body = res.text.toString(); 234 | body.should.include(''); 235 | body.should.include(''); 236 | body.should.match(/\d{13}<\/CreateTime>/); 237 | body.should.include(''); 238 | body.should.include(''); 239 | done(); 240 | }); 241 | }); 242 | 243 | it('should ok with event pic_weixin', function (done) { 244 | var info = { 245 | sp: 'nvshen', 246 | user: 'pic_weixin', 247 | type: 'event', 248 | event: 'pic_weixin', 249 | eventKey: 'sendPic', 250 | text: '测试中' 251 | }; 252 | 253 | request(app) 254 | .post('/wechat' + tail(false)) 255 | .send(template(info)) 256 | .expect(200) 257 | .end(function(err, res){ 258 | if (err) return done(err); 259 | var body = res.text.toString(); 260 | body.should.include(''); 261 | body.should.include(''); 262 | body.should.match(/\d{13}<\/CreateTime>/); 263 | body.should.include(''); 264 | body.should.include(''); 265 | done(); 266 | }); 267 | }); 268 | 269 | it('should ok with customer service', function (done) { 270 | var info = { 271 | sp: 'gaofushuai', 272 | user: 'cs', 273 | type: 'text', 274 | text: '测试中' 275 | }; 276 | 277 | request(app) 278 | .post('/wechat' + tail(false)) 279 | .send(template(info)) 280 | .expect(200) 281 | .end(function(err, res){ 282 | if (err) return done(err); 283 | var body = res.text.toString(); 284 | body.should.include(''); 285 | body.should.include(''); 286 | body.should.match(/\d{13}<\/CreateTime>/); 287 | body.should.include(''); 288 | done(); 289 | }); 290 | }); 291 | 292 | 293 | it('should ok with transfer info to kfAccount', function(done) { 294 | var info = { 295 | sp: 'zhong', 296 | user: 'kf', 297 | type: 'text', 298 | text: '测试中' 299 | }; 300 | request(app) 301 | .post('/wechat' + tail(false)) 302 | .send(template(info)) 303 | .expect(200) 304 | .end(function(err, res) { 305 | if (err) return done(err); 306 | var body = res.text.toString(); 307 | body.should.include(''); 308 | body.should.include(''); 309 | body.should.match(/\d{13}<\/CreateTime>/); 310 | body.should.include(''); 311 | body.should.include(''); 312 | done(); 313 | }); 314 | }); 315 | 316 | it('should ok with empty message', function (done) { 317 | var info = { 318 | sp: 'nvshen', 319 | user: 'empty', 320 | type: 'text', 321 | text: '测试中' 322 | }; 323 | 324 | request(app) 325 | .post('/wechat' + tail(false)) 326 | .send(template(info)) 327 | .expect(200) 328 | .end(function (err, res) { 329 | if (err) return done(err); 330 | var body = res.text.toString(); 331 | body.should.match(''); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('should ok with web wechat message', function (done) { 337 | var info = { 338 | sp: 'nvshen', 339 | user: 'web', 340 | type: 'text', 341 | text: '测试中' 342 | }; 343 | 344 | request(app) 345 | .post('/wechat' + tail(false)) 346 | .send(template(info)) 347 | .expect(200) 348 | .end(function(err, res){ 349 | if (err) return done(err); 350 | var body = res.text.toString(); 351 | body.should.include(''); 352 | body.should.include(''); 353 | body.should.match(/\d{13}<\/CreateTime>/); 354 | body.should.include(''); 355 | body.should.include(''); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('should pass to next', function (done) { 361 | var info = { 362 | sp: 'nvshen', 363 | user: 'hehe', 364 | type: 'next', 365 | text: '测试中' 366 | }; 367 | 368 | request(app) 369 | .post('/wechat' + tail(false)) 370 | .send(template(info)) 371 | .expect(200) 372 | .end(function(err, res){ 373 | if (err) return done(err); 374 | var body = res.text.toString(); 375 | body.should.include(''); 376 | body.should.include(''); 377 | body.should.match(/\d{13}<\/CreateTime>/); 378 | body.should.include(''); 379 | body.should.include(''); 380 | body.should.include(''); 381 | body.should.include('<![CDATA[来段音乐吧<]]>'); 382 | body.should.include(']]>'); 383 | body.should.include(''); 384 | body.should.include(''); 385 | done(); 386 | }); 387 | }); 388 | }); 389 | 390 | describe('exception', function () { 391 | var xml = '\ 392 | \ 393 | 1362161914\ 394 | \ 395 | 30.283878\ 396 | 120.063370\ 397 | 15\ 398 | \ 399 | 5850440872586764820\ 400 | '; 401 | it('should ok', function () { 402 | request(app) 403 | .post('/wechat' + tail(false)) 404 | .send(xml) 405 | .expect(200) 406 | .end(function(err, res){ 407 | if (err) return done(err); 408 | }); 409 | }); 410 | }); 411 | }); 412 | --------------------------------------------------------------------------------