• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-webot / wechat / 307

pending completion
307

push

travis-ci

JacksonTian
Bump 1.2.3

286 of 311 relevant lines covered (91.96%)

38.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.76
/lib/wechat.js
1
var crypto = require('crypto');
4✔
2
var xml2js = require('xml2js');
4✔
3
var ejs = require('ejs');
4✔
4
var Session = require('./session');
4✔
5
var List = require('./list');
4✔
6
var WXBizMsgCrypt = require('wechat-crypto');
4✔
7

8
/**
9
 * 检查签名
10
 */
11
var checkSignature = function (query, token) {
4✔
12
  var signature = query.signature;
140✔
13
  var timestamp = query.timestamp;
140✔
14
  var nonce = query.nonce;
140✔
15

16
  var shasum = crypto.createHash('sha1');
140✔
17
  var arr = [token, timestamp, nonce].sort();
140✔
18
  shasum.update(arr.join(''));
140✔
19

20
  return shasum.digest('hex') === signature;
140✔
21
};
22

23
/*!
24
 * 响应模版
25
 */
26
var tpl = ['<xml>',
4✔
27
    '<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>',
28
    '<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>',
29
    '<CreateTime><%=createTime%></CreateTime>',
30
    '<MsgType><![CDATA[<%=msgType%>]]></MsgType>',
31
  '<% if (msgType === "news") { %>',
32
    '<ArticleCount><%=content.length%></ArticleCount>',
33
    '<Articles>',
34
    '<% content.forEach(function(item){ %>',
35
      '<item>',
36
        '<Title><![CDATA[<%-item.title%>]]></Title>',
37
        '<Description><![CDATA[<%-item.description%>]]></Description>',
38
        '<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic %>]]></PicUrl>',
39
        '<Url><![CDATA[<%-item.url%>]]></Url>',
40
      '</item>',
41
    '<% }); %>',
42
    '</Articles>',
43
  '<% } else if (msgType === "music") { %>',
44
    '<Music>',
45
      '<Title><![CDATA[<%-content.title%>]]></Title>',
46
      '<Description><![CDATA[<%-content.description%>]]></Description>',
47
      '<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>',
48
      '<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>',
49
      '<ThumbMediaId><![CDATA[<%-content.thumbMediaId || content.mediaId %>]]></ThumbMediaId>',
50
    '</Music>',
51
  '<% } else if (msgType === "voice") { %>',
52
    '<Voice>',
53
      '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
54
    '</Voice>',
55
  '<% } else if (msgType === "image") { %>',
56
    '<Image>',
57
      '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
58
    '</Image>',
59
  '<% } else if (msgType === "video") { %>',
60
    '<Video>',
61
      '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
62
      '<Title><![CDATA[<%-content.title%>]]></Title>',
63
      '<Description><![CDATA[<%-content.description%>]]></Description>',
64
    '</Video>',
65
  '<% } else if (msgType === "transfer_customer_service") { %>',
66
    '<% if (content && content.kfAccount) { %>',
67
      '<TransInfo>',
68
        '<KfAccount><![CDATA[<%-content.kfAccount%>]]></KfAccount>',
69
      '</TransInfo>',
70
    '<% } %>',
71
  '<% } else { %>',
72
    '<Content><![CDATA[<%-content%>]]></Content>',
73
  '<% } %>',
74
  '</xml>'].join('');
75

76
/*!
77
 * 编译过后的模版
78
 */
79
var compiled = ejs.compile(tpl);
4✔
80

81
var wrapTpl = '<xml>' +
4✔
82
  '<Encrypt><![CDATA[<%-encrypt%>]]></Encrypt>' +
83
  '<MsgSignature><![CDATA[<%-signature%>]]></MsgSignature>' +
84
  '<TimeStamp><%-timestamp%></TimeStamp>' +
85
  '<Nonce><![CDATA[<%-nonce%>]]></Nonce>' +
86
'</xml>';
87

88
var encryptWrap = ejs.compile(wrapTpl);
4✔
89

