node-weixin-media-platform-api
Version:
weixin media platform common api
646 lines (614 loc) • 21.8 kB
JavaScript
// let _ = require('lodash')
const crypto = require('crypto')
const http = require('request')
const async = require('async')
const moment = require('moment')
const parseString = require('xml2js').parseString
const axios = require('axios')
const token = require('./libs/token')
module.exports = () => {
return {
init: (wxConfig) => {
const sdk = {
token: wxConfig.token,
appId: wxConfig.appId,
appSecret: wxConfig.appSecret,
accessToken: '',
expireTime: 0,
expireTimeJS: 0,
jsApiTicket: '',
domain: wxConfig.domain,
bizDomain: wxConfig.bizDomain,
debug: wxConfig.debug,
redisConfig: wxConfig.redisConfig
}
// let updateToken, getToken
if (sdk.redisConfig) {
token.setRedisStore(sdk)
} else {
token.setFileStore(sdk)
}
// 返回 timeStamp
const getTimeStamp = () => {
return moment().format('X')
}
// private: 构造nonceStr
const getNonceStr = () => {
const $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const maxPos = $chars.length
let noceStr = ''
for (let i = 0; i < 32; i++) {
noceStr += $chars.charAt(Math.floor(Math.random() * maxPos))
}
return noceStr
}
// private: 参数签名
const generateSign = (obj) => {
const params = Object.keys(obj).sort().map(key => `${key}=` + obj[key])
return crypto.createHash('sha1').update(params.join('&')).digest('hex')
}
// exports.generateSign = generateSign
// private: 刷新token
const getAccessToken = (fn) => {
// 先取一下accessToken
token.getAccessToken(sdk, async (error, accessToken) => {
sdk.accessToken = accessToken.accessToken
sdk.expireTime = accessToken.expireTime
if (new Date().getTime() > sdk.expireTime) {
// 如果token超时,或者没有token(expireTime 初始值为0)
// 则开始获取token
const url = `${sdk.domain}/cgi-bin/token?grant_type=client_credential&appid=${sdk.appId}&secret=${sdk.appSecret}`
const response = await axios.get(url)
if (response.data.errcode) {
console.error('getAccessToken', response.data)
fn(response.data.errmsg, null)
} else {
const { access_token, expires_in } = response.data
token.setAccessToken(sdk, access_token, expires_in)
sdk.accessToken = access_token
sdk.expireTime = expires_in
fn(null, sdk.accessToken)
}
} else {
fn(null, sdk.accessToken)
}
})
}
// get access token的同步版本
const getAccessTokenSync = () => {
return new Promise((resolve, reject) => {
getAccessToken((error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
const getJsApiTicket = (fn) => {
// 先获取一下jsApiTicket
token.getJSTicket(sdk, (error, jsTicket) => {
sdk.expireTimeJS = jsTicket.expireTime
sdk.jsApiTicket = jsTicket.jsApiTicket
if (new Date().getTime() > sdk.expireTimeJS) {
// 如果超时了,或者还没刷新过
async.series([
getAccessToken,
(cb) => {
const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${sdk.accessToken}&type=jsapi`
http.get(url, { json: true }, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
// TODO: 核实一下 有没有这个errmsg
cb(body.errmsg, null)
} else {
cb(null, body)
}
}
})
}
], (error, result) => {
if (error) {
console.log('jsApiTicket Error %s', error)
fn(error, null)
} else {
const jsTicket = token.setJSTicket(sdk, result[1].ticket, result[1].expires_in)
sdk.jsApiTicket = jsTicket.jsApiTicket
sdk.expireTimeJS = jsTicket.expireTime
fn(null, sdk.jsApiTicket)
}
})
} else {
// 如果没有过期
fn(null, sdk.jsApiTicket)
}
})
}
sdk.parseXml = (xmlString, fn) => {
parseString(xmlString, {
explicitArray: false
}, (error, json) => {
if (error) {
fn(new Error('xmlParseError'), null)
} else {
fn(null, json)
}
})
}
// 检查签名是否正确
sdk.checkSign = (signature, timestamp, nonce, fn) => {
const str = [sdk.token, timestamp, nonce].sort().join('')
const mySign = crypto.createHash('sha1').update(str).digest('hex')
if (mySign === signature) {
fn(null, null)
} else {
fn(new Error('signError'), null)
}
}
sdk.text = (from, to, content) => {
const now = +new Date()
return `<xml><ToUserName><![CDATA[${to}]]></ToUserName><FromUserName><![CDATA[${from}]]></FromUserName><CreateTime>${now}</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[${content}]]></Content></xml>`
}
// 回复图片消息
// 首先将图片上传到微信服务器
// type: 图片(image)、语音(voice)、视频(video)和缩略图(thumb)
// 这里直接返回了需要返回给用户的xml结构
sdk.media = (from, to, type, fileBuffer, fn) => {
async.auto({
getAccessToken,
upload: ['getAccessToken', (results, cb) => {
const url = `${wxConfig.domain}/cgi-bin/media/upload?access_token=${sdk.accessToken}&type=${type}`
const r = http.post(url, {
json: true
}, (error, response, body) => {
if (error) {
console.log(error)
cb(error, null)
} else if (body.errcode) {
console.log(body.errmsg)
cb(body.errmsg, null)
} else {
cb(null, body)
}
})
r.form().append('file', fileBuffer, {
filename: 'myFile.png',
contentType: 'image/png'
})
}]
}, (error, result) => {
const now = +new Date()
if (error) {
console.log('send media error:')
console.log('accesstoken: %s expireTime: %s', sdk.accessToken, sdk.expireTime)
fn(null, `<xml><ToUserName><![CDATA[${to}]]></ToUserName><FromUserName><![CDATA[${from}]]></FromUserName><CreateTime>${now}</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[获取失败,请重试!]]></Content></xml>`)
} else {
// console.log(result.upload)
const mediaInfo = `<Image><MediaId><![CDATA[${result.upload.media_id}]]></MediaId></Image>`
fn(null, `<xml><ToUserName><![CDATA[${to}]]></ToUserName><FromUserName><![CDATA[${from}]]></FromUserName><CreateTime>${now}</CreateTime><MsgType><![CDATA[${type}]]></MsgType>${mediaInfo}</xml>`)
}
})
}
sdk.oAuth = (code, fn) => {
http.get(`${wxConfig.domain}/sns/oauth2/access_token`, {
qs: {
appid: sdk.appId,
secret: sdk.appSecret,
code,
grant_type: 'authorization_code'
},
json: true
}, (error, response, body) => {
if (error) {
fn(error, null)
} else {
if (body.errcode) {
fn(body.errcode, null)
} else {
fn(null, body.openid)
}
}
})
}
// 获取已创建标签
// {
// "tags":[
// {
// "id":1,
// "name":"每天一罐可乐星人",
// "count":0
// },
// {
// "id":2,
// "name":"星标组",
// "count":0
// },
// {
// "id":127,
// "name":"广东",
// "count":5
// }
// ]
// }
sdk.getTags = async () => {
try {
await getAccessTokenSync()
} catch (e) {
//
}
return axios.get(`https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${sdk.accessToken}`)
}
// 批量为用户打标签
// 一次最多为50个openIds打标签
sdk.addTag = async (openid_list, tagid) => {
try {
await getAccessTokenSync()
} catch (e) {
//
}
const url = `https://api.weixin.qq.com/cgi-bin/tags/members/batchtagging?access_token=${sdk.accessToken}`
return axios.post(url, {
openid_list,
tagid
})
}
// 用户授权 获取SNS信息 需要跳转的方式为 snsapi_userinfo
sdk.oAuthSNS = (code, fn) => {
async.auto({
auth: (cb) => {
http.get(`${wxConfig.domain}/sns/oauth2/access_token`, {
qs: {
appid: sdk.appId,
secret: sdk.appSecret,
code,
grant_type: 'authorization_code'
},
json: true
}, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errcode, null)
} else {
cb(null, body)
}
}
})
},
profile: ['auth', (results, cb) => {
const url = `${wxConfig.domain}/sns/userinfo?access_token=${results.auth.access_token}&openid=${results.auth.openid}&lang=zh_CN`
http.get(url, {
json: true
}, (error, response, body) => {
if (error) {
cb(error, {})
} else {
if (body.errcode) {
cb(body.errmsg, {})
} else {
cb(null, body)
}
}
})
}]
}, (error, result) => {
fn(error, result.profile)
})
}
sdk.getConfigParams = (url, debug, jsApiList, cb) => {
const appId = sdk.appId
const noncestr = getNonceStr()
const timestamp = getTimeStamp()
getJsApiTicket((error, jsapi_ticket) => {
const signature = generateSign({ jsapi_ticket, noncestr, timestamp, url })
cb(error, {
debug,
appId,
timestamp,
nonceStr: noncestr,
signature,
jsApiList
})
})
}
// 创建菜单
sdk.createMenu = (menuConfig, fn) => {
async.auto({
getAccessToken,
createMenu: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/menu/create?access_token=${sdk.accessToken}`
http.post(url, { json: true, form: menuConfig }, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
cb(null, null)
}
}
})
}]
}, fn)
}
/**
* https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html
* 中搜索 客服接口-发消息
*
* 发送文字消息
{
"touser":"OPENID",
"msgtype":"text",
"text":
{
"content":"Hello World"
}
}
中间可以插入小程序链接(其中href中指定的链接是在客户端不支持小程序的时候使用的跳转页面)
<a href="http://www.qq.com"
data-miniprogram-appid="appid"
data-miniprogram-path="pages/index/index">
点击跳小程序
</a>
发送小程序卡片
{
"touser":"OPENID",
"msgtype":"miniprogrampage",
"miniprogrampage":
{
"title":"title",
"appid":"appid",
"pagepath":"pagepath",
"thumb_media_id":"thumb_media_id"
}
}
*/
sdk.sendKfMsg = (form, fn) => {
async.auto({
getAccessToken,
delMenu: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/message/custom/send?access_token=${sdk.accessToken}`
http.post(url, { json: form }, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
cb(null, null)
}
}
})
}]
}, fn)
}
// 删除菜单
sdk.delMenu = (fn) => {
async.auto({
getAccessToken,
delMenu: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/menu/delete?access_token=${sdk.accessToken}`
http.post(url, { json: true }, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
cb(null, null)
}
}
})
}]
}, fn)
}
// 根据openId获取用户信息(这个是获取关注过公众号的用户的Profile)
sdk.getProfile = (openId, fn) => {
async.auto({
getAccessToken,
profile: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/user/info?access_token=${sdk.accessToken}&openid=${openId}&lang=zh_CN`
http.post(url, {
json: true
}, (error, response, body) => {
if (error) {
cb(error, {})
} else {
if (body.errcode) {
cb(body.errmsg, {})
} else {
cb(null, body)
}
}
})
}]
}, (error, result) => {
if (error) {
fn(error, {})
} else {
fn(null, result.profile)
}
})
}
// 获取带参数的二维码,返回一个buffer
sdk.getMPQRCode = (sceneId, fn) => {
async.auto({
getAccessToken,
ticket: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/qrcode/create?access_token=${sdk.accessToken}`
http.post(url, {
json: {
action_name: 'QR_LIMIT_STR_SCENE',
action_info: {
scene: {
scene_str: sceneId
}
}
}
}, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
console.log(encodeURIComponent(body.ticket))
console.log((body.ticket))
cb(null, body.ticket)
}
}
})
}],
qrCode: ['ticket', (results, cb) => {
// 注意:这个地方不要傻逼呵呵的用${wxConfig.domain} 这个是mp.weixin.qq.com 再说一遍,这个是 mp.weixin.qq.com
const url = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${results.ticket}`
console.log(url)
http.get(url, {
encoding: null
}, (error, response, body) => {
if (error) {
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
cb(null, body)
}
}
})
}]
}, (error, result) => {
if (error) {
// 如果报错,则生成一个空数组
fn(error, [])
} else {
fn(null, result.qrCode)
}
})
}
// 发送客服消息(模板消息)
// to: 发给谁? openId
// templateId: 客服模板编号
// link: 点击模板后到达的页面
// customData: 在指定模板中自定义的消息
/**
* 发送模板消息
* to
* templateId
* link
* customData
* miniprogram {appid: 'wxe54fe812586edb10', path: ''}
* fn
*/
sdk.sendTemplateMsg = (to, templateId, link, customData, miniprogram, fn) => {
if (typeof miniprogram === 'function') {
fn = miniprogram
}
async.auto({
getAccessToken,
send: ['getAccessToken', (dummy, cb) => {
const url = `${wxConfig.domain}/cgi-bin/message/template/send?access_token=${sdk.accessToken}`
http.post(url, {
json: {
touser: to,
template_id: templateId,
url: link,
data: customData,
miniprogram
}
}, (error, response, body) => {
if (error) {
console.error(`sendTemplateMsg`, 'accessToken:', sdk.accessToken, 'accessToken', sdk.expireTime)
cb(error, null)
} else {
if (body.errcode) {
cb(body.errmsg, null)
} else {
cb(null, null)
}
}
})
}]
}, fn)
}
// 静默授权
// 静默授权后,session中自然会带openId
sdk.auth = (req, res, next) => {
const { URL } = require('url')
if (req.session.openId) {
next()
} else {
const { code } = req.query
if (code) {
sdk.oAuth(code, async (error, result) => {
if (!error) {
req.session.openId = result
next()
} else {
const originUrl = new URL(`${req.protocol}://${req.hostname}${req.originalUrl}`)
originUrl.searchParams.delete('code')
const searchParams = originUrl.searchParams.toString()
const url = encodeURIComponent(`${originUrl.origin}${originUrl.pathname}?${searchParams}`)
res.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${url}&response_type=code&scope=snsapi_base&state=rta#wechat_redirect`)
}
})
} else {
const url = encodeURIComponent(`${req.protocol}://${req.hostname}${req.originalUrl}`)
res.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${url}&response_type=code&scope=snsapi_base&state=rta#wechat_redirect`)
}
}
}
// 带信息的授权
sdk.snsAuth = (req, res, next) => {
const { URL } = require('url')
const { code } = req.query
if (req.session.headImageUrl) {
// 如果session中已经加载过headImageUrl表示已经做个SNA授权
next()
} else {
if (code) {
sdk.oAuthSNS(code, (error, result) => {
if (!error) {
req.session.openId = result.openid
req.session.nickName = result.nickname
req.session.sex = result.sex === 2 ? 0 : result.sex
req.session.headImageUrl = result.headimgurl
req.session.wxProfile = result
next()
} else {
const originUrl = new URL(`${req.protocol}://${req.hostname}${req.originalUrl}`)
originUrl.searchParams.delete('code')
const searchParams = originUrl.searchParams.toString()
const url = `${wxConfig.bizDomain}${originUrl.pathname}?${searchParams}`
res.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${url}&response_type=code&scope=snsapi_userinfo&state=rta#wechat_redirect`)
}
})
} else {
const url = `${wxConfig.bizDomain}${req.originalUrl}`
res.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${url}&response_type=code&scope=snsapi_userinfo&state=rta#wechat_redirect`)
}
}
}
// 仅仅在第一次接入的时候有用,以后用不上了
sdk.verifyEcho = (req, res) => {
const { signature, timestamp, nonce, echostr } = req.query
sdk.checkSign(signature, timestamp, nonce, (error) => {
if (error) {
res.send('error')
} else {
if (echostr === null) {
res.send('error')
} else {
res.send(echostr)
}
}
})
}
return sdk
}
}
}