diff --git a/Makefile b/Makefile index b411e8dd..a9fbd0ee 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ test: $(TESTS) test-cov: - @$(MAKE) test REPORTER=dot @$(MAKE) test MOCHA_OPTS='--require blanket' REPORTER=html-cov > coverage.html @$(MAKE) test MOCHA_OPTS='--require blanket' REPORTER=travis-cov diff --git a/index.js b/index.js index ba85520e..ba111b50 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,21 @@ var wechat = require('./lib/wechat'); wechat.List = require('./lib/list'); -wechat.API = require('./lib/common'); +var API = require('./lib/api_common'); +// 菜单接口 +API.mixin(require('./lib/api_menu')); +// 分组管理 +API.mixin(require('./lib/api_group')); +// 用户信息 +API.mixin(require('./lib/api_user')); +// 二维码 +API.mixin(require('./lib/api_qrcode')); +// 客服消息 +API.mixin(require('./lib/api_customer')); +// 媒体管理(上传、下载) +API.mixin(require('./lib/api_media')); +// 支付接口 +API.mixin(require('./lib/api_pay')); +wechat.API = API; wechat.OAuth = require('./lib/oauth'); +wechat.util = require('./lib/util'); module.exports = wechat; diff --git a/lib/api_common.js b/lib/api_common.js new file mode 100644 index 00000000..b8cc994b --- /dev/null +++ b/lib/api_common.js @@ -0,0 +1,142 @@ +var inherits = require('util').inherits; +var EventEmitter = require('events').EventEmitter; +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; + +var AccessToken = function (accessToken, expireTime) { + if (!(this instanceof AccessToken)) { + return new AccessToken(accessToken, expireTime); + } + this.accessToken = accessToken; + this.expireTime = expireTime; +}; + +/** + * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比 + * + * Examples: + * ``` + * api.isAccessTokenValid(); + * ``` + */ +AccessToken.prototype.isValid = function () { + return !!this.accessToken && (new Date().getTime()) < this.expireTime; +}; + +/** + * 根据appid和appsecret创建API的构造函数 + * + * Examples: + * ``` + * var API = require('wechat').API; + * var api = new API('appid', 'secret'); + * api.on('token', function (token) { + * // 请将token存储到全局,跨进程级别的全局 + * // 比如写到数据库、redis等 + * // 这样才能在cluster模式下使用 + * }); + * // 或者 + * getTokenFromGlobal(function (token) { + * // var api = new API('appid', 'secret', token); + * }); + * ``` + * @param {String} appid 在公众平台上申请得到的appid + * @param {String} appsecret 在公众平台上申请得到的app secret + * @param {Object} token 可选的。全局token对象,该对象在调用一次`getAccessToken`后得到 + */ +var API = function (appid, appsecret, token) { + this.appid = appid; + this.appsecret = appsecret; + if (token) { + this.token = AccessToken(token.accessToken, token.expireTime); + } + this.prefix = 'https://api.weixin.qq.com/cgi-bin/'; + this.mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/'; + this.fileServerPrefix = 'http://file.api.weixin.qq.com/cgi-bin/'; + this.payPrefix = 'https://api.weixin.qq.com/pay/'; + EventEmitter.call(this); +}; +inherits(API, EventEmitter); + +/** + * 根据创建API时传入的appid和appsecret获取access token + * 进行后续所有API调用时,需要先获取access token + * 详细请看: + * + * Examples: + * ``` + * api.getAccessToken(callback); + * ``` + * Callback: + * + * - `err`, 获取access token出现异常时的异常对象 + * - `result`, 成功时得到的响应结果 + * + * Result: + * ``` + * {"access_token": "ACCESS_TOKEN","expires_in": 7200} + * ``` + * @param {Function} callback 回调函数 + */ +API.prototype.getAccessToken = function (callback) { + var that = this; + var url = this.prefix + 'token?grant_type=client_credential&appid=' + this.appid + '&secret=' + this.appsecret; + urllib.request(url, {dataType: 'json'}, wrapper(function (err, data) { + if (err) { + return callback(err); + } + // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 + var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000; + that.token = AccessToken(data.access_token, expireTime); + that.emit('token', that.token); + callback(err, that.token); + })); + return this; +}; + +/** + * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用 + * 无需依赖getAccessToken为前置调用 + * + * @param {Function} method 需要封装的方法 + * @param {Array} args 方法需要的参数 + */ +API.prototype.preRequest = function (method, args) { + var that = this; + var callback = args[args.length - 1]; + if (that.token && that.token.isValid()) { + method.apply(that, args); + } else { + that.getAccessToken(function (err, data) { + // 如遇错误,通过回调函数传出 + if (err) { + callback(err, data); + return; + } + method.apply(that, args); + }); + } +}; + +/** + * 用于支持对象合并。将对象合并到API.prototype上,使得能够支持扩展 + * Examples: + * ``` + * // 媒体管理(上传、下载) + * API.mixin(require('./lib/api_media')); + * ``` + * @param {Object} obj 要合并的对象 + */ +API.mixin = function (obj) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if (API.prototype.hasOwnProperty(key)) { + throw new Error('Don\'t allow override existed prototype method.'); + } + API.prototype[key] = obj[key]; + } + } +}; + +module.exports = API; diff --git a/lib/api_customer.js b/lib/api_customer.js new file mode 100644 index 00000000..db18b25c --- /dev/null +++ b/lib/api_customer.js @@ -0,0 +1,293 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; + +/** + * 客服消息,发送文字消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendText('openid', 'Hello world', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {String} text 发送的消息内容 + * @param {Function} callback 回调函数 + */ +exports.sendText = function (openid, text, callback) { + this.preRequest(this._sendText, arguments); +}; + +/*! + * 客服消息,发送文字消息的未封装版本 + */ +exports._sendText = function (openid, text, callback) { + // { + // "touser":"OPENID", + // "msgtype":"text", + // "text": { + // "content":"Hello World" + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype": "text", + "text": { + "content": text + } + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 客服消息,发送图片消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendImage('openid', 'media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {String} mediaId 媒体文件的ID,参见uploadMedia方法 + * @param {Function} callback 回调函数 + */ +exports.sendImage = function (openid, mediaId, callback) { + this.preRequest(this._sendImage, arguments); +}; + +/*! + * 客服消息,发送图片消息的未封装版本 + */ +exports._sendImage = function (openid, mediaId, callback) { + // { + // "touser":"OPENID", + // "msgtype":"image", + // "image": { + // "media_id":"MEDIA_ID" + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype":"image", + "image": { + "media_id": mediaId + } + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 客服消息,发送语音消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendVoice('openid', 'media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {String} mediaId 媒体文件的ID + * @param {Function} callback 回调函数 + */ +exports.sendVoice = function (openid, mediaId, callback) { + this.preRequest(this._sendVoice, arguments); +}; + +/*! + * 客服消息,发送语音消息的未封装版本 + */ +exports._sendVoice = function (openid, mediaId, callback) { + // { + // "touser":"OPENID", + // "msgtype":"voice", + // "voice": { + // "media_id":"MEDIA_ID" + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype": "voice", + "voice": { + "media_id": mediaId + } + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 客服消息,发送视频消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * api.sendVideo('openid', 'media_id', 'thumb_media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {String} mediaId 媒体文件的ID + * @param {String} thumbMediaId 缩略图文件的ID + * @param {Function} callback 回调函数 + */ +exports.sendVideo = function (openid, mediaId, thumbMediaId, callback) { + this.preRequest(this._sendVideo, arguments); +}; + +/*! + * 客服消息,发送视频消息的未封装版本 + */ +exports._sendVideo = function (openid, mediaId, thumbMediaId, callback) { + // { + // "touser":"OPENID", + // "msgtype":"video", + // "image": { + // "media_id":"MEDIA_ID" + // "thumb_media_id":"THUMB_MEDIA_ID" + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype":"video", + "video": { + "media_id": mediaId, + "thumb_media_id": thumbMediaId + } + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 客服消息,发送音乐消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * var music = { + * title: '音乐标题', // 可选 + * description: '描述内容', // 可选 + * musicurl: 'http://url.cn/xxx', 音乐文件地址 + * hqmusicurl: "HQ_MUSIC_URL", + * thumb_media_id: "THUMB_MEDIA_ID" + * }; + * api.sendMusic('openid', music, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {Object} music 音乐文件 + * @param {Function} callback 回调函数 + */ +exports.sendMusic = function (openid, music, callback) { + this.preRequest(this._sendMusic, arguments); +}; + +/*! + * 客服消息,发送音乐消息的未封装版本 + */ +exports._sendMusic = function (openid, music, callback) { + // { + // "touser":"OPENID", + // "msgtype":"music", + // "music": { + // "title":"MUSIC_TITLE", // 可选 + // "description":"MUSIC_DESCRIPTION", // 可选 + // "musicurl":"MUSIC_URL", + // "hqmusicurl":"HQ_MUSIC_URL", + // "thumb_media_id":"THUMB_MEDIA_ID" + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype":"music", + "music": music + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 客服消息,发送图文消息 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 + * Examples: + * ``` + * var articles = [ + * { + * "title":"Happy Day", + * "description":"Is Really A Happy Day", + * "url":"URL", + * "picurl":"PIC_URL" + * }, + * { + * "title":"Happy Day", + * "description":"Is Really A Happy Day", + * "url":"URL", + * "picurl":"PIC_URL" + * }]; + * api.sendNews('openid', articles, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * @param {String} openid 用户的openid + * @param {Array} articles 图文列表 + * @param {Function} callback 回调函数 + */ +exports.sendNews = function (openid, articles, callback) { + this.preRequest(this._sendNews, arguments); +}; + +/*! + * 客服消息,发送图文消息的未封装版本 + */ +exports._sendNews = function (openid, articles, callback) { + // { + // "touser":"OPENID", + // "msgtype":"news", + // "news":{ + // "articles": [ + // { + // "title":"Happy Day", + // "description":"Is Really A Happy Day", + // "url":"URL", + // "picurl":"PIC_URL" + // }, + // { + // "title":"Happy Day", + // "description":"Is Really A Happy Day", + // "url":"URL", + // "picurl":"PIC_URL" + // }] + // } + // } + var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; + var data = { + "touser": openid, + "msgtype":"news", + "news": { + "articles": articles + } + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; diff --git a/lib/api_group.js b/lib/api_group.js new file mode 100644 index 00000000..6dde3fef --- /dev/null +++ b/lib/api_group.js @@ -0,0 +1,193 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; + +/** + * 获取分组列表 + * 详情请见: + * Examples: + * ``` + * api.getGroups(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "groups": [ + * {"id": 0, "name": "未分组", "count": 72596}, + * {"id": 1, "name": "黑名单", "count": 36} + * ] + * } + * ``` + * @param {Function} callback 回调函数 + */ +exports.getGroups = function (callback) { + this.preRequest(this._getGroups, arguments); +}; + +/*! + * 获取分组列表的未封装版本 + */ +exports._getGroups = function (callback) { + // https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN + var url = this.prefix + 'groups/get?access_token=' + this.token.accessToken; + urllib.request(url, {dataType: 'json'}, wrapper(callback)); +}; + +/** + * 查询用户在哪个分组 + * 详情请见: + * Examples: + * ``` + * api.getWhichGroup(openid, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "groupid": 102 + * } + * ``` + * @param {String} openid Open ID + * @param {Function} callback 回调函数 + */ +exports.getWhichGroup = function (openid, callback) { + this.preRequest(this._getWhichGroup, arguments); +}; + +/*! + * 查询用户在哪个分组未分组版本 + */ +exports._getWhichGroup = function (openid, callback) { + // https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=ACCESS_TOKEN + var url = this.prefix + 'groups/getid?access_token=' + this.token.accessToken; + var data = { + "openid": openid + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 创建分组 + * 详情请见: + * Examples: + * ``` + * api.createGroup('groupname', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"group": {"id": 107, "name": "test"}} + * ``` + * @param {String} name 分组名字 + * @param {Function} callback 回调函数 + */ +exports.createGroup = function (name, callback) { + this.preRequest(this._createGroup, arguments); +}; + +/*! + * 创建分组的未封装版本 + */ +exports._createGroup = function (name, callback) { + // https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN + // POST数据格式:json + // POST数据例子:{"group":{"name":"test"}} + var url = this.prefix + 'groups/create?access_token=' + this.token.accessToken; + var data = { + "group": {"name": name} + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 更新分组名字 + * 详情请见: + * Examples: + * ``` + * api.updateGroup(107, 'new groupname', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode": 0, "errmsg": "ok"} + * ``` + * @param {Number} id 分组ID + * @param {String} name 新的分组名字 + * @param {Function} callback 回调函数 + */ +exports.updateGroup = function (id, name, callback) { + this.preRequest(this._updateGroup, arguments); +}; + +/*! + * 更新分组名字的未封装版本 + */ +exports._updateGroup = function (id, name, callback) { + // http请求方式: POST(请使用https协议) + // https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN + // POST数据格式:json + // POST数据例子:{"group":{"id":108,"name":"test2_modify2"}} + var url = this.prefix + 'groups/update?access_token=' + this.token.accessToken; + var data = { + "group": {"id": id, "name": name} + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 移动用户进分组 + * 详情请见: + * Examples: + * ``` + * api.moveUserToGroup(openid, groupId, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode": 0, "errmsg": "ok"} + * ``` + * @param {String} openid 用户的openid + * @param {Number} groupId 分组ID + * @param {Function} callback 回调函数 + */ +exports.moveUserToGroup = function (openid, groupId, callback) { + this.preRequest(this._moveUserToGroup, arguments); +}; + +/*! + * 移动用户进分组的未封装版本 + */ +exports._moveUserToGroup = function (openid, groupId, callback) { + // http请求方式: POST(请使用https协议) + // https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=ACCESS_TOKEN + // POST数据格式:json + // POST数据例子:{"openid":"oDF3iYx0ro3_7jD4HFRDfrjdCM58","to_groupid":108} + var url = this.prefix + 'groups/members/update?access_token=' + this.token.accessToken; + var data = { + "openid": openid, + "to_groupid": groupId + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; diff --git a/lib/api_media.js b/lib/api_media.js new file mode 100644 index 00000000..d115a186 --- /dev/null +++ b/lib/api_media.js @@ -0,0 +1,114 @@ +var path = require('path'); +var fs = require('fs'); +var urllib = require('urllib'); +var formstream = require('formstream'); +var wrapper = require('./util').wrapper; + +/** + * 上传多媒体文件,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) + * 详情请见: + * Examples: + * ``` + * api.uploadMedia('filepath', type, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789} + * ``` + * Shortcut: + * + * - `exports.uploadImage(filepath, callback);` + * - `exports.uploadVoice(filepath, callback);` + * - `exports.uploadVideo(filepath, callback);` + * - `exports.uploadThumb(filepath, callback);` + * + * @param {String} filepath 文件路径 + * @param {String} type 媒体类型,可用值有image、voice、video、thumb + * @param {Function} callback 回调函数 + */ +exports.uploadMedia = function (filepath, type, callback) { + this.preRequest(this._uploadMedia, arguments); +}; + +/*! + * 上传多媒体文件的未封装版本 + */ +exports._uploadMedia = function (filepath, type, callback) { + var that = this; + fs.stat(filepath, function (err, stat) { + if (err) { + return callback(err); + } + var form = formstream(); + form.file('media', filepath, path.basename(filepath), stat.size); + var url = that.fileServerPrefix + 'media/upload?access_token=' + that.token.accessToken + '&type=' + type; + var opts = { + dataType: 'json', + type: 'POST', + timeout: 60000, // 60秒超时 + headers: form.headers(), + stream: form + }; + urllib.request(url, opts, wrapper(callback)); + }); +}; + +['image', 'voice', 'video', 'thumb'].forEach(function (type) { + var method = 'upload' + type[0].toUpperCase() + type.substring(1); + exports[method] = function (filepath, callback) { + this.uploadMedia(filepath, type, callback); + }; +}); + +/** + * 根据媒体ID获取媒体内容 + * 详情请见: + * Examples: + * ``` + * api.getMedia('media_id', callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的文件Buffer对象 + * - `res`, HTTP响应对象 + * + * @param {String} mediaId 媒体文件的ID + * @param {Function} callback 回调函数 + */ +exports.getMedia = function (mediaId, callback) { + this.preRequest(this._getMedia, arguments); +}; + +/*! + * 上传多媒体文件的未封装版本 + */ +exports._getMedia = function (mediaId, callback) { + var url = this.fileServerPrefix + 'media/get?access_token=' + this.token.accessToken + '&media_id=' + mediaId; + urllib.request(url, {}, wrapper(function (err, data, res) { + // 不用处理err,因为wrapper函数处理过 + var contentType = res.headers['content-type']; + if (contentType === 'application/json') { + var ret; + try { + ret = JSON.parse(data); + if (ret.errcode) { + err = new Error(ret.errmsg); + err.name = 'WeChatAPIError'; + } + } catch (ex) { + callback(ex, data, res); + return; + } + callback(err, ret, res); + } else { + // 输出Buffer对象 + callback(null, data, res); + } + })); +}; diff --git a/lib/api_menu.js b/lib/api_menu.js new file mode 100644 index 00000000..0597779d --- /dev/null +++ b/lib/api_menu.js @@ -0,0 +1,137 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; + +/** + * 创建自定义菜单 + * 详细请看:http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口 + * + * Menu: + * ``` + * { + * "button":[ + * { + * "type":"click", + * "name":"今日歌曲", + * "key":"V1001_TODAY_MUSIC" + * }, + * { + * "name":"菜单", + * "sub_button":[ + * { + * "type":"view", + * "name":"搜索", + * "url":"http://www.soso.com/" + * }, + * { + * "type":"click", + * "name":"赞一下我们", + * "key":"V1001_GOOD" + * }] + * }] + * } + * ] + * } + * ``` + * Examples: + * ``` + * api.createMenu(menu, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode":0,"errmsg":"ok"} + * ``` + * @param {Object} menu 菜单对象 + * @param {Function} callback 回调函数 + */ +exports.createMenu = function (menu, callback) { + this.preRequest(this._createMenu, arguments); +}; + +/*! + * 创建自定义菜单的未封装版本 + */ +exports._createMenu = function (menu, callback) { + var url = this.prefix + 'menu/create?access_token=' + this.token.accessToken; + urllib.request(url, postJSON(menu), wrapper(callback)); +}; + +/** + * 获取菜单 + * 详细请看: + * + * Examples: + * ``` + * api.getMenu(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * // 结果示例 + * { + * "menu": { + * "button":[ + * {"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]}, + * {"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]}, + * {"name":"菜单","sub_button":[ + * {"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]}, + * {"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]}, + * {"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}] + * } + * ] + * } + * } + * ``` + * @param {Function} callback 回调函数 + */ +exports.getMenu = function (callback) { + this.preRequest(this._getMenu, arguments); +}; + +/*! + * 获取自定义菜单的未封装版本 + */ +exports._getMenu = function (callback) { + var url = this.prefix + 'menu/get?access_token=' + this.token.accessToken; + urllib.request(url, {dataType: 'json'}, wrapper(callback)); +}; + +/** + * 删除自定义菜单 + * 详细请看: + * Examples: + * ``` + * api.removeMenu(callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode":0,"errmsg":"ok"} + * ``` + * @param {Function} callback 回调函数 + */ +exports.removeMenu = function (callback) { + this.preRequest(this._removeMenu, arguments); +}; + +/*! + * 删除自定义菜单的未封装版本 + */ +exports._removeMenu = function (callback) { + var url = this.prefix + 'menu/delete?access_token=' + this.token.accessToken; + urllib.request(url, {dataType: 'json'}, wrapper(callback)); +}; diff --git a/lib/api_pay.js b/lib/api_pay.js new file mode 100644 index 00000000..7f9e8bdf --- /dev/null +++ b/lib/api_pay.js @@ -0,0 +1,120 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; + +/** + * 微信公众号支付: 发货通知 + * 详情请见: 接口文档订单发货通知 + * + * Package: + * ``` + * { + * "appid" : "wwwwb4f85f3a797777", + * "openid" : "oX99MDgNcgwnz3zFN3DNmo8uwa-w", + * "transid" : "111112222233333", + * "out_trade_no" : "555666uuu", + * "deliver_timestamp" : "1369745073", + * "deliver_status" : "1", + * "deliver_msg" : "ok", + * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", + * "sign_method" : "sha1" + * } + * ``` + * Examples: + * ``` + * api.deliverNotify(package, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * {"errcode":0,"errmsg":"ok"} + * ``` + * + * @param {Object} package package对象 + * @param {Function} callback 回调函数 + */ +exports.deliverNotify = function (package, callback) { + this.preRequest(this._deliverNotify, arguments); +}; + +/*! + * 发货通知的未封装版本 + */ +exports._deliverNotify = function (package, callback) { + var url = this.payPrefix + 'delivernotify?access_token=' + this.token.accessToken; + urllib.request(url, postJSON(package), wrapper(callback)); +}; + +/** + * 微信公众号支付: 订单查询 + * 详情请见: 接口文档订单查询部分 + * + * Package: + * ``` + * { + * "appid" : "wwwwb4f85f3a797777", + * "package" : "out_trade_no=11122&partner=1900090055&sign=4e8d0df3da0c3d0df38f", + * "timestamp" : "1369745073", + * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", + * "sign_method" : "sha1" + * } + * ``` + * Examples: + * ``` + * api.orderQuery(query, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "errcode":0, + * "errmsg":"ok", + * "order_info": + * { + * "ret_code":0, + * "ret_msg":"", + * "input_charset":"GBK", + * "trade_state":"0", + * "trade_mode":"1", + * "partner":"1900000109", + * "bank_type":"CMB_FP", + * "bank_billno":"207029722724", + * "total_fee":"1", + * "fee_type":"1", + * "transaction_id":"1900000109201307020305773741", + * "out_trade_no":"2986872580246457300", + * "is_split":"false", + * "is_refund":"false", + * "attach":"", + * "time_end":"20130702175943", + * "transport_fee":"0", + * "product_fee":"1", + * "discount":"0", + * "rmb_total_fee":"" + * } + * } + * ``` + * + * @param {Object} query query对象 + * @param {Function} callback 回调函数 + */ +exports.orderQuery = function (query, callback) { + this.preRequest(this._orderQuery, arguments); +}; + +/*! + * 发货通知的未封装版本 + */ +exports._orderQuery = function (query, callback) { + var url = this.payPrefix + 'orderquery?access_token=' + this.token.accessToken; + urllib.request(url, postJSON(query), wrapper(callback)); +}; diff --git a/lib/api_qrcode.js b/lib/api_qrcode.js new file mode 100644 index 00000000..fe3c857d --- /dev/null +++ b/lib/api_qrcode.js @@ -0,0 +1,95 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; + +/** + * 创建临时二维码 + * 详细请看: + * Examples: + * ``` + * api.createTmpQRCode(10000, 1800, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==", + * "expire_seconds":1800 + * } + * ``` + * @param {Number} sceneId 场景ID + * @param {Number} expire 过期时间,单位秒。最大不超过1800 + * @param {Function} callback 回调函数 + */ +exports.createTmpQRCode = function (sceneId, expire, callback) { + this.preRequest(this._createTmpQRCode, arguments); +}; + +/*! + * 创建临时二维码的未封装版本 + */ +exports._createTmpQRCode = function (sceneId, expire, callback) { + var url = this.prefix + 'qrcode/create?access_token=' + this.token.accessToken; + var data = { + "expire_seconds": expire, + "action_name": "QR_SCENE", + "action_info": {"scene": {"scene_id": sceneId}} + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 创建永久二维码 + * 详细请看: + * Examples: + * ``` + * api.createLimitQRCode(100, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==" + * } + * ``` + * @param {Number} sceneId 场景ID。ID不能大于100000 + * @param {Function} callback 回调函数 + */ +exports.createLimitQRCode = function (sceneId, callback) { + this.preRequest(this._createLimitQRCode, arguments); +}; + +/*! + * 创建永久二维码的未封装版本 + */ +exports._createLimitQRCode = function (sceneId, callback) { + var url = this.prefix + 'qrcode/create?access_token=' + this.token.accessToken; + var data = { + "action_name": "QR_LIMIT_SCENE", + "action_info": {"scene": {"scene_id": sceneId}} + }; + urllib.request(url, postJSON(data), wrapper(callback)); +}; + +/** + * 生成显示二维码的链接。微信扫描后,可立即进入场景 + * Examples: + * ``` + * api.showQRCodeURL(titck); + * // => https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET + * ``` + * @param {String} ticket 二维码Ticket + * @return {String} 显示二维码的URL地址,通过img标签可以显示出来 + */ +exports.showQRCodeURL = function (ticket) { + return this.mpPrefix + 'showqrcode?ticket=' + ticket; +}; diff --git a/lib/api_user.js b/lib/api_user.js new file mode 100644 index 00000000..8ca3386f --- /dev/null +++ b/lib/api_user.js @@ -0,0 +1,92 @@ +var urllib = require('urllib'); +var util = require('./util'); +var wrapper = util.wrapper; + +/** + * 获取用户基本信息 + * 详情请见: + * Examples: + * ``` + * api.getUser(openid, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "subscribe": 1, + * "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", + * "nickname": "Band", + * "sex": 1, + * "language": "zh_CN", + * "city": "广州", + * "province": "广东", + * "country": "中国", + * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", + * "subscribe_time": 1382694957 + * } + * ``` + * @param {String} openid 用户的openid + * @param {Function} callback 回调函数 + */ +exports.getUser = function (openid, callback) { + this.preRequest(this._getUser, arguments); +}; + +/*! + * 获取用户基本信息的未封装版本 + */ +exports._getUser = function (openid, callback) { + // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID + var url = this.prefix + 'user/info?openid=' + openid + '&access_token=' + this.token.accessToken; + urllib.request(url, {dataType: 'json'}, wrapper(callback)); +}; + +/** + * 获取关注者列表 + * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表 + * Examples: + * ``` + * api.getFollowers(callback); + * // or + * api.getFollowers(nextOpenid, callback); + * ``` + * Callback: + * + * - `err`, 调用失败时得到的异常 + * - `result`, 调用正常时得到的对象 + * + * Result: + * ``` + * { + * "total":2, + * "count":2, + * "data":{ + * "openid":["","OPENID1","OPENID2"] + * }, + * "next_openid":"NEXT_OPENID" + * } + * ``` + * @param {String} nextOpenid 调用一次之后,传递回来的nextOpenid。第一次获取时可不填 + * @param {Function} callback 回调函数 + */ +exports.getFollowers = function (nextOpenid, callback) { + this.preRequest(this._getFollowers, arguments); +}; + +/*! + * 获取关注者列表的未封装版本 + */ +exports._getFollowers = function (nextOpenid, callback) { + // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID + if (typeof nextOpenid === 'function') { + callback = nextOpenid; + nextOpenid = ''; + } + var url = this.prefix + 'user/get?next_openid=' + nextOpenid + '&access_token=' + this.token.accessToken; + urllib.request(url, {dataType: 'json'}, wrapper(callback)); +}; + diff --git a/lib/common.js b/lib/common.js deleted file mode 100644 index 8e8d89b9..00000000 --- a/lib/common.js +++ /dev/null @@ -1,1141 +0,0 @@ -var path = require('path'); -var fs = require('fs'); -var inherits = require('util').inherits; -var EventEmitter = require('events').EventEmitter; -var urllib = require('urllib'); -var formstream = require('formstream'); -var util = require('./util'); -var wrapper = util.wrapper; -var postJSON = util.postJSON; - -var AccessToken = function (accessToken, expireTime) { - if (!(this instanceof AccessToken)) { - return new AccessToken(accessToken, expireTime); - } - this.accessToken = accessToken; - this.expireTime = expireTime; -}; - -/** - * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比 - * - * Examples: - * ``` - * api.isAccessTokenValid(); - * ``` - */ -AccessToken.prototype.isValid = function () { - return !!this.accessToken && (new Date().getTime()) < this.expireTime; -}; - -/** - * 根据appid和appsecret创建API的构造函数 - * - * Examples: - * ``` - * var API = require('wechat').API; - * var api = new API('appid', 'secret'); - * api.on('token', function (token) { - * // 请将token存储到全局,跨进程级别的全局 - * // 比如写到数据库、redis等 - * // 这样才能在cluster模式下使用 - * }); - * // 或者 - * getTokenFromGlobal(function (token) { - * // var api = new API('appid', 'secret', token); - * }); - * ``` - * @param {String} appid 在公众平台上申请得到的appid - * @param {String} appsecret 在公众平台上申请得到的app secret - * @param {Object} token 可选的。全局token对象,该对象在调用一次`getAccessToken`后得到 - */ -var API = function (appid, appsecret, token) { - this.appid = appid; - this.appsecret = appsecret; - if (token) { - this.token = AccessToken(token.accessToken, token.expireTime); - } - this.prefix = 'https://api.weixin.qq.com/cgi-bin/'; - this.mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/'; - this.fileServerPrefix = 'http://file.api.weixin.qq.com/cgi-bin/'; - this.payPrefix = 'https://api.weixin.qq.com/pay/'; - EventEmitter.call(this); -}; -inherits(API, EventEmitter); - -/** - * 根据创建API时传入的appid和appsecret获取access token - * 进行后续所有API调用时,需要先获取access token - * 详细请看: - * - * Examples: - * ``` - * api.getAccessToken(callback); - * ``` - * Callback: - * - * - `err`, 获取access token出现异常时的异常对象 - * - `result`, 成功时得到的响应结果 - * - * Result: - * ``` - * {"access_token": "ACCESS_TOKEN","expires_in": 7200} - * ``` - * @param {Function} callback 回调函数 - */ -API.prototype.getAccessToken = function (callback) { - var that = this; - var url = this.prefix + 'token?grant_type=client_credential&appid=' + this.appid + '&secret=' + this.appsecret; - urllib.request(url, {dataType: 'json'}, wrapper(function (err, data) { - if (err) { - return callback(err); - } - // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 - var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000; - that.token = AccessToken(data.access_token, expireTime); - that.emit('token', that.token); - callback(err, that.token); - })); - return this; -}; - -/** - * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用 - * 无需依赖getAccessToken为前置调用 - * - * @param {Function} method 需要封装的方法 - * @param {Array} args 方法需要的参数 - */ -API.prototype.preRequest = function (method, args) { - var that = this; - var callback = args[args.length - 1]; - if (that.token && that.token.isValid()) { - method.apply(that, args); - } else { - that.getAccessToken(function (err, data) { - // 如遇错误,通过回调函数传出 - if (err) { - callback(err, data); - return; - } - method.apply(that, args); - }); - } -}; - -/** - * 创建自定义菜单 - * 详细请看:http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口 - * - * Menu: - * ``` - * { - * "button":[ - * { - * "type":"click", - * "name":"今日歌曲", - * "key":"V1001_TODAY_MUSIC" - * }, - * { - * "name":"菜单", - * "sub_button":[ - * { - * "type":"view", - * "name":"搜索", - * "url":"http://www.soso.com/" - * }, - * { - * "type":"click", - * "name":"赞一下我们", - * "key":"V1001_GOOD" - * }] - * }] - * } - * ] - * } - * ``` - * Examples: - * ``` - * api.createMenu(menu, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"errcode":0,"errmsg":"ok"} - * ``` - * @param {Object} menu 菜单对象 - * @param {Function} callback 回调函数 - */ -API.prototype.createMenu = function (menu, callback) { - this.preRequest(this._createMenu, arguments); -}; - -/*! - * 创建自定义菜单的未封装版本 - */ -API.prototype._createMenu = function (menu, callback) { - var url = this.prefix + 'menu/create?access_token=' + this.token.accessToken; - urllib.request(url, postJSON(menu), wrapper(callback)); -}; - -/** - * 获取菜单 - * 详细请看: - * - * Examples: - * ``` - * api.getMenu(callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * // 结果示例 - * { - * "menu": { - * "button":[ - * {"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]}, - * {"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]}, - * {"name":"菜单","sub_button":[ - * {"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]}, - * {"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]}, - * {"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}] - * } - * ] - * } - * } - * ``` - * @param {Function} callback 回调函数 - */ -API.prototype.getMenu = function (callback) { - this.preRequest(this._getMenu, arguments); -}; - -/*! - * 获取自定义菜单的未封装版本 - */ -API.prototype._getMenu = function (callback) { - var url = this.prefix + 'menu/get?access_token=' + this.token.accessToken; - urllib.request(url, {dataType: 'json'}, wrapper(callback)); -}; - -/** - * 删除自定义菜单 - * 详细请看: - * Examples: - * ``` - * api.removeMenu(callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"errcode":0,"errmsg":"ok"} - * ``` - * @param {Function} callback 回调函数 - */ -API.prototype.removeMenu = function (callback) { - this.preRequest(this._removeMenu, arguments); -}; - -/*! - * 删除自定义菜单的未封装版本 - */ -API.prototype._removeMenu = function (callback) { - var url = this.prefix + 'menu/delete?access_token=' + this.token.accessToken; - urllib.request(url, {dataType: 'json'}, wrapper(callback)); -}; - -/** - * 创建临时二维码 - * 详细请看: - * Examples: - * ``` - * api.createTmpQRCode(10000, 1800, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==", - * "expire_seconds":1800 - * } - * ``` - * @param {Number} sceneId 场景ID - * @param {Number} expire 过期时间,单位秒。最大不超过1800 - * @param {Function} callback 回调函数 - */ -API.prototype.createTmpQRCode = function (sceneId, expire, callback) { - this.preRequest(this._createTmpQRCode, arguments); -}; - -/*! - * 创建临时二维码的未封装版本 - */ -API.prototype._createTmpQRCode = function (sceneId, expire, callback) { - var url = this.prefix + 'qrcode/create?access_token=' + this.token.accessToken; - var data = { - "expire_seconds": expire, - "action_name": "QR_SCENE", - "action_info": {"scene": {"scene_id": sceneId}} - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 创建永久二维码 - * 详细请看: - * Examples: - * ``` - * api.createLimitQRCode(100, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==" - * } - * ``` - * @param {Number} sceneId 场景ID。ID不能大于100000 - * @param {Function} callback 回调函数 - */ -API.prototype.createLimitQRCode = function (sceneId, callback) { - this.preRequest(this._createLimitQRCode, arguments); -}; - -/*! - * 创建永久二维码的未封装版本 - */ -API.prototype._createLimitQRCode = function (sceneId, callback) { - var url = this.prefix + 'qrcode/create?access_token=' + this.token.accessToken; - var data = { - "action_name": "QR_LIMIT_SCENE", - "action_info": {"scene": {"scene_id": sceneId}} - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 生成显示二维码的链接。微信扫描后,可立即进入场景 - * Examples: - * ``` - * api.showQRCodeURL(titck); - * // => https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET - * ``` - * @param {String} ticket 二维码Ticket - * @return {String} 显示二维码的URL地址,通过img标签可以显示出来 - */ -API.prototype.showQRCodeURL = function (ticket) { - return this.mpPrefix + 'showqrcode?ticket=' + ticket; -}; - -/** - * 获取分组列表 - * 详情请见: - * Examples: - * ``` - * api.getGroups(callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "groups": [ - * {"id": 0, "name": "未分组", "count": 72596}, - * {"id": 1, "name": "黑名单", "count": 36} - * ] - * } - * ``` - * @param {Function} callback 回调函数 - */ -API.prototype.getGroups = function (callback) { - this.preRequest(this._getGroups, arguments); -}; - -/*! - * 获取分组列表的未封装版本 - */ -API.prototype._getGroups = function (callback) { - // https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN - var url = this.prefix + 'groups/get?access_token=' + this.token.accessToken; - urllib.request(url, {dataType: 'json'}, wrapper(callback)); -}; - -/** - * 查询用户在哪个分组 - * 详情请见: - * Examples: - * ``` - * api.getWhichGroup(openid, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "groupid": 102 - * } - * ``` - * @param {String} openid Open ID - * @param {Function} callback 回调函数 - */ -API.prototype.getWhichGroup = function (openid, callback) { - this.preRequest(this._getWhichGroup, arguments); -}; - -/*! - * 查询用户在哪个分组未分组版本 - */ -API.prototype._getWhichGroup = function (openid, callback) { - // https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=ACCESS_TOKEN - var url = this.prefix + 'groups/getid?access_token=' + this.token.accessToken; - var data = { - "openid": openid - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 创建分组 - * 详情请见: - * Examples: - * ``` - * api.createGroup('groupname', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"group": {"id": 107, "name": "test"}} - * ``` - * @param {String} name 分组名字 - * @param {Function} callback 回调函数 - */ -API.prototype.createGroup = function (name, callback) { - this.preRequest(this._createGroup, arguments); -}; - -/*! - * 创建分组的未封装版本 - */ -API.prototype._createGroup = function (name, callback) { - // https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN - // POST数据格式:json - // POST数据例子:{"group":{"name":"test"}} - var url = this.prefix + 'groups/create?access_token=' + this.token.accessToken; - var data = { - "group": {"name": name} - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 更新分组名字 - * 详情请见: - * Examples: - * ``` - * api.updateGroup(107, 'new groupname', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"errcode": 0, "errmsg": "ok"} - * ``` - * @param {Number} id 分组ID - * @param {String} name 新的分组名字 - * @param {Function} callback 回调函数 - */ -API.prototype.updateGroup = function (id, name, callback) { - this.preRequest(this._updateGroup, arguments); -}; - -/*! - * 更新分组名字的未封装版本 - */ -API.prototype._updateGroup = function (id, name, callback) { - // http请求方式: POST(请使用https协议) - // https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN - // POST数据格式:json - // POST数据例子:{"group":{"id":108,"name":"test2_modify2"}} - var url = this.prefix + 'groups/update?access_token=' + this.token.accessToken; - var data = { - "group": {"id": id, "name": name} - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 移动用户进分组 - * 详情请见: - * Examples: - * ``` - * api.moveUserToGroup(openid, groupId, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"errcode": 0, "errmsg": "ok"} - * ``` - * @param {String} openid 用户的openid - * @param {Number} groupId 分组ID - * @param {Function} callback 回调函数 - */ -API.prototype.moveUserToGroup = function (openid, groupId, callback) { - this.preRequest(this._moveUserToGroup, arguments); -}; - -/*! - * 移动用户进分组的未封装版本 - */ -API.prototype._moveUserToGroup = function (openid, groupId, callback) { - // http请求方式: POST(请使用https协议) - // https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=ACCESS_TOKEN - // POST数据格式:json - // POST数据例子:{"openid":"oDF3iYx0ro3_7jD4HFRDfrjdCM58","to_groupid":108} - var url = this.prefix + 'groups/members/update?access_token=' + this.token.accessToken; - var data = { - "openid": openid, - "to_groupid": groupId - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 获取用户基本信息 - * 详情请见: - * Examples: - * ``` - * api.getUser(openid, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "subscribe": 1, - * "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", - * "nickname": "Band", - * "sex": 1, - * "language": "zh_CN", - * "city": "广州", - * "province": "广东", - * "country": "中国", - * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", - * "subscribe_time": 1382694957 - * } - * ``` - * @param {String} openid 用户的openid - * @param {Function} callback 回调函数 - */ -API.prototype.getUser = function (openid, callback) { - this.preRequest(this._getUser, arguments); -}; - -/*! - * 获取用户基本信息的未封装版本 - */ -API.prototype._getUser = function (openid, callback) { - // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID - var url = this.prefix + 'user/info?openid=' + openid + '&access_token=' + this.token.accessToken; - urllib.request(url, {dataType: 'json'}, wrapper(callback)); -}; - -/** - * 获取关注者列表 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表 - * Examples: - * ``` - * api.getFollowers(callback); - * // or - * api.getFollowers(nextOpenid, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "total":2, - * "count":2, - * "data":{ - * "openid":["","OPENID1","OPENID2"] - * }, - * "next_openid":"NEXT_OPENID" - * } - * ``` - * @param {String} nextOpenid 调用一次之后,传递回来的nextOpenid。第一次获取时可不填 - * @param {Function} callback 回调函数 - */ -API.prototype.getFollowers = function (nextOpenid, callback) { - this.preRequest(this._getFollowers, arguments); -}; - -/*! - * 获取关注者列表的未封装版本 - */ -API.prototype._getFollowers = function (nextOpenid, callback) { - // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID - if (typeof nextOpenid === 'function') { - callback = nextOpenid; - nextOpenid = ''; - } - var url = this.prefix + 'user/get?next_openid=' + nextOpenid + '&access_token=' + this.token.accessToken; - urllib.request(url, {dataType: 'json'}, wrapper(callback)); -}; - -/** - * 客服消息,发送文字消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * api.sendText('openid', 'Hello world', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {String} text 发送的消息内容 - * @param {Function} callback 回调函数 - */ -API.prototype.sendText = function (openid, text, callback) { - this.preRequest(this._sendText, arguments); -}; - -/*! - * 客服消息,发送文字消息的未封装版本 - */ -API.prototype._sendText = function (openid, text, callback) { - // { - // "touser":"OPENID", - // "msgtype":"text", - // "text": { - // "content":"Hello World" - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype": "text", - "text": { - "content": text - } - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 客服消息,发送图片消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * api.sendImage('openid', 'media_id', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {String} mediaId 媒体文件的ID,参见uploadMedia方法 - * @param {Function} callback 回调函数 - */ -API.prototype.sendImage = function (openid, mediaId, callback) { - this.preRequest(this._sendImage, arguments); -}; - -/*! - * 客服消息,发送图片消息的未封装版本 - */ -API.prototype._sendImage = function (openid, mediaId, callback) { - // { - // "touser":"OPENID", - // "msgtype":"image", - // "image": { - // "media_id":"MEDIA_ID" - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype":"image", - "image": { - "media_id": mediaId - } - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 客服消息,发送语音消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * api.sendVoice('openid', 'media_id', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {String} mediaId 媒体文件的ID - * @param {Function} callback 回调函数 - */ -API.prototype.sendVoice = function (openid, mediaId, callback) { - this.preRequest(this._sendVoice, arguments); -}; - -/*! - * 客服消息,发送语音消息的未封装版本 - */ -API.prototype._sendVoice = function (openid, mediaId, callback) { - // { - // "touser":"OPENID", - // "msgtype":"voice", - // "voice": { - // "media_id":"MEDIA_ID" - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype": "voice", - "voice": { - "media_id": mediaId - } - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 客服消息,发送视频消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * api.sendVideo('openid', 'media_id', 'thumb_media_id', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {String} mediaId 媒体文件的ID - * @param {String} thumbMediaId 缩略图文件的ID - * @param {Function} callback 回调函数 - */ -API.prototype.sendVideo = function (openid, mediaId, thumbMediaId, callback) { - this.preRequest(this._sendVideo, arguments); -}; - -/*! - * 客服消息,发送视频消息的未封装版本 - */ -API.prototype._sendVideo = function (openid, mediaId, thumbMediaId, callback) { - // { - // "touser":"OPENID", - // "msgtype":"video", - // "image": { - // "media_id":"MEDIA_ID" - // "thumb_media_id":"THUMB_MEDIA_ID" - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype":"video", - "video": { - "media_id": mediaId, - "thumb_media_id": thumbMediaId - } - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 客服消息,发送音乐消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * var music = { - * title: '音乐标题', // 可选 - * description: '描述内容', // 可选 - * musicurl: 'http://url.cn/xxx', 音乐文件地址 - * hqmusicurl: "HQ_MUSIC_URL", - * thumb_media_id: "THUMB_MEDIA_ID" - * }; - * api.sendMusic('openid', music, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {Object} music 音乐文件 - * @param {Function} callback 回调函数 - */ -API.prototype.sendMusic = function (openid, music, callback) { - this.preRequest(this._sendMusic, arguments); -}; - -/*! - * 客服消息,发送音乐消息的未封装版本 - */ -API.prototype._sendMusic = function (openid, music, callback) { - // { - // "touser":"OPENID", - // "msgtype":"music", - // "music": { - // "title":"MUSIC_TITLE", // 可选 - // "description":"MUSIC_DESCRIPTION", // 可选 - // "musicurl":"MUSIC_URL", - // "hqmusicurl":"HQ_MUSIC_URL", - // "thumb_media_id":"THUMB_MEDIA_ID" - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype":"music", - "music": music - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 客服消息,发送图文消息 - * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * Examples: - * ``` - * var articles = [ - * { - * "title":"Happy Day", - * "description":"Is Really A Happy Day", - * "url":"URL", - * "picurl":"PIC_URL" - * }, - * { - * "title":"Happy Day", - * "description":"Is Really A Happy Day", - * "url":"URL", - * "picurl":"PIC_URL" - * }]; - * api.sendNews('openid', articles, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * @param {String} openid 用户的openid - * @param {Array} articles 图文列表 - * @param {Function} callback 回调函数 - */ -API.prototype.sendNews = function (openid, articles, callback) { - this.preRequest(this._sendNews, arguments); -}; - -/*! - * 客服消息,发送图文消息的未封装版本 - */ -API.prototype._sendNews = function (openid, articles, callback) { - // { - // "touser":"OPENID", - // "msgtype":"news", - // "news":{ - // "articles": [ - // { - // "title":"Happy Day", - // "description":"Is Really A Happy Day", - // "url":"URL", - // "picurl":"PIC_URL" - // }, - // { - // "title":"Happy Day", - // "description":"Is Really A Happy Day", - // "url":"URL", - // "picurl":"PIC_URL" - // }] - // } - // } - var url = this.prefix + 'message/custom/send?access_token=' + this.token.accessToken; - var data = { - "touser": openid, - "msgtype":"news", - "news": { - "articles": articles - } - }; - urllib.request(url, postJSON(data), wrapper(callback)); -}; - -/** - * 上传多媒体文件,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) - * 详情请见: - * Examples: - * ``` - * api.uploadMedia('filepath', type, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789} - * ``` - * Shortcut: - * - * - `API.prototype.uploadImage(filepath, callback);` - * - `API.prototype.uploadVoice(filepath, callback);` - * - `API.prototype.uploadVideo(filepath, callback);` - * - `API.prototype.uploadThumb(filepath, callback);` - * - * @param {String} filepath 文件路径 - * @param {String} type 媒体类型,可用值有image、voice、video、thumb - * @param {Function} callback 回调函数 - */ -API.prototype.uploadMedia = function (filepath, type, callback) { - this.preRequest(this._uploadMedia, arguments); -}; - -/*! - * 上传多媒体文件的未封装版本 - */ -API.prototype._uploadMedia = function (filepath, type, callback) { - var that = this; - fs.stat(filepath, function (err, stat) { - if (err) { - return callback(err); - } - var form = formstream(); - form.file('media', filepath, path.basename(filepath), stat.size); - var url = that.fileServerPrefix + 'media/upload?access_token=' + that.token.accessToken + '&type=' + type; - var opts = { - dataType: 'json', - type: 'POST', - timeout: 60000, // 60秒超时 - headers: form.headers(), - stream: form - }; - urllib.request(url, opts, wrapper(callback)); - }); -}; - -['image', 'voice', 'video', 'thumb'].forEach(function (type) { - var method = 'upload' + type[0].toUpperCase() + type.substring(1); - API.prototype[method] = function (filepath, callback) { - this.uploadMedia(filepath, type, callback); - }; -}); - -/** - * 根据媒体ID获取媒体内容 - * 详情请见: - * Examples: - * ``` - * api.getMedia('media_id', callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的文件Buffer对象 - * - `res`, HTTP响应对象 - * - * @param {String} mediaId 媒体文件的ID - * @param {Function} callback 回调函数 - */ -API.prototype.getMedia = function (mediaId, callback) { - this.preRequest(this._getMedia, arguments); -}; - -/*! - * 上传多媒体文件的未封装版本 - */ -API.prototype._getMedia = function (mediaId, callback) { - var url = this.fileServerPrefix + 'media/get?access_token=' + this.token.accessToken + '&media_id=' + mediaId; - urllib.request(url, {}, wrapper(function (err, data, res) { - // 不用处理err,因为wrapper函数处理过 - var contentType = res.headers['content-type']; - if (contentType === 'application/json') { - var ret; - try { - ret = JSON.parse(data); - if (ret.errcode) { - err = new Error(ret.errmsg); - err.name = 'WeChatAPIError'; - } - } catch (ex) { - callback(ex, data, res); - return; - } - callback(err, ret, res); - } else { - // 输出Buffer对象 - callback(null, data, res); - } - })); -}; - -/** - * 微信公众号支付: 发货通知 - * 详情请见: 接口文档订单发货通知 - * - * Package: - * ``` - * { - * "appid" : "wwwwb4f85f3a797777", - * "openid" : "oX99MDgNcgwnz3zFN3DNmo8uwa-w", - * "transid" : "111112222233333", - * "out_trade_no" : "555666uuu", - * "deliver_timestamp" : "1369745073", - * "deliver_status" : "1", - * "deliver_msg" : "ok", - * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", - * "sign_method" : "sha1" - * } - * ``` - * Examples: - * ``` - * api.deliverNotify(package, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * {"errcode":0,"errmsg":"ok"} - * ``` - * - * @param {Object} package package对象 - * @param {Function} callback 回调函数 - */ -API.prototype.deliverNotify = function (package, callback) { - this.preRequest(this._deliverNotify, arguments); -}; - -/*! - * 发货通知的未封装版本 - */ -API.prototype._deliverNotify = function (package, callback) { - var url = this.payPrefix + 'delivernotify?access_token=' + this.token.accessToken; - urllib.request(url, postJSON(package), wrapper(callback)); -}; - -/** - * 微信公众号支付: 订单查询 - * 详情请见: 接口文档订单查询部分 - * - * Package: - * ``` - * { - * "appid" : "wwwwb4f85f3a797777", - * "package" : "out_trade_no=11122&partner=1900090055&sign=4e8d0df3da0c3d0df38f", - * "timestamp" : "1369745073", - * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", - * "sign_method" : "sha1" - * } - * ``` - * Examples: - * ``` - * api.orderQuery(query, callback); - * ``` - * Callback: - * - * - `err`, 调用失败时得到的异常 - * - `result`, 调用正常时得到的对象 - * - * Result: - * ``` - * { - * "errcode":0, - * "errmsg":"ok", - * "order_info": - * { - * "ret_code":0, - * "ret_msg":"", - * "input_charset":"GBK", - * "trade_state":"0", - * "trade_mode":"1", - * "partner":"1900000109", - * "bank_type":"CMB_FP", - * "bank_billno":"207029722724", - * "total_fee":"1", - * "fee_type":"1", - * "transaction_id":"1900000109201307020305773741", - * "out_trade_no":"2986872580246457300", - * "is_split":"false", - * "is_refund":"false", - * "attach":"", - * "time_end":"20130702175943", - * "transport_fee":"0", - * "product_fee":"1", - * "discount":"0", - * "rmb_total_fee":"" - * } - * } - * ``` - * - * @param {Object} query query对象 - * @param {Function} callback 回调函数 - */ -API.prototype.orderQuery = function (query, callback) { - this.preRequest(this._orderQuery, arguments); -}; - -/*! - * 发货通知的未封装版本 - */ -API.prototype._orderQuery = function (query, callback) { - var url = this.payPrefix + 'orderquery?access_token=' + this.token.accessToken; - urllib.request(url, postJSON(query), wrapper(callback)); -}; - -module.exports = API; diff --git a/lib/oauth.js b/lib/oauth.js index b29273b9..cc616141 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -171,7 +171,7 @@ OAuth.prototype._getUser = function (openid, callback) { * "province": "PROVINCE" * "city": "CITY", * "country": "COUNTRY", - * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", + * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", * "privilege": [ * "PRIVILEGE1" * "PRIVILEGE2" diff --git a/lib/wechat.js b/lib/wechat.js index 36115879..971c9a39 100644 --- a/lib/wechat.js +++ b/lib/wechat.js @@ -344,7 +344,8 @@ var middleware = function (token, handle) { }; }); +middleware.toXML = compiled; +middleware.reply = reply; +middleware.checkSignature = checkSignature; + module.exports = middleware; -module.exports.toXML = compiled; -module.exports.reply = reply; -module.exports.checkSignature = checkSignature; diff --git a/package.json b/package.json index 3028824f..97a51c09 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "devDependencies": { "supertest": "*", "mocha": "*", - "should": "*", + "should": "~3.0.0", "expect.js": "*", "connect": "*", "blanket": "*", diff --git a/test/common.test.js b/test/common.test.js index 5396d45c..8004772c 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -747,25 +747,4 @@ describe('common.js', function () { }); }); }); - - describe('pay', function () { - var api = new API('appid', 'secret'); - it('deliverNotify should ok', function (done) { - api.deliverNotify('{}', function (err, menu) { - should.exist(err); - err.name.should.be.equal('WeChatAPIError'); - err.message.should.be.equal('invalid appid'); - done(); - }); - }); - - it('orderQuery should ok', function (done) { - api.orderQuery('{}', function (err, menu) { - should.exist(err); - err.name.should.be.equal('WeChatAPIError'); - err.message.should.be.equal('invalid appid'); - done(); - }); - }); - }); }); diff --git a/test/pay.test.js b/test/pay.test.js new file mode 100644 index 00000000..7a9c8930 --- /dev/null +++ b/test/pay.test.js @@ -0,0 +1,23 @@ +var should = require('should'); +var API = require('../').API; + +describe('pay', function () { + var api = new API('appid', 'secret'); + it('deliverNotify should ok', function (done) { + api.deliverNotify('{}', function (err, menu) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('invalid appid'); + done(); + }); + }); + + it('orderQuery should ok', function (done) { + api.orderQuery('{}', function (err, menu) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('invalid appid'); + done(); + }); + }); +});