90
var load = function (stream, callback) {
4✔
91
  var buffers = [];
116✔
92
  stream.on('data', function (trunk) {
116✔
93
    buffers.push(trunk);
114✔
94
  });
95
  stream.on('end', function () {
116✔
96
    callback(null, Buffer.concat(buffers));
114✔
97
  });
98
  stream.once('error', callback);
116✔
99
};
100

101
/*!
102
 * 从微信的提交中提取XML文件
103
 */
104
var getMessage = function (stream, callback) {
4✔
105
  load(stream, function (err, buf) {
110✔
106
    if (err) {
110✔
107
      return callback(err);
2✔
108
    }
109
    var xml = buf.toString('utf-8');
108✔
110
    stream.weixin_xml = xml;
108✔
111
    xml2js.parseString(xml, {trim: true}, callback);
108✔
112
  });
113
};
114

115
/*!
116
 * 将xml2js解析出来的对象转换成直接可访问的对象
117
 */
118
var formatMessage = function (result) {
4✔
119
  var message = {};
118✔
120
  if (typeof result === 'object') {
118✔
121
    for (var key in result) {
116✔
122
      if (!(result[key] instanceof Array) || result[key].length === 0) {
602✔
123
        continue;
2✔
124
      }
125
      if (result[key].length === 1) {
600✔
126
        var val = result[key][0];
600✔
127
        if (typeof val === 'object') {
600✔
128
          message[key] = formatMessage(val);
8✔
129
        } else {
130
          message[key] = (val || '').trim();
592✔
131
        }
132
      } else {
133
        message[key] = [];
×
134
        result[key].forEach(function (item) {
×
135
          message[key].push(formatMessage(item));
×
136
        });
137
      }
138
    }
139
  }
140
  return message;
118✔
141
};
142

143
/*!
144
 * 将内容回复给微信的封装方法
145
 */
146
var reply = function (content, fromUsername, toUsername) {
4✔
147
  var info = {};
112✔
148
  var type = 'text';
112✔
149
  info.content = content || '';
112✔
150
  if (Array.isArray(content)) {
112✔
151
    type = 'news';
12✔
152
  } else if (typeof content === 'object') {
100✔
153
    if (content.hasOwnProperty('type')) {
24✔
154
      type = content.type;
14✔
155
      info.content = content.content;
14✔
156
    } else {
157
      type = 'music';
10✔
158
    }
159
  }
160
  info.msgType = type;
112✔
161
  info.createTime = new Date().getTime();
112✔
162
  info.toUsername = toUsername;
112✔
163
  info.fromUsername = fromUsername;
112✔
164
  return compiled(info);
112✔
165
};
166

167
var reply2CustomerService = function (fromUsername, toUsername, kfAccount) {
4✔
168
  var info = {};
8✔
169
  info.msgType = 'transfer_customer_service';
8✔
170
  info.createTime = new Date().getTime();
8✔
171
  info.toUsername = toUsername;
8✔
172
  info.fromUsername = fromUsername;
8✔
173
  info.content = {};
8✔
174
  if (typeof kfAccount === 'string') {
8✔
175
    info.content.kfAccount = kfAccount;
4✔
176
  }
177
  return compiled(info);
8✔
178
};
179

