我该如何回复你
请原谅我并没有去看webchat-api的文档,所以关于这个话题暂时没什么可说的。不过我也没闲着,作为一名能动手就不动口的暴力型男程序员,本着Definitely Re-invent the wheel的学习精神,自己动手做了公众号服务器比较简单的那一部分,被动回复消息。
从上节啃文档啃出来的成果看,微信服务器(小龙出品)会给公众号服务器(小虫出品)的同一个url发两种请求,分别是get
和post
,我们要在公众号服务器中分别处理这两种请求:
- 处理get请求是为了让微信服务器和公众号服务器接头,说白了就是对暗号的过程。微信服务器发过来一个“天王盖地虎”,我们的公众号服务器回一个“宝塔镇河妖”,那肯定是不行的。完成这个过程要借助别人都不知道的token,如果请求中发过来的signature经过验证是有效的,就把echostr还给它,如果无效,就回它“认错人了吧!”。
- 处理post请求是为了回应用户发过来的消息或触发的事件,让用户能跟我们的公众号服务器愉快地玩耍。但因为这些消息和事件是放在xml里发过来的,而且响应的时候也要用xml格式封装好,所以除了业务逻辑,还要处理xml的解析和封装。
说到xml解析,因为有express-xml-bodyparser这样的middleware存在,并且这个轮子也不在我们的学习范围里,就拿过来直接用了。
除此之外,既然只有第二项的业务逻辑部分是不同的,那其他的部分我们就可以像webchat一样,搞一个共用的库。而我们对这个库的要求也很简单:
- 能验证signature
- 能提供json格式的消息给我们
- 能把json格式的返回消息封装成xml
而这个库的用法,我们希望是:
- 在get请求处理函数中把验证signature需要的数据给它,让它告诉我们true还是false
- 在post请求处理函数中把消息或事件给它,让它把要返回的xml数据给我们
- 它在处理消息或事件时,能调用我们提供的消息或事件处理函数,给我们json格式的消息,接收我们函数返回的json结果
综合上面这两种考虑,我想用ES 6的类实现模板方法模式。因为这个类干的是为微信服务器提供服务的工作,我决定管它叫Waiter。我们的Waiter类有三个方法:
- verifySignature:验证signature
- process:处理接收到的消息,调用业务逻辑,将返回结果封装成xml返回
- populateReply:由process调用,子类要实现的业务逻辑就放在这里
总体来说,完成后我们的应用大概是下图这个样子的:
具体实现以keystone为基础,首先来看我们的路由定义:
app.get('/api/weixin',routes.weixin.verify)
app.post('/api/weixin',
xmlparser({
trim: false,
explicitArray: false}),
routes.weixin.handle)
针对/api/weixin的post请求添加了中间件xmlparser。
verify的定义非常简单,只是调用waiter的verifySignature:
import keystone from 'keystone'
exports = module.exports = (req, res) => {
const waiter = keystone.get('weixin waiter')
const {
signature,timestamp,nonce
} = req.query
console.log("get verify request:",req.query)
if(waiter.verifySignature(signature,timestamp,nonce)) {
console.log(`signature ${signature} is valid`)
res.send(req.query.echostr)
} else {
res.send('error')
}
}
handle的定义更简单,把req交给waiter去process,得到结果,将响应的Content-type
设为xml,然后把reply send出去:
import keystone from 'keystone'
exports = module.exports = (req, res) => {
const waiter = keystone.get('weixin waiter')
const reply = waiter.process(req)
res.set('Content-Type', 'text/xml')
res.send(reply)
}
DWaiter
是Waiter
的子类,只实现了populateReply
方法:
class DWaiter extends Waiter {
populateReply(message) {
console.log(message)
var reply = false
switch(message.msgtype) {
case 'text':
reply = {
msgtype:'text',
content: message.content
}
break
case 'image':
reply = {
msgtype:'image',
mediaId: message.mediaid
}
break
case 'voice':
reply = {
msgtype:'voice',
mediaId: message.mediaid
}
break
case 'video':
case 'shortvideo':
case 'location':
case 'link':
case 'event':
default:
break
}
return reply
}
}
这个实现也很简单,只处理了文本、图片和语音三种消息,收到什么就回复什么;其它的全不理。
最后是Waiter类:
class Waiter {
constructor(options) {
this.appId = options.appId
this.appSecret = options.appSecret
this.token = options.token
}
/**
* Verify Signature in request from Weixin
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
* @param signature
* @param timestamp
* @param nonce
*/
verifySignature(signature,timestamp,nonce) {
console.log('verify siganture',signature)
const token = this.token
let shasum = crypto.createHash('sha1');
let arr = [token, timestamp, nonce].sort();
shasum.update(arr.join(''));
return shasum.digest('hex') === signature;
}
/**
* http://mp.weixin.qq.com/wiki/14/89b871b5466b19b3efa4ada8e577d45e.html
*/
process(req) {
const {
signature,timestamp,nonce
} = req.query
var reply = 'success'
if(this.verifySignature(signature,timestamp,nonce)) {
const message = req.body.xml
const _reply = this.populateReply(message)
var replyContent = false
switch(_reply && _reply.msgtype) {
case 'text':
replyContent = `<Content><![CDATA[${_reply.content}]]></Content>`
break
case 'image':
replyContent = `<Image>
<MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
</Image>`
break
case 'voice':
replyContent = `<Voice>
<MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
</Voice>`
break
case 'video':
replyContent = `<Video>
<MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
<Title><![CDATA[${_reply.title}]]></Title>
<Description><![CDATA[${_reply.description}]]></Description>
</Video>`
break
case 'music':
replyContent = `<Music>
<Title><![CDATA[${_reply.title}]]]></Title>
<Description><![CDATA[${_reply.description}]]></Description>
<MusicUrl><![CDATA[${_reply.musicUrl}]]></MusicUrl>
<HQMusicUrl><![CDATA[${_reply.hqMusicUrl}]]></HQMusicUrl>
<ThumbMediaId><![CDATA[${_reply.mediaId}]]></ThumbMediaId>
</Music>`
break
case 'news':
var articleItems = _reply.articles.map((article) => {
return `<item>
<Title><![CDATA[${article.title}]]></Title>
<Description><![CDATA[${article.description}]]></Description>
<PicUrl><![CDATA[${article.picurl}]]></PicUrl>
<Url><![CDATA[${article.url}]]></Url>
</item>`
})
replyContent = `<ArticleCount>${_reply.articles.length}</ArticleCount>
<Articles>
${articleItems.join('')}
</Articles>`
break
default:
break
}
replay = replyContent ?
`<xml>
<ToUserName><![CDATA[${message.fromusername}]]></ToUserName>
<FromUserName><![CDATA[${message.tousername}]]></FromUserName>
<CreateTime><![CDATA[${Date.now()}]]></CreateTime>
<MsgType><![CDATA[${_reply.msgtype}]]></MsgType>
${replayContent}
</xml>`
:
false
}
return reply
}
populateReply(message) {
const reply = {
msgtype : 'text',
content: '已收到您的消息!'
}
return reply
}
}
代码也非常简单直白,verifySignature
跟webchat的是一样一样一样的,process
在签名验证通过后,从req.body.xml
中获得解析好的消息或事件,交给populateReply
,然后根据populateReply
返回的消息类型封装成不同的xml数据。
就酱!