UNPKG

koahub-wechat

Version:
319 lines (287 loc) 9.22 kB
'use strict'; import getRawBody from 'raw-body' import xml2js from 'xml2js' import crypto from 'crypto' import ejs from 'ejs' import WXBizMsgCrypt from 'wechat-crypto' var wechat = function (config) { if (!(this instanceof wechat)) { return new wechat(config); } this.setToken(config); }; wechat.prototype.setToken = function (config) { 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 Error('please check your config'); } }; var getSignature = function (timestamp, nonce, token) { var shasum = crypto.createHash('sha1'); var arr = [token, timestamp, nonce].sort(); shasum.update(arr.join('')); return shasum.digest('hex'); }; var parseXML = async (xml) => { return new Promise((resolve, reject) => { xml2js.parseString(xml, {trim: true}, function (err, json) { if (err) reject(err); else resolve(json); }); }); } /*! * 将xml2js解析出来的对象转换成直接可访问的对象 */ var formatMessage = function (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].forEach(function (item) { message[key].push(formatMessage(item)); }); } } } return message; }; /*! * 响应模版 */ 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 %>]]></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(''); /*! * 编译过后的模版 */ 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); /*! * 将内容回复给微信的封装方法 */ var reply = function (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); }; var reply2CustomerService = function (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); }; wechat.prototype.middleware = function (handle) { var that = this; if (this.encodingAESKey) { that.cryptor = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid); } return async (ctx,next) => { var query = ctx.query; // 加密模式 var encrypted = !!(query.encrypt_type && query.encrypt_type === 'aes' && query.msg_signature); var timestamp = query.timestamp; var nonce = query.nonce; var echostr = query.echostr; var method = ctx.method; if (method === 'GET') { var valid = false; if (encrypted) { var signature = query.msg_signature; valid = signature === that.cryptor.getSignature(timestamp, nonce, echostr); } else { // 校验 valid = query.signature === getSignature(timestamp, nonce, that.token); } if (!valid) { ctx.status = 401; ctx.body = 'Invalid signature'; } else { if (encrypted) { var decrypted = that.cryptor.decrypt(echostr); // TODO 检查appId的正确性 ctx.body = decrypted.message; } else { ctx.body = echostr; } } } else if (method === 'POST') { if (!encrypted) { // 校验 if (query.signature !== getSignature(timestamp, nonce, that.token)) { ctx.status = 401; ctx.body = 'Invalid signature'; return; } } // 取原始数据 var xml = await getRawBody(ctx.req, { length: ctx.request.length, limit: '1mb', encoding: ctx.request.charset }); ctx.state.weixin_xml = xml; // 解析xml var result = await parseXML(xml); var formated = formatMessage(result.xml); if (encrypted) { var encryptMessage = formated.Encrypt; if (query.msg_signature !== that.cryptor.getSignature(timestamp, nonce, encryptMessage)) { ctx.status = 401; ctx.body = 'Invalid signature'; return; } var decryptedXML = that.cryptor.decrypt(encryptMessage); var messageWrapXml = decryptedXML.message; if (messageWrapXml === '') { ctx.status = 401; ctx.body = 'Invalid signature'; return; } var decodedXML = await parseXML(messageWrapXml); formated = formatMessage(decodedXML.xml); } // 挂载处理后的微信消息 ctx.state.weixin = formated; // 取session数据 /* if (this.sessionStore) { this.wxSessionId = formated.FromUserName; this.wxsession = await this.sessionStore.get(this.wxSessionId); if (!this.wxsession) { this.wxsession = {}; this.wxsession.cookie = this.session.cookie; } }*/ // 业务逻辑处理 await handle(ctx); // 更新session /* if (this.sessionStore) { if (!this.wxsession) { if (this.wxSessionId) { await this.sessionStore.destroy(this.wxSessionId); } } else { await this.sessionStore.set(this.wxSessionId, this.wxsession); } }* /* * 假如服务器无法保证在五秒内处理并回复,可以直接回复空串。 * 微信服务器不会对此作任何处理,并且不会发起重试。 */ if (ctx.body === '') { return; } var replyMessageXml = reply(ctx.body, formated.ToUserName, formated.FromUserName); if (!query.encrypt_type || query.encrypt_type === 'raw') { ctx.body = replyMessageXml; } else { var wrap = {}; wrap.encrypt = that.cryptor.encrypt(replyMessageXml); wrap.nonce = parseInt((Math.random() * 100000000000), 10); wrap.timestamp = new Date().getTime(); wrap.signature = that.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 = wechat;