180
var respond = function (handler) {
4✔
181
  return function (req, res, next) {
22✔
182
    var message = req.weixin;
106✔
183
    var callback = handler.getHandler(message.MsgType);
106✔
184
    res.reply = function (content) {
106✔
185
      res.writeHead(200);
96✔
186
      // 响应空字符串,用于响应慢的情况,避免微信重试
187
      if (!content) {
96✔
188
        return res.end('');
2✔
189
      }
190
      var xml = reply(content, message.ToUserName, message.FromUserName);
94✔
191
      if (!req.query.encrypt_type || req.query.encrypt_type === 'raw') {
94✔
192
        res.end(xml);
92✔
193
      } else {
194
        // 判断是否已有前置cryptor
195
        var cryptor = req.cryptor || handler.cryptor;
2✔
196
        var wrap = {};
2✔
197
        wrap.encrypt = cryptor.encrypt(xml);
2✔
198
        wrap.nonce = parseInt((Math.random() * 100000000000), 10);
2✔
199
        wrap.timestamp = new Date().getTime();
2✔
200
        wrap.signature = cryptor.getSignature(wrap.timestamp, wrap.nonce, wrap.encrypt);
2✔
201
        res.end(encryptWrap(wrap));
2✔
202
      }
203
    };
204

205
    // 响应消息,转到客服模式
206
    res.transfer2CustomerService = function (kfAccount) {
106✔
207
      res.writeHead(200);
4✔
208
      res.end(reply2CustomerService(message.ToUserName, message.FromUserName, kfAccount));
4✔
209
    };
210

211
    var done = function () {
106✔
212
      // 如果session中有_wait标记
213
      if (message.MsgType === 'text' && req.wxsession && req.wxsession._wait) {
106✔
214
        var list = List.get(req.wxsession._wait);
14✔
215
        var handle = list.get(message.Content);
14✔
216
        var wrapper = function (message) {
14✔
217
          return handler.handle ? function(req, res) {
4✔
218
            res.reply(message);
2✔
219
          } : function (info, req, res) {
220
            res.reply(message);
2✔
221
          };
222
        };
223

224
        // 如果回复命中规则,则用预置的方法回复
225
        if (handle) {
14✔
226
          callback = typeof handle === 'string' ? wrapper(handle) : handle;
10✔
227
        }
228
      }
229

230
      // 兼容旧API
231
      if (handler.handle) {
106✔
232
        callback(req, res, next);
32✔
233
      } else {
234
        callback(message, req, res, next);
74✔
235
      }
236
    };
237

238
    if (req.sessionStore) {
106✔
239
      var storage = req.sessionStore;
32✔
240
      var _end = res.end;
32✔
241
      var openid = message.FromUserName + ':' + message.ToUserName;
32✔
242
      res.end = function () {
32✔
243
        _end.apply(res, arguments);
32✔
244
        if (req.wxsession) {
32✔
245
          req.wxsession.save();
30✔
246
        }
247
      };
248
      // 等待列表
249
      res.wait = function (name, callback) {
32✔
250
        var list = List.get(name);
6✔
251
        if (list) {
6✔
252
          req.wxsession._wait = name;
4✔
253
          res.reply(list.description);
4✔
254
        } else {
255
          var err = new Error('Undefined list: ' + name);
2✔
256
          err.name = 'UndefinedListError';
2✔
257
          res.writeHead(500);
2✔
258
          res.end(err.name);
2✔
259
          callback && callback(err);
2✔
260
        }
261
      };
262

263
      // 清除等待列表
264
      res.nowait = function () {
32✔
265
        delete req.wxsession._wait;
2✔
266
        res.reply.apply(res, arguments);
2✔
267
      };
268

269
      storage.get(openid, function (err, session) {
32✔
270
        if (!session) {
32✔
271
          req.wxsession = new Session(openid, req);
8✔
272
          req.wxsession.cookie = req.session.cookie;
8✔
273
        } else {
274
          req.wxsession = new Session(openid, req, session);
24✔
275
        }
276
        done();
32✔
277
      });
278
    } else {
279
      done();
74✔
280
    }
281
  };
282
};
283

284
/**
285
 * 微信自动回复平台的内部的Handler对象
286
 * @param {String/Object} config 配置
287
 * @param {Function} handle handle对象
288
 */
289
var Handler = function (token, handle) {
4✔
290
  if (token) {
22✔
291
    this.setToken(token);
14✔
292
  }
293
  this.handlers = {};
22✔
294
  this.handle = handle;
22✔
295
};
296

297
Handler.prototype.setToken = function (token) {
4✔
298
  if (typeof token === 'string') {
22✔
299
    this.token = token;
20✔
300
  } else {
301
    this.token = token.token;
2✔
302
    this.appid = token.appid;
2✔
303
    this.encodingAESKey = token.encodingAESKey;
2✔
304
  }
305
};
306

307
/**
308
 * 设置handler对象
309
 * 按消息设置handler对象的快捷方式
310
 *
311
 * - `text(fn)`
312
 * - `image(fn)`
313
 * - `voice(fn)`
314
 * - `video(fn)`
315
 * - `location(fn)`
316
 * - `link(fn)`
317
 * - `event(fn)`
318
 * @param {String} type handler处理的消息类型
319
 * @param {Function} handle handle对象
320
 */
