co-wechat
Version:
wechat api for co
303 lines (273 loc) • 9.24 kB
JavaScript
'use strict';
const getRawBody = require('raw-body');
const xml2js = require('xml2js');
const crypto = require('crypto');
const ejs = require('ejs');
const WXBizMsgCrypt = require('wechat-crypto');
function getSignature (timestamp, nonce, token) {
var shasum = crypto.createHash('sha1');
var arr = [token, timestamp, nonce].sort();
shasum.update(arr.join(''));
return shasum.digest('hex');
}
function parseXML (xml) {
return new Promise((resolve, reject) => {
xml2js.parseString(xml, {trim: true}, function (err, obj) {
if (err) {
return reject(err);
}
resolve(obj);
});
});
}
/*!
* 将xml2js解析出来的对象转换成直接可访问的对象
*/
function formatMessage (result) {
var message = {};
if (typeof result === 'object') {
for (var key in result) {
if (!(result[key] instanceof Array) || result[key].length === 0) {
continue;
}
if (result[key].length === 1) {
var val = result[key][0];
if (typeof val === 'object') {
message[key] = formatMessage(val);
} else {
message[key] = (val || '').trim();
}
} else {
message[key] = result[key].map(function (item) {
return formatMessage(item);
});
}
}
}
return message;
}
/*!
* 响应模版
*/
/* eslint-disable indent */
var tpl = ['<xml>',
'<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>',
'<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>',
'<CreateTime><%=createTime%></CreateTime>',
'<MsgType><![CDATA[<%=msgType%>]]></MsgType>',
'<% if (msgType === "news") { %>',
'<ArticleCount><%=content.length%></ArticleCount>',
'<Articles>',
'<% content.forEach(function(item){ %>',
'<item>',
'<Title><![CDATA[<%-item.title%>]]></Title>',
'<Description><![CDATA[<%-item.description%>]]></Description>',
'<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic || item.thumb_url %>]]></PicUrl>',
'<Url><![CDATA[<%-item.url%>]]></Url>',
'</item>',
'<% }); %>',
'</Articles>',
'<% } else if (msgType === "music") { %>',
'<Music>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>',
'<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>',
'</Music>',
'<% } else if (msgType === "voice") { %>',
'<Voice>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Voice>',
'<% } else if (msgType === "image") { %>',
'<Image>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Image>',
'<% } else if (msgType === "video") { %>',
'<Video>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'</Video>',
'<% } else if (msgType === "transfer_customer_service") { %>',
'<% if (content && content.kfAccount) { %>',
'<TransInfo>',
'<KfAccount><![CDATA[<%-content.kfAccount%>]]></KfAccount>',
'</TransInfo>',
'<% } %>',
'<% } else { %>',
'<Content><![CDATA[<%-content%>]]></Content>',
'<% } %>',
'</xml>'].join('');
/* eslint-enable indent */
/*!
* 编译过后的模版
*/
var compiled = ejs.compile(tpl);
var wrapTpl = '<xml>' +
'<Encrypt><![CDATA[<%-encrypt%>]]></Encrypt>' +
'<MsgSignature><![CDATA[<%-signature%>]]></MsgSignature>' +
'<TimeStamp><%-timestamp%></TimeStamp>' +
'<Nonce><![CDATA[<%-nonce%>]]></Nonce>' +
'</xml>';
var encryptWrap = ejs.compile(wrapTpl);
function reply2CustomerService (fromUsername, toUsername, kfAccount) {
var info = {};
info.msgType = 'transfer_customer_service';
info.createTime = new Date().getTime();
info.toUsername = toUsername;
info.fromUsername = fromUsername;
info.content = {};
if (typeof kfAccount === 'string') {
info.content.kfAccount = kfAccount;
}
return compiled(info);
}
/*!
* 将内容回复给微信的封装方法
*/
function reply (content, fromUsername, toUsername) {
var info = {};
var type = 'text';
info.content = content || '';
if (Array.isArray(content)) {
type = 'news';
} else if (typeof content === 'object') {
if (content.hasOwnProperty('type')) {
if (content.type === 'customerService') {
return reply2CustomerService(fromUsername, toUsername, content.kfAccount);
}
type = content.type;
info.content = content.content;
} else {
type = 'music';
}
}
info.msgType = type;
info.createTime = new Date().getTime();
info.toUsername = toUsername;
info.fromUsername = fromUsername;
return compiled(info);
}
class Wechat {
constructor(config, isDebug) {
this.isDebug = isDebug;
if (typeof config === 'string') {
this.token = config;
} else if (typeof config === 'object' && config.token) {
this.token = config.token;
this.appid = config.appid || '';
this.encodingAESKey = config.encodingAESKey || '';
} else {
throw new TypeError('please check your config');
}
}
middleware(handle) {
if (this.encodingAESKey) {
this.cryptor = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid);
}
return async (ctx, next) => {
const query = ctx.query;
// 加密模式
const encrypted = !!(query.encrypt_type && query.encrypt_type === 'aes' && query.msg_signature);
const timestamp = query.timestamp;
const nonce = query.nonce;
const echostr = query.echostr;
const method = ctx.method;
const TOKEN = ctx.wx_token || this.token;
const CRYPTOR = ctx.wx_cryptor || this.cryptor;
if (method === 'GET') {
var valid = false;
if (encrypted) {
var signature = query.msg_signature;
valid = signature === CRYPTOR.getSignature(timestamp, nonce, echostr);
} else {
// 校验
valid = query.signature === getSignature(timestamp, nonce, TOKEN);
}
if (!valid) {
ctx.status = 401;
ctx.body = 'Invalid signature';
} else {
if (encrypted) {
var decrypted = CRYPTOR.decrypt(echostr);
// TODO 检查appId的正确性
ctx.body = decrypted.message;
} else {
ctx.body = echostr;
}
}
} else if (method === 'POST') {
if (!this.isDebug && !encrypted) {
// 校验
if (query.signature !== getSignature(timestamp, nonce, TOKEN)) {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
}
var xml;
if (ctx.request.body && typeof ctx.request.body === 'string') {
xml = ctx.request.body;
} else {
// 取原始数据
xml = await getRawBody(ctx.req, {
length: ctx.request.length,
limit: '1mb',
encoding: ctx.request.charset || 'utf-8'
});
}
// 保存原始xml
ctx.weixin_xml = xml;
// 解析xml
var result = await parseXML(xml);
var formatted = formatMessage(result.xml);
if (!this.isDebug && encrypted) {
var encryptMessage = formatted.Encrypt;
if (query.msg_signature !== CRYPTOR.getSignature(timestamp, nonce, encryptMessage)) {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
var decryptedXML = CRYPTOR.decrypt(encryptMessage);
var messageWrapXml = decryptedXML.message;
if (messageWrapXml === '') {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
var decodedXML = await parseXML(messageWrapXml);
formatted = formatMessage(decodedXML.xml);
}
// 业务逻辑处理
// 注意不要在业务逻辑中操作 body、type
const body = await handle(formatted, ctx);
/*
* 假如服务器无法保证在五秒内处理并回复,可以直接回复空串。
* 微信服务器不会对此作任何处理,并且不会发起重试。
*/
if (body === '') {
ctx.body = '';
return;
}
var replyMessageXml = reply(body, formatted.ToUserName, formatted.FromUserName);
if (this.isDebug || !query.encrypt_type || query.encrypt_type === 'raw') {
ctx.body = replyMessageXml;
} else {
var wrap = {};
wrap.encrypt = CRYPTOR.encrypt(replyMessageXml);
wrap.nonce = parseInt((Math.random() * 100000000000), 10);
wrap.timestamp = new Date().getTime();
wrap.signature = CRYPTOR.getSignature(wrap.timestamp, wrap.nonce, wrap.encrypt);
ctx.body = encryptWrap(wrap);
}
ctx.type = 'application/xml';
} else {
ctx.status = 501;
ctx.body = 'Not Implemented';
}
};
}
}
module.exports = function (config, isDebug) {
return new Wechat(config, isDebug);
};