321
Handler.prototype.setHandler = function (type, fn) {
4✔
322
  this.handlers[type] = fn;
30✔
323
  return this;
30✔
324
};
325

326
['text', 'image', 'voice', 'video', 'location', 'link', 'event'].forEach(function (method) {
4✔
327
  Handler.prototype[method] = function (fn) {
28✔
328
    return this.setHandler(method, fn);
30✔
329
  };
330
});
331

332
/**
333
 * 根据消息类型取出handler对象
334
 * @param {String} type 消息类型
335
 */
336
Handler.prototype.getHandler = function (type) {
4✔
337
  return this.handle || this.handlers[type] || function (info, req, res, next) {
106✔
338
    next();
2✔
339
  };
340
};
341

342
var serveEncrypt = function (that, req, res, next, _respond) {
4✔
343
  var method = req.method;
12✔
344
  // 加密模式
345
  var signature = req.query.msg_signature;
12✔
346
  var timestamp = req.query.timestamp;
12✔
347
  var nonce = req.query.nonce;
12✔
348

349
  // 判断是否已有前置cryptor
350
  var cryptor = req.cryptor || that.cryptor;
12✔
351

352
  if (method === 'GET') {
12✔
353
    var echostr = req.query.echostr;
4✔
354
    if (signature !== cryptor.getSignature(timestamp, nonce, echostr)) {
4✔
355
      res.writeHead(401);
2✔
356
      res.end('Invalid signature');
2✔
357
      return;
2✔
358
    }
359
    var result = cryptor.decrypt(echostr);
2✔
360
    // TODO 检查appId的正确性
361
    res.writeHead(200);
2✔
362
    res.end(result.message);
2✔
363
  } else if (method === 'POST') {
8✔
364
    load(req, function (err, buf) {
6✔
365
      if (err) {
6✔
366
        return next(err);
×
367
      }
368
      var xml = buf.toString('utf-8');
6✔
369
      if (!xml) {
6✔
370
        var emptyErr = new Error('body is empty');
2✔
371
        emptyErr.name = 'Wechat';
2✔
372
        return next(emptyErr);
2✔
373
      }
374
      xml2js.parseString(xml, {trim: true}, function (err, result) {
4✔
375
        if (err) {
4✔
376
          err.name = 'BadMessage' + err.name;
×
377
          return next(err);
×
378
        }
379
        var xml = formatMessage(result.xml);
4✔
380
        var encryptMessage = xml.Encrypt;
4✔
381
        if (signature !== cryptor.getSignature(timestamp, nonce, encryptMessage)) {
4✔
382
          res.writeHead(401);
2✔
383
          res.end('Invalid signature');
2✔
384
          return;
2✔
385
        }
386
        var decrypted = cryptor.decrypt(encryptMessage);
2✔
387
        var messageWrapXml = decrypted.message;
2✔
388
        if (messageWrapXml === '') {
2✔
389
          res.writeHead(401);
×
390
          res.end('Invalid appid');
×
391
          return;
×
392
        }
393
        req.weixin_xml = messageWrapXml;
2✔
394
        xml2js.parseString(messageWrapXml, {trim: true}, function (err, result) {
2✔
395
          if (err) {
2✔
396
            err.name = 'BadMessage' + err.name;
×
397
            return next(err);
×
398
          }
399
          req.weixin = formatMessage(result.xml);
2✔
400
          _respond(req, res, next);
2✔
401
        });
402
      });
403
    });
404
  } else {
405
    res.writeHead(501);
2✔
406
    res.end('Not Implemented');
2✔
407
  }
408
};
409

410
/**
411
 * 根据Handler对象生成响应方法,并最终生成中间件函数
412
 */
413
Handler.prototype.middlewarify = function () {
4✔
414
  var that = this;
22✔
415
  if (this.encodingAESKey) {
22✔
416
    that.cryptor = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid);
2✔
417
  }
418
  var token = this.token;
22✔
419
  var _respond = respond(this);
22✔
420
  return function (req, res, next) {
22✔
421
    // 如果已经解析过了,调用相关handle处理
422
    if (req.weixin) {
152✔
423
      _respond(req, res, next);
×
424
      return;
×
425
    }
426
    if (req.query.encrypt_type && req.query.msg_signature) {
152✔
427
      serveEncrypt(that, req, res, next, _respond);
12✔
428
    } else {
429
      var method = req.method;
140✔
430
      // 动态token,在前置中间件中设置该值req.wechat_token,优先选用
431
      if (!checkSignature(req.query, req.wechat_token || token)) {
140✔
432
        res.writeHead(401);
24✔
433
        res.end('Invalid signature');
24✔
434
        return;
24✔
435
      }
436
      if (method === 'GET') {
116✔
437
        res.writeHead(200);
8✔
438
        res.end(req.query.echostr);
8✔
439
      } else if (method === 'POST') {
108✔
440
        getMessage(req, function (err, result) {
106✔
441
          if (err) {
106✔
442
            err.name = 'BadMessage' + err.name;
2✔
443
            return next(err);
2✔
444
          }
445
          req.weixin = formatMessage(result.xml);
104✔
446
          _respond(req, res, next);
104✔
447
        });
448
      } else {
449
        res.writeHead(501);
2✔
450
        res.end('Not Implemented');
2✔
451
      }
452
    }
453
  };
454
};
455

456
/**
457
 * 根据口令
458
 *
459
 * Examples:
460
 * 使用wechat作为自动回复中间件的三种方式
461
 * ```
462
 * wechat(token, function (req, res, next) {});
463
 *
464
 * wechat(token, wechat.text(function (message, req, res, next) {
465
 *   // TODO
466
 * }).location(function (message, req, res, next) {
467
 *   // TODO
468
 * }));
469
 *
470
 * wechat(token)
471
 *   .text(function (message, req, res, next) {
472
 *     // TODO
473
 *   }).location(function (message, req, res, next) {
474
 *    // TODO
475
 *   }).middlewarify();
476
 * ```
477
 * 加密模式下token为config
478
 *
479
 * ```
480
 * var config = {
481
 *  token: 'token',
482
 *  appid: 'appid',
483
 *  encodingAESKey: 'encodinAESKey'
484
 * };
485
 * wechat(config, function (req, res, next) {});
486
 * ```
487
 *
488
 * 静态方法
489
 *
490
 * - `text`,处理文字推送的回调函数,接受参数为(text, req, res, next)。
491
 * - `image`,处理图片推送的回调函数,接受参数为(image, req, res, next)。
492
 * - `voice`,处理声音推送的回调函数,接受参数为(voice, req, res, next)。
493
 * - `video`,处理视频推送的回调函数,接受参数为(video, req, res, next)。
494
 * - `location`,处理位置推送的回调函数,接受参数为(location, req, res, next)。
495
 * - `link`,处理链接推送的回调函数,接受参数为(link, req, res, next)。
496
 * - `event`,处理事件推送的回调函数,接受参数为(event, req, res, next)。
497
 * @param {String} token 在微信平台填写的口令
498
 * @param {Function} handle 生成的回调函数,参见示例
499
 */
500
var middleware = function (token, handle) {
4✔
501
  if (arguments.length === 1) {
22✔
502
    return new Handler(token);
2✔
503
  }
504

505
  if (handle instanceof Handler) {
20✔
506
    handle.setToken(token);
8✔
507
    return handle.middlewarify();
8✔
508
  } else {
509
    return new Handler(token, handle).middlewarify();
12✔
510
  }
511
};
512

513
['text', 'image', 'voice', 'video', 'location', 'link', 'event'].forEach(function (method) {
4✔
514
  middleware[method] = function (fn) {
28✔
515
    return (new Handler())[method](fn);
8✔
516
  };
517
});
518

519
middleware.toXML = compiled;
4✔
520
middleware.reply = reply;
4✔
521
middleware.reply2CustomerService = reply2CustomerService;
4✔
522
middleware.checkSignature = checkSignature;
4✔
523

524
module.exports = middleware;
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2024 Coveralls, Inc