├── .gitignore ├── .travis.yml ├── test ├── fixture │ ├── pic.jpg │ ├── image.jpg │ ├── movie.mp4 │ ├── test.mp3 │ └── menu.json ├── config.js ├── api_message.test.js ├── api_menu.test.js ├── api_tag.test.js ├── support.js ├── api_department.test.js ├── api_user.test.js ├── api_common.test.js └── api_media.test.js ├── index.js ├── .jshintrc ├── Makefile ├── lib ├── api_ip.js ├── api_shake.js ├── util.js ├── api_media.js ├── api_menu.js ├── api_message.js ├── api_agent.js ├── api_department.js ├── api_js.js ├── api_batch.js ├── api_common.js ├── api_tag.js ├── api_material.js └── api_user.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | example 5 | .DS_Store 6 | coverage 7 | *.sublime* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | script: make test-coveralls 6 | -------------------------------------------------------------------------------- /test/fixture/pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/wechat-enterprise-api/HEAD/test/fixture/pic.jpg -------------------------------------------------------------------------------- /test/fixture/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/wechat-enterprise-api/HEAD/test/fixture/image.jpg -------------------------------------------------------------------------------- /test/fixture/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/wechat-enterprise-api/HEAD/test/fixture/movie.mp4 -------------------------------------------------------------------------------- /test/fixture/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/wechat-enterprise-api/HEAD/test/fixture/test.mp3 -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | corpid: 'wx4e2c2b771c467c9f', 3 | corpsecret: 'k7TGD8xJLDU6-sPH3NwY0eTs2oBPyAINMdbSbGN80fuEt01UK0Z8dWzhm7crgkz7', 4 | token: 'mRoQySqj2XBEORdnuOh9wei17', 5 | encodingAESKey: '6mRfWp9o1dfXHnmRBCFmTlpM3IIY377wy2iDJJjx4lM' 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixture/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "button":[ 3 | { 4 | "type":"click", 5 | "name":"今日歌曲", 6 | "key":"V1001_TODAY_MUSIC" 7 | }, 8 | { 9 | "name":"菜单", 10 | "sub_button":[ 11 | { 12 | "type":"view", 13 | "name":"搜索", 14 | "url":"http://www.soso.com/" 15 | }, 16 | { 17 | "type":"click", 18 | "name":"赞一下我们", 19 | "key":"V1001_GOOD" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var API = require('./lib/api_common'); 2 | // 部门管理 3 | API.mixin(require('./lib/api_department')); 4 | // 媒体管理(上传、下载) 5 | API.mixin(require('./lib/api_media')); 6 | // 菜单管理 7 | API.mixin(require('./lib/api_menu')); 8 | // 消息发送 9 | API.mixin(require('./lib/api_message')); 10 | // 标签管理 11 | API.mixin(require('./lib/api_tag')); 12 | // 用户管理 13 | API.mixin(require('./lib/api_user')); 14 | // IP段查询 15 | API.mixin(require('./lib/api_ip')); 16 | // JS SDK 17 | API.mixin(require('./lib/api_js')); 18 | // 企业号应用 19 | API.mixin(require('./lib/api_agent')); 20 | // 通讯录批量操作接口 21 | API.mixin(require('./lib/api_batch')); 22 | // 永久素材管理接口 23 | API.mixin(require('./lib/api_material')); 24 | // 摇一摇接口 25 | API.mixin(require('./lib/api_shake')); 26 | module.exports = API; 27 | -------------------------------------------------------------------------------- /test/api_message.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var config = require('./config'); 3 | var API = require('../'); 4 | 5 | describe('message', function () { 6 | var api = new API(config.corpid, config.corpsecret, '0'); 7 | 8 | before(function (done) { 9 | api.getAccessToken(done); 10 | }); 11 | 12 | it('send message should ok', function (done) { 13 | var to = { 14 | "touser": "@all" 15 | }; 16 | var message = { 17 | "msgtype": "text", 18 | "text": { 19 | "content": "Holiday Request For Pony(http://xxxxx)" 20 | }, 21 | "safe": "0" 22 | }; 23 | api.send(to, message, function (err, data, res) { 24 | expect(err).not.to.be.ok(); 25 | expect(data).to.have.property('errmsg', 'ok'); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "module", 5 | "require", 6 | "__dirname", 7 | "process", 8 | "console", 9 | "it", 10 | "xit", 11 | "describe", 12 | "xdescribe", 13 | "before", 14 | "beforeEach", 15 | "after", 16 | "afterEach" 17 | ], 18 | 19 | "node": true, 20 | "bitwise": true, 21 | "curly": true, 22 | "eqeqeq": true, 23 | "forin": false, 24 | "immed": true, 25 | "latedef": true, 26 | "noarg": true, 27 | "noempty": true, 28 | "nonew": true, 29 | "plusplus": false, 30 | "undef": true, 31 | "strict": false, 32 | "trailing": false, 33 | "globalstrict": true, 34 | "nonstandard": true, 35 | "white": false, 36 | "indent": 2, 37 | "expr": true, 38 | "multistr": true, 39 | "onevar": false, 40 | "unused": "vars" 41 | } 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | ISTANBUL = ./node_modules/.bin/istanbul 5 | MOCHA = ./node_modules/mocha/bin/_mocha 6 | COVERALLS = ./node_modules/coveralls/bin/coveralls.js 7 | 8 | debug: 9 | @NODE_ENV=test ./node_modules/.bin/node-debug $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 10 | $(MOCHA_OPTS) \ 11 | $(TESTS) 12 | 13 | test: 14 | @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 15 | $(MOCHA_OPTS) \ 16 | $(TESTS) 17 | 18 | test-cov: 19 | @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 20 | 21 | test-coveralls: 22 | @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 23 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 24 | @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage 25 | 26 | test-all: test test-coveralls 27 | 28 | .PHONY: test 29 | -------------------------------------------------------------------------------- /lib/api_ip.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var wrapper = util.wrapper; 3 | var make = util.make; 4 | 5 | /** 6 | * 创建标签 7 | * 8 | * 详细请看: 9 | * Examples: 10 | * ``` 11 | * api.getCallbackIP(name, callback); 12 | * ``` 13 | * 14 | * Callback: 15 | * 16 | * - `err`, 调用失败时得到的异常 17 | * - `result`, 调用正常时得到的对象 18 | * 19 | * Result: 20 | * ``` 21 | * { 22 | * "ip_list": ["101.226.103.*", "101.226.62.*"] 23 | * } 24 | * ``` 25 | * @param {Function} callback 回调函数 26 | */ 27 | make(exports, 'getCallbackIP', function (callback) { 28 | // https://qyapi.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN 29 | var url = this.prefix + 'getcallbackip?access_token=' + this.token.accessToken; 30 | this.request(url, wrapper(callback)); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-enterprise-api", 3 | "version": "0.3.0", 4 | "description": "微信公共平台企业号版本Node API库", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-all" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/node-webot/wechat-enterprise-api.git" 12 | }, 13 | "keywords": [ 14 | "weixin", 15 | "wechat", 16 | "enterprise", 17 | "api" 18 | ], 19 | "dependencies": { 20 | "formstream": ">=0.0.8", 21 | "urllib": ">=0.5.17" 22 | }, 23 | "devDependencies": { 24 | "mocha": "*", 25 | "expect.js": "*", 26 | "travis-cov": "*", 27 | "coveralls": "*", 28 | "mocha-lcov-reporter": "*", 29 | "muk": "*", 30 | "rewire": "*", 31 | "istanbul": "*" 32 | }, 33 | "author": "Jackson Tian", 34 | "license": "MIT", 35 | "readmeFilename": "README.md", 36 | "directories": { 37 | "test": "test" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/api_shake.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var wrapper = util.wrapper; 3 | var postJSON = util.postJSON; 4 | var make = util.make; 5 | 6 | 7 | /** 8 | * 获取设备及用户信息 9 | * 10 | * Examples: 11 | * ``` 12 | * api.getShakeInfo("ticket", callback); 13 | * ``` 14 | * 15 | * Callback: 16 | * 17 | * - `err`, 调用失败时得到的异常 18 | * - `result`, 调用正常时得到的对象 19 | * 20 | * Result: 21 | * ``` 22 | * { 23 | * "errcode": 0, 24 | * "errmsg": "ok" 25 | * } 26 | * ``` 27 | * @param {String} ticket 摇周边业务的ticket,可在摇到的URL中得到,ticket生效时间为30分钟,每一次摇都会重新生成新的ticket 28 | * @param {Function} callback 回调函数 29 | */ 30 | 31 | make(exports, 'getShakeInfo', function (ticket, callback) { 32 | //https://qyapi.weixin.qq.com/cgi-bin/shakearound/getshakeinfo?access_token=ACCESS_TOKEN 33 | var url = this.prefix + 'shakearound/getshakeinfo?access_token=' + this.token.accessToken; 34 | var opts = { 35 | "ticket": ticket 36 | }; 37 | this.request(url, postJSON(opts), wrapper(callback)); 38 | }); -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 3 | * 参见:http://mp.weixin.qq.com/wiki/index.php?title=返回码说明 4 | */ 5 | exports.wrapper = function (callback) { 6 | return function (err, data, res) { 7 | callback = callback || function () {}; 8 | if (err) { 9 | err.name = 'WeChatAPI' + err.name; 10 | return callback(err, data, res); 11 | } 12 | if (data.errcode) { 13 | err = new Error(data.errmsg); 14 | err.name = 'WeChatAPIError'; 15 | err.code = data.errcode; 16 | return callback(err, data, res); 17 | } 18 | callback(null, data, res); 19 | }; 20 | }; 21 | 22 | /*! 23 | * 对提交参数一层封装,当POST JSON,并且结果也为JSON时使用 24 | */ 25 | exports.postJSON = function (data) { 26 | return { 27 | dataType: 'json', 28 | type: 'POST', 29 | data: data, 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | } 33 | }; 34 | }; 35 | 36 | /*! 37 | * 用于加快API开发 38 | */ 39 | exports.make = function (host, name, fn) { 40 | host[name] = function () { 41 | this.preRequest(this['_' + name], arguments); 42 | }; 43 | 44 | host['_' + name] = fn; 45 | }; 46 | -------------------------------------------------------------------------------- /test/api_menu.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var config = require('./config'); 3 | var API = require('../'); 4 | 5 | describe('menu', function () { 6 | var api = new API(config.corpid, config.corpsecret, '0'); 7 | before(function (done) { 8 | api.getAccessToken(done); 9 | }); 10 | 11 | it('createMenu should ok', function (done) { 12 | this.timeout(20000); 13 | var menu = JSON.stringify(require('./fixture/menu.json')); 14 | api.createMenu(menu, function (err, result) { 15 | expect(err).not.to.be.ok(); 16 | expect(result).to.have.property('errcode', 0); 17 | expect(result).to.have.property('errmsg', 'ok'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('getMenu should ok', function (done) { 23 | api.getMenu(function (err, menu) { 24 | expect(err).not.to.be.ok(); 25 | expect(menu).to.have.property('menu'); 26 | expect(menu.menu).to.have.property('button'); 27 | expect(menu.menu.button).to.have.length(2); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('removeMenu should ok', function (done) { 33 | api.removeMenu(function (err, result) { 34 | expect(err).not.to.be.ok(); 35 | expect(result).to.have.property('errcode', 0); 36 | expect(result).to.have.property('errmsg', 'ok'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/api_tag.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var config = require('./config'); 3 | var API = require('../'); 4 | 5 | describe('tag', function () { 6 | var api = new API(config.corpid, config.corpsecret); 7 | var id; 8 | before(function (done) { 9 | api.getAccessToken(done); 10 | }); 11 | 12 | it('createTag should ok', function (done) { 13 | api.createTag('tag_' + Math.random(), function (err, data, res) { 14 | expect(err).not.to.be.ok(); 15 | expect(data).to.have.key('tagid'); 16 | id = data.tagid; 17 | done(); 18 | }); 19 | }); 20 | 21 | it('updateTagName should ok', function (done) { 22 | api.updateTagName(id, 'new_tag_' + Math.random(), function (err, data, res) { 23 | expect(err).not.to.be.ok(); 24 | expect(data).to.have.property('errmsg', 'updated'); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('addTagUsers should ok', function (done) { 30 | api.addTagUsers(id, ['JacksonTian'], function (err, data, res) { 31 | expect(err).not.to.be.ok(); 32 | expect(data).to.have.property('errmsg', 'ok'); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('getTagUsers should not ok', function (done) { 38 | api.getTagUsers(id, function (err, data, res) { 39 | expect(err).not.to.be.ok(); 40 | expect(data).to.have.property('errmsg', 'ok'); 41 | expect(data.userlist).to.be.an('array'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('deleteTagUsers should ok', function (done) { 47 | api.deleteTagUsers(id, ['JacksonTian'], function (err, data, res) { 48 | expect(err).not.to.be.ok(); 49 | expect(data).to.have.property('errmsg', 'ok'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('deleteTag should ok', function (done) { 55 | api.deleteTag(id, function (err, data, res) { 56 | expect(err).not.to.be.ok(); 57 | expect(data).to.have.property('errmsg', 'deleted'); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | var xml2js = require('xml2js'); 3 | 4 | exports.tail = function (token, message, get) { 5 | var q = { 6 | timestamp: new Date().getTime(), 7 | nonce: parseInt((Math.random() * 100000000000), 10) 8 | }; 9 | if (get) { 10 | q.echostr = message; 11 | } 12 | var s = [token, q.timestamp, q.nonce, message].sort().join(''); 13 | q.msg_signature = require('crypto').createHash('sha1').update(s).digest('hex'); 14 | return querystring.stringify(q); 15 | }; 16 | 17 | var tpl = '' + 18 | ']]>' + 19 | ']]>' + 20 | ']]>' + 21 | ''; 22 | 23 | exports.template = require('ejs').compile(tpl); 24 | 25 | exports.buildXML = require('ejs').compile('' + 26 | ']]>' + 27 | ']]>' + 28 | '<%-new Date().getTime()%>' + 29 | '' + 30 | ']]>' + 31 | 'msgid' + 32 | '1' + 33 | ''); 34 | 35 | var formatMessage = function (result) { 36 | var message = {}; 37 | if (typeof result === 'object') { 38 | for (var key in result) { 39 | if (result[key].length === 1) { 40 | var val = result[key][0]; 41 | if (typeof val === 'object') { 42 | message[key] = formatMessage(val); 43 | } else { 44 | message[key] = (val || '').trim(); 45 | } 46 | } else { 47 | message = result[key].map(formatMessage); 48 | } 49 | } 50 | } 51 | return message; 52 | }; 53 | 54 | exports.parse = function (xml, callback) { 55 | xml2js.parseString(xml, {trim: true}, function (err, result) { 56 | var xml = formatMessage(result.xml); 57 | callback(null, xml); 58 | }); 59 | }; 60 | 61 | exports.postData = function (token, message) { 62 | var q = { 63 | timestamp: new Date().getTime(), 64 | nonce: parseInt((Math.random() * 100000000000), 10) 65 | }; 66 | 67 | var s = [token, q.timestamp, q.nonce, message].sort().join(''); 68 | var signature = require('crypto').createHash('sha1').update(s).digest('hex'); 69 | q.msg_signature = signature; 70 | 71 | var info = { 72 | msg_encrypt: message, 73 | toAgentID: 'agentid', 74 | toUser: 'to_user' 75 | }; 76 | 77 | return { 78 | xml: exports.template(info), 79 | querystring: querystring.stringify(q) 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /test/api_department.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var config = require('./config'); 3 | var API = require('../'); 4 | 5 | describe('department', function () { 6 | var api = new API(config.corpid, config.corpsecret); 7 | var id; 8 | before(function (done) { 9 | api.getAccessToken(done); 10 | }); 11 | 12 | it('createDepartment(name, parentid)', function (done) { 13 | api.createDepartment('department_' + Math.random(), 1, function (err, data, res) { 14 | expect(err).not.to.be.ok(); 15 | expect(data).to.have.key('id'); 16 | expect(data).to.have.property('errmsg', 'created'); 17 | id = data.id; 18 | done(); 19 | }); 20 | }); 21 | 22 | it('createDepartment(name, opts)', function (done) { 23 | var opts = { 24 | parentid: 1, 25 | order: 1 26 | }; 27 | api.createDepartment('department_' + Math.random(), opts, function (err, data, res) { 28 | expect(err).not.to.be.ok(); 29 | expect(data).to.have.key('id'); 30 | expect(data).to.have.property('errmsg', 'created'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('updateDepartment(id, newname) should ok', function (done) { 36 | api.updateDepartment(id, 'new_department_' + Math.random(), function (err, data, res) { 37 | expect(err).not.to.be.ok(); 38 | expect(data).to.have.property('errmsg', 'updated'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('updateDepartment(id, opts) should ok', function (done) { 44 | api.updateDepartment(id, {}, function (err, data, res) { 45 | expect(err).not.to.be.ok(); 46 | expect(data).to.have.property('errmsg', 'updated'); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('updateDepartment(id, opts) should ok', function (done) { 52 | var opts = {name: 'new_name', parentid: 1, order: 2}; 53 | api.updateDepartment(id, opts, function (err, data, res) { 54 | expect(err).not.to.be.ok(); 55 | expect(data).to.have.property('errmsg', 'updated'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('deleteDepartment should ok', function (done) { 61 | api.deleteDepartment(id, function (err, data, res) { 62 | expect(err).not.to.be.ok(); 63 | expect(data).to.have.property('errmsg', 'deleted'); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('getDepartments should ok', function (done) { 69 | api.getDepartments(function (err, data, res) { 70 | expect(err).not.to.be.ok(); 71 | expect(data).to.have.property('errmsg', 'ok'); 72 | expect(data.department).to.be.an('array'); 73 | data.department.forEach(function (item) { 74 | expect(item).to.have.key('id', 'name', 'parentid'); 75 | }); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wechat enterprise api 2 | ===================== 3 | 4 | 微信公共平台企业号版主动调用API 5 | 6 | ## 模块状态 7 | - [![NPM version](https://badge.fury.io/js/wechat-enterprise-api.png)](http://badge.fury.io/js/wechat-enterprise-api) 8 | - [![Build Status](https://travis-ci.org/node-webot/wechat-enterprise-api.png?branch=master)](https://travis-ci.org/node-webot/wechat-enterprise-api) 9 | - [![Dependencies Status](https://david-dm.org/node-webot/wechat-enterprise-api.png)](https://david-dm.org/node-webot/wechat-enterprise-api) 10 | - [![Coverage Status](https://coveralls.io/repos/node-webot/wechat-enterprise-api/badge.png)](https://coveralls.io/r/node-webot/wechat-enterprise-api) 11 | 12 | ## 功能列表 13 | - 主动消息 14 | - 菜单操作 15 | - 部门管理 16 | - 用户管理 17 | - 标签管理 18 | - 媒体文件 19 | - OAuth API(授权、获取基本信息) 20 | - JS SDK 授权 21 | - 管理企业号应用 22 | - 通讯录批量操作接口 23 | - 永久素材管理接口 24 | - 企业号摇一摇周边接口 25 | 26 | 27 | ## 详细文档 28 | - [文档主页](http://doxmate.cool/node-webot/wechat-enterprise-api/index.html) 29 | - [API文档](http://doxmate.cool/node-webot/wechat-enterprise-api/api.html) 30 | - 代码[测试覆盖率](http://node-webot.github.io/wechat-enterprise-api/coverage/index.html) 31 | - [新手上路](http://node-webot.github.io/wechat-enterprise-api/Getting%20start.html) 32 | 33 | ### 通过代理服务器访问 34 | 35 | #### 场景 36 | 37 | 对于大规模的集群部署模式,为了安全和速度,会有一些负载均衡的节点放在内网的服务器上(即负载均衡的节点与主结点通过内网连接,并且内网服务器上没有外网的IP)。这时,就需要配置代理服务器来使内网的机器可以有限度的访问外网的资源。例如:微信套件中的各种主动调用接口。 38 | 39 | 如何架设代理服务器在这里不做赘述,一般推荐使用squid 3,免费、快速、配置简单。 40 | 41 | #### 技术原理 42 | 43 | 由于需要访问的微信API服务器是https协议,所以普通的http代理模式不能使用。 44 | 而一般都是http协议的代理服务器。 45 | 我们要实现的就是通过http代理通道来走https的请求。 46 | 47 | 基本的步骤是2步: 48 | 49 | - 连接到代理服务器,发送CONNECT命令,打开一个TCP连接。 50 | - 使用上一步打开的TCP连接,发送https的请求。 51 | 52 | #### 实现步骤 53 | 54 | 一、下载[node-tunnel](https://github.com/koichik/node-tunnel) 注意:npm上的版本较老,不支持node v0.10以上的版本。 55 | 56 | 二、使用 httpsOverHttp 这个agent。 57 | 58 | 三、将agent配置给urllib,通过urllib的beforeRequest这个方法。 59 | 60 | ```js 61 | var tunnel = require('tunnel'); 62 | 63 | var agent = tunnel.httpsOverHttp({ 64 | proxy: { 65 | host: 'proxy_host_ip', 66 | port: 3128 67 | } 68 | }); 69 | 70 | api.setOpts({ 71 | beforeRequest:function(options){ 72 | options.agent = agent; 73 | } 74 | }); 75 | 76 | ``` 77 | 78 | ## Show cases 79 | ### Node.js API自动回复 80 | 81 | ![Node.js API自动回复机器人](http://nodeapi.diveintonode.org/assets/qrcode.jpg) 82 | 83 | 欢迎关注。 84 | 85 | 代码: 86 | 87 | 你可以在[CloudFoundry](http://www.cloudfoundry.com/)、[appfog](https://www.appfog.com/)、[BAE](http://developer.baidu.com/wiki/index.php?title=docs/cplat/rt/node.js)等搭建自己的机器人。 88 | 89 | ## License 90 | The MIT license. 91 | 92 | ## 交流群 93 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 94 | 95 | ## 感谢 96 | 感谢以下贡献者: 97 | 98 | ``` 99 | 100 | project : wechat-enterprise-api 101 | repo age : 7 months 102 | active : 20 days 103 | commits : 32 104 | files : 34 105 | authors : 106 | 18 Jackson Tian 56.2% 107 | 12 Nick Ma 37.5% 108 | 2 Qun Lin 6.2% 109 | 110 | ``` 111 | 112 | ## 捐赠 113 | 如果您觉得Wechat企业号版本对您有帮助,欢迎请作者一杯咖啡 114 | 115 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 116 | -------------------------------------------------------------------------------- /lib/api_media.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var formstream = require('formstream'); 4 | var util = require('./util'); 5 | var wrapper = util.wrapper; 6 | 7 | /** 8 | * 上传多媒体文件,分别有图片(image)、语音(voice)、视频(video)和普通文件(file) 9 | * 详情请见: 10 | * Examples: 11 | * ``` 12 | * api.uploadMedia('filepath', type, callback); 13 | * ``` 14 | * Callback: 15 | * 16 | * - `err`, 调用失败时得到的异常 17 | * - `result`, 调用正常时得到的对象 18 | * 19 | * Result: 20 | * ``` 21 | * {"type":"image","media_id":"0000001","created_at":123456789} 22 | * ``` 23 | * Shortcut: 24 | * 25 | * - `exports.uploadImage(filepath, callback);` 26 | * - `exports.uploadVoice(filepath, callback);` 27 | * - `exports.uploadVideo(filepath, callback);` 28 | * - `exports.uploadFile(filepath, callback);` 29 | * 30 | * @param {String} filepath 文件路径 31 | * @param {String} type 媒体类型,可用值有image、voice、video、file 32 | * @param {Function} callback 回调函数 33 | */ 34 | exports.uploadMedia = function (filepath, type, callback) { 35 | this.preRequest(this._uploadMedia, arguments); 36 | }; 37 | 38 | /*! 39 | * 上传多媒体文件的未封装版本 40 | */ 41 | exports._uploadMedia = function (filepath, type, callback) { 42 | var that = this; 43 | fs.stat(filepath, function (err, stat) { 44 | if (err) { 45 | return callback(err); 46 | } 47 | var form = formstream(); 48 | form.file('media', filepath, path.basename(filepath), stat.size); 49 | var url = that.prefix + 'media/upload?access_token=' + that.token.accessToken + '&type=' + type; 50 | var opts = { 51 | dataType: 'json', 52 | type: 'POST', 53 | timeout: 60000, // 60秒超时 54 | headers: form.headers(), 55 | stream: form 56 | }; 57 | that.request(url, opts, wrapper(callback)); 58 | }); 59 | }; 60 | 61 | ['image', 'voice', 'video', 'file'].forEach(function (type) { 62 | var method = 'upload' + type[0].toUpperCase() + type.substring(1); 63 | exports[method] = function (filepath, callback) { 64 | this.uploadMedia(filepath, type, callback); 65 | }; 66 | }); 67 | 68 | /** 69 | * 根据媒体ID获取媒体内容 70 | * 详情请见: 71 | * Examples: 72 | * ``` 73 | * api.getMedia('media_id', callback); 74 | * ``` 75 | * Callback: 76 | * 77 | * - `err`, 调用失败时得到的异常 78 | * - `result`, 调用正常时得到的文件Buffer对象 79 | * - `res`, HTTP响应对象 80 | * 81 | * @param {String} mediaId 媒体文件的ID 82 | * @param {Function} callback 回调函数 83 | */ 84 | exports.getMedia = function (mediaId, callback) { 85 | this.preRequest(this._getMedia, arguments); 86 | }; 87 | 88 | /*! 89 | * 上传多媒体文件的未封装版本 90 | */ 91 | exports._getMedia = function (mediaId, callback) { 92 | var url = this.prefix + 'media/get?access_token=' + this.token.accessToken + '&media_id=' + mediaId; 93 | this.request(url, {}, wrapper(function (err, data, res) { 94 | // handle some err 95 | if (err) { 96 | return callback(err); 97 | } 98 | var contentType = res.headers['content-type']; 99 | if (contentType === 'application/json') { 100 | var ret; 101 | try { 102 | ret = JSON.parse(data); 103 | if (ret.errcode) { 104 | err = new Error(ret.errmsg); 105 | err.name = 'WeChatAPIError'; 106 | } 107 | } catch (ex) { 108 | callback(ex, data, res); 109 | return; 110 | } 111 | callback(err, ret, res); 112 | } else { 113 | // 输出Buffer对象 114 | callback(null, data, res); 115 | } 116 | })); 117 | }; 118 | -------------------------------------------------------------------------------- /lib/api_menu.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var wrapper = util.wrapper; 3 | var postJSON = util.postJSON; 4 | 5 | /** 6 | * 创建自定义菜单 7 | * 详细请看: 8 | * 9 | * Menu: 10 | * ``` 11 | * { 12 | * "button":[ 13 | * { 14 | * "type":"click", 15 | * "name":"今日歌曲", 16 | * "key":"V1001_TODAY_MUSIC" 17 | * }, 18 | * { 19 | * "name":"菜单", 20 | * "sub_button":[ 21 | * { 22 | * "type":"view", 23 | * "name":"搜索", 24 | * "url":"http://www.soso.com/" 25 | * }, 26 | * { 27 | * "type":"click", 28 | * "name":"赞一下我们", 29 | * "key":"V1001_GOOD" 30 | * }] 31 | * }] 32 | * } 33 | * ] 34 | * } 35 | * ``` 36 | * Examples: 37 | * ``` 38 | * api.createMenu(menu, callback); 39 | * ``` 40 | * Callback: 41 | * 42 | * - `err`, 调用失败时得到的异常 43 | * - `result`, 调用正常时得到的对象 44 | * 45 | * Result: 46 | * ``` 47 | * {"errcode":0,"errmsg":"ok"} 48 | * ``` 49 | * @param {Object} menu 菜单对象 50 | * @param {Function} callback 回调函数 51 | */ 52 | exports.createMenu = function (menu, callback) { 53 | this.preRequest(this._createMenu, arguments); 54 | }; 55 | 56 | /*! 57 | * 创建自定义菜单的未封装版本 58 | */ 59 | exports._createMenu = function (menu, callback) { 60 | var url = this.prefix + 'menu/create?agentid=' + this.agentid + '&access_token=' + this.token.accessToken; 61 | this.request(url, postJSON(menu), wrapper(callback)); 62 | }; 63 | 64 | /** 65 | * 获取菜单 66 | * 详细请看: 67 | * 68 | * Examples: 69 | * ``` 70 | * api.getMenu(callback); 71 | * ``` 72 | * Callback: 73 | * 74 | * - `err`, 调用失败时得到的异常 75 | * - `result`, 调用正常时得到的对象 76 | * 77 | * Result: 78 | * ``` 79 | * // 结果示例 80 | * { 81 | * "menu": { 82 | * "button":[ 83 | * {"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]}, 84 | * {"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]}, 85 | * {"name":"菜单","sub_button":[ 86 | * {"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]}, 87 | * {"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]}, 88 | * {"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}] 89 | * } 90 | * ] 91 | * } 92 | * } 93 | * ``` 94 | * @param {Function} callback 回调函数 95 | */ 96 | exports.getMenu = function (callback) { 97 | this.preRequest(this._getMenu, arguments); 98 | }; 99 | 100 | /*! 101 | * 获取自定义菜单的未封装版本 102 | */ 103 | exports._getMenu = function (callback) { 104 | var url = this.prefix + 'menu/get?agentid=' + this.agentid + '&access_token=' + this.token.accessToken; 105 | this.request(url, {dataType: 'json'}, wrapper(callback)); 106 | }; 107 | 108 | /** 109 | * 删除自定义菜单 110 | * 详细请看: 111 | * Examples: 112 | * ``` 113 | * api.removeMenu(callback); 114 | * ``` 115 | * Callback: 116 | * 117 | * - `err`, 调用失败时得到的异常 118 | * - `result`, 调用正常时得到的对象 119 | * 120 | * Result: 121 | * ``` 122 | * {"errcode":0,"errmsg":"ok"} 123 | * ``` 124 | * @param {Function} callback 回调函数 125 | */ 126 | exports.removeMenu = function (callback) { 127 | this.preRequest(this._removeMenu, arguments); 128 | }; 129 | 130 | /*! 131 | * 删除自定义菜单的未封装版本 132 | */ 133 | exports._removeMenu = function (callback) { 134 | var url = this.prefix + 'menu/delete?agentid=' + this.agentid + '&access_token=' + this.token.accessToken; 135 | this.request(url, {dataType: 'json'}, wrapper(callback)); 136 | }; 137 | -------------------------------------------------------------------------------- /test/api_user.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var config = require('./config'); 3 | var API = require('../'); 4 | 5 | var puling = 'ofL4cs7hr04cJIcu600_W-ZwwxHg'; 6 | 7 | describe('user', function () { 8 | var api = new API(config.corpid, config.corpsecret); 9 | before(function (done) { 10 | api.getAccessToken(done); 11 | }); 12 | 13 | it('getUser should not ok', function (done) { 14 | api.getUser(puling, function (err, data, res) { 15 | expect(err).to.be.ok(); 16 | expect(err).to.have.property('name', 'WeChatAPIError'); 17 | expect(err).to.have.property('message', 'userid not found'); 18 | done(); 19 | }); 20 | }); 21 | 22 | var userid = 'id_' + Math.random(); 23 | 24 | it('createUser should ok', function (done) { 25 | var user = { 26 | "userid": userid, 27 | "name": "张三", 28 | "department": [1], 29 | "position": "产品经理", 30 | "mobile": "15913215422", 31 | "gender": 1, 32 | "tel": "62394", 33 | "email": "zhangsan@dev.com", 34 | "extattr": { 35 | "attrs":[ 36 | {"name":"爱好","value":"旅游"}, 37 | {"name":"卡号","value":"1234567234"} 38 | ] 39 | } 40 | }; 41 | api.createUser(user, function (err, data, res) { 42 | expect(err).not.to.be.ok(); 43 | expect(data).to.have.property('errmsg', 'created'); 44 | done(); 45 | }); 46 | }); 47 | 48 | 49 | it('updateUser should not ok', function (done) { 50 | var user = { 51 | "userid": userid, 52 | "name": "new_name", 53 | "department": [1], 54 | "position": "产品经理", 55 | "mobile": "15913215423", 56 | "gender": 1, 57 | "tel": "62394", 58 | "email": "zhangsan@dev.com", 59 | "extattr": { 60 | "attrs":[ 61 | {"name":"爱好","value":"旅游"}, 62 | {"name":"卡号","value":"1234567234"} 63 | ] 64 | } 65 | }; 66 | api.updateUser(user, function (err, data, res) { 67 | expect(err).not.to.be.ok(); 68 | expect(data).to.have.property('errmsg', 'updated'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('getUser should ok', function (done) { 74 | api.getUser(userid, function (err, data, res) { 75 | expect(err).not.to.be.ok(); 76 | expect(data).to.have.property('errmsg', 'ok'); 77 | expect(data).to.have.keys('userid', 'name', 'department', 'position', 78 | 'mobile', 'gender', 'tel', 'email', 'status', 'extattr'); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('deleteUser should ok', function (done) { 84 | api.deleteUser(userid, function (err, data, res) { 85 | expect(err).not.to.be.ok(); 86 | expect(data).to.have.property('errmsg', 'deleted'); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('getDepartmentUsers should ok', function (done) { 92 | api.getDepartmentUsers(1, 0, 0, function (err, data, res) { 93 | expect(err).not.to.be.ok(); 94 | expect(data).to.have.property('errmsg', 'ok'); 95 | expect(data.userlist).to.be.an('array'); 96 | data.userlist.forEach(function (item) { 97 | expect(item).to.have.key('userid', 'name'); 98 | }); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('getUserIdByCode should not ok', function (done) { 104 | api.getUserIdByCode('code', function (err, data, res) { 105 | expect(err).to.be.ok(); 106 | expect(err).to.have.property('message', 'invalid code'); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('getAuthorizeURL should ok', function () { 112 | var url = api.getAuthorizeURL('http://baidu.com/callback'); 113 | expect(url).to.be('https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx4e2c2b771c467c9f&redirect_uri=http%3A%2F%2Fbaidu.com%2Fcallback&response_type=code&scope=snsapi_base&state=#wechat_redirect'); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /lib/api_message.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var extend = require('util')._extend; 3 | var wrapper = util.wrapper; 4 | var postJSON = util.postJSON; 5 | 6 | /** 7 | * 发送消息分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 8 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=发送接口说明 9 | * Examples: 10 | * ``` 11 | * api.send(to, message, callback); 12 | * ``` 13 | * To: 14 | * ``` 15 | * { 16 | * "touser": "UserID1|UserID2|UserID3", 17 | * "toparty": " PartyID1 | PartyID2 ", 18 | * "totag": " TagID1 | TagID2 " 19 | * } 20 | * ``` 21 | * Message: 22 | * 文本消息: 23 | * ``` 24 | * { 25 | * "msgtype": "text", 26 | * "text": { 27 | * "content": "Holiday Request For Pony(http://xxxxx)" 28 | * }, 29 | * "safe":"0" 30 | * } 31 | * ``` 32 | * 图片消息: 33 | * ``` 34 | * { 35 | * "msgtype": "image", 36 | * "image": { 37 | * "media_id": "MEDIA_ID" 38 | * }, 39 | * "safe":"0" 40 | * } 41 | * ``` 42 | * 图片消息: 43 | * ``` 44 | * { 45 | * "msgtype": "image", 46 | * "image": { 47 | * "media_id": "MEDIA_ID" 48 | * }, 49 | * "safe":"0" 50 | * } 51 | * ``` 52 | * 语音消息: 53 | * ``` 54 | * { 55 | * "msgtype": "voice", 56 | * "voice": { 57 | * "media_id": "MEDIA_ID" 58 | * }, 59 | * "safe":"0" 60 | * } 61 | * ``` 62 | * 视频消息: 63 | * ``` 64 | * { 65 | * "msgtype": "video", 66 | * "video": { 67 | * "media_id": "MEDIA_ID" 68 | * "title": "Title", 69 | * "description": "Description" 70 | * }, 71 | * "safe":"0" 72 | * } 73 | * ``` 74 | * 文件消息: 75 | * ``` 76 | * { 77 | * "msgtype": "file", 78 | * "file": { 79 | * "media_id": "MEDIA_ID" 80 | * }, 81 | * "safe":"0" 82 | * } 83 | * ``` 84 | * 图文消息: 85 | * ``` 86 | * { 87 | * "msgtype": "news", 88 | * "news": { 89 | * "articles":[ 90 | * { 91 | * "title": "Title", 92 | * "description": "Description", 93 | * "url": "URL", 94 | * "picurl": "PIC_URL", 95 | * }, 96 | * { 97 | * "title": "Title", 98 | * "description": "Description", 99 | * "url": "URL", 100 | * "picurl": "PIC_URL", 101 | * } 102 | * ] 103 | * }, 104 | * "safe":"0" 105 | * } 106 | * ``` 107 | * MP消息: 108 | * ``` 109 | * { 110 | * "msgtype": "mpnews", 111 | * "mpnews": { 112 | * "articles":[ 113 | * { 114 | * "thumb_media_id": "id", 115 | * "author": "Author", 116 | * "content_source_url": "URL", 117 | * "content": "Content" 118 | * "digest": "Digest description", 119 | * "show_cover_pic": "0" 120 | * }, 121 | * { 122 | * "thumb_media_id": "id", 123 | * "author": "Author", 124 | * "content_source_url": "URL", 125 | * "content": "Content" 126 | * "digest": "Digest description", 127 | * "show_cover_pic": "0" 128 | * } 129 | * ], 130 | * "media_id": "id" 131 | * }, 132 | * "safe":"0" 133 | * } 134 | * ``` 135 | * Callback: 136 | * 137 | * - `err`, 调用失败时得到的异常 138 | * - `result`, 调用正常时得到的对象 139 | * 140 | * Result: 141 | * ``` 142 | * { 143 | * "errcode": 0, 144 | * "errmsg": "ok", 145 | * "invaliduser": "UserID1", 146 | * "invalidparty":"PartyID1", 147 | * "invalidtag":"TagID1" 148 | * } 149 | * ``` 150 | * 151 | * @param {Object} to 接受消息的用户 152 | * @param {Object} message 消息对象 153 | * @param {Function} callback 回调函数 154 | */ 155 | exports.send = function (to, message, callback) { 156 | this.preRequest(this._send, arguments); 157 | }; 158 | 159 | /*! 160 | * 发送消息的未封装版本 161 | */ 162 | exports._send = function (to, message, callback) { 163 | // https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN 164 | var url = this.prefix + 'message/send?access_token=' + this.token.accessToken; 165 | var data = { 166 | agentid: this.agentid 167 | }; 168 | extend(data, to); 169 | extend(data, message); 170 | 171 | this.request(url, postJSON(data), wrapper(callback)); 172 | }; 173 | -------------------------------------------------------------------------------- /lib/api_agent.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var wrapper = util.wrapper; 3 | var postJSON = util.postJSON; 4 | var make = util.make; 5 | 6 | /** 7 | * 获取企业号应用 8 | * 详细请看: 9 | * 10 | * Examples: 11 | * ``` 12 | * api.getAgent(agentid, callback); 13 | * ``` 14 | * 15 | * Callback: 16 | * 17 | * - `err`, 调用失败时得到的异常 18 | * - `result`, 调用正常时得到的对象 19 | * 20 | * Result: 21 | * ``` 22 | * { 23 | * "errcode":"0", 24 | * "errmsg":"ok" , 25 | * "agentid":"1" , 26 | * "name":"NAME" , 27 | * "square_logo_url":"xxxxxxxx" , 28 | * "round_logo_url":"yyyyyyyy" , 29 | * "description":"desc" , 30 | * "allow_userinfos":{ 31 | * "user":[ 32 | * { 33 | * "userid":"id1", 34 | * "status":"1" 35 | * }, 36 | * { 37 | * "userid":"id2", 38 | * "status":"1" 39 | * }, 40 | * { 41 | * "userid":"id3", 42 | * "status":"1" 43 | * } 44 | * ] 45 | * }, 46 | * "allow_partys":{ 47 | * "partyid": [1] 48 | * }, 49 | * "allow_tags":{ 50 | * "tagid": [1,2,3] 51 | * } 52 | * "close":0 , 53 | * "redirect_domain":"www.qq.com", 54 | * "report_location_flag":0, 55 | * "isreportuser":0, 56 | * "isreportenter":0 57 | * ``` 58 | * @param {String} agentid 企业号应用的id 59 | * @param {Function} callback 回调函数 60 | */ 61 | make(exports, 'getAgent', function (agentid, callback) { 62 | if (typeof agentid === 'function') { 63 | callback = agentid; 64 | agentid = this.agentid; 65 | } 66 | if (!agentid) { 67 | throw new Error('It requires an agentid.'); 68 | } 69 | var url = this.prefix + 'agent/get?access_token=' + this.token.accessToken; 70 | var opts = { 71 | dataType: 'json', 72 | data: { 73 | agentid: agentid 74 | } 75 | }; 76 | this.request(url, opts, wrapper(callback)); 77 | }); 78 | 79 | /** 80 | * 设置企业号应用 81 | * 详细请看: 82 | * 83 | * Examples: 84 | * ``` 85 | * api.setAgent(opts, callback); 86 | * ``` 87 | * Opts: 88 | * ``` 89 | * { 90 | * "agentid": "5", 91 | * "report_location_flag": "0", 92 | * "logo_mediaid": "xxxxx", 93 | * "name": "NAME", 94 | * "description": "DESC", 95 | * "redirect_domain": "xxxxxx", 96 | * "isreportuser":0, 97 | * "isreportenter":0 98 | * } 99 | * ``` 100 | * 101 | * Callback: 102 | * 103 | * - `err`, 调用失败时得到的异常 104 | * - `result`, 调用正常时得到的对象 105 | * 106 | * ``` 107 | * 108 | * Result: 109 | * { 110 | * "errcode":"0", 111 | * "errmsg":"ok" , 112 | * } 113 | * ``` 114 | * @param {Object} opts 更新的数据 115 | * @param {Function} callback 回调函数 116 | */ 117 | make(exports, 'setAgent', function (opts, callback) { 118 | var url = this.prefix + 'agent/set?access_token=' + this.token.accessToken; 119 | this.request(url, postJSON(opts), wrapper(callback)); 120 | }); 121 | 122 | 123 | /** 124 | * 获取应用概况列表 125 | * 详细请看: 126 | * 127 | * Examples: 128 | * ``` 129 | * api.listAgent(callback); 130 | * ``` 131 | * 132 | * Callback: 133 | * 134 | * - `err`, 调用失败时得到的异常 135 | * - `result`, 调用正常时得到的对象 136 | * 137 | * ``` 138 | * 139 | * Result: 140 | * { 141 | * "errcode": 0, 142 | * "errmsg": "ok", 143 | * "agentlist": [ 144 | * { 145 | * "agentid": "5", 146 | * "name": "企业小助手", 147 | * "square_logo_url": "url", 148 | * "round_logo_url": "url" 149 | * }, 150 | * { 151 | * "agentid": "8", 152 | * "name": "HR小助手", 153 | * "square_logo_url": "url", 154 | * "round_logo_url": "url" 155 | * } 156 | * ] 157 | * } 158 | * ``` 159 | * @param {Function} callback 回调函数 160 | */ 161 | make(exports, 'listAgent', function (callback) { 162 | var url = this.prefix + 'agent/list?access_token=' + this.token.accessToken; 163 | var opts = { 164 | dataType: 'json' 165 | }; 166 | this.request(url, opts, wrapper(callback)); 167 | }); 168 | -------------------------------------------------------------------------------- /test/api_common.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var urllib = require('urllib'); 3 | var muk = require('muk'); 4 | var config = require('./config'); 5 | var API = require('../'); 6 | 7 | describe('common.js', function () { 8 | describe('mixin', function () { 9 | it('should ok', function () { 10 | API.mixin({sayHi: function () {}}); 11 | expect(API.prototype).to.have.property('sayHi'); 12 | }); 13 | 14 | it('should not ok when override method', function () { 15 | var obj = {sayHi: function () {}}; 16 | expect(API.mixin).withArgs(obj).to.throwException(/Don't allow override existed prototype method\./); 17 | }); 18 | }); 19 | 20 | describe('getAccessToken', function () { 21 | it('should ok', function (done) { 22 | var api = new API(config.corpid, config.corpsecret); 23 | api.getAccessToken(function (err, token) { 24 | expect(err).not.to.be.ok(); 25 | expect(token).to.only.have.keys('accessToken'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should not ok', function (done) { 31 | var api = new API('corpid', 'corpsecret'); 32 | api.getAccessToken(function (err, token) { 33 | expect(err).to.be.ok(); 34 | expect(err).to.have.property('name', 'WeChatAPIError'); 35 | expect(err).to.have.property('code', 40013); 36 | expect(err).to.have.property('message', 'invalid corpid'); 37 | done(); 38 | }); 39 | }); 40 | 41 | describe('mock urllib err', function () { 42 | before(function () { 43 | muk(urllib, 'request', function (url, args, callback) { 44 | var err = new Error('Urllib Error'); 45 | err.name = 'UrllibError'; 46 | callback(err); 47 | }); 48 | }); 49 | 50 | after(function () { 51 | muk.restore(); 52 | }); 53 | 54 | it('should get mock error', function (done) { 55 | var api = new API('appid', 'secret'); 56 | api.getAccessToken(function (err, token) { 57 | expect(err).to.be.ok(); 58 | expect(err.name).to.be('WeChatAPIUrllibError'); 59 | expect(err.message).to.be('Urllib Error'); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('mock token', function () { 66 | before(function () { 67 | muk(urllib, 'request', function (url, args, callback) { 68 | process.nextTick(function () { 69 | callback(null, {"access_token": "ACCESS_TOKEN","expires_in": 7200}); 70 | }); 71 | }); 72 | }); 73 | after(function () { 74 | muk.restore(); 75 | }); 76 | 77 | it('should ok', function (done) { 78 | var api = new API('appid', 'secret'); 79 | api.getAccessToken(function (err, token) { 80 | expect(err).to.not.be.ok(); 81 | expect(token).to.have.property('accessToken', 'ACCESS_TOKEN'); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('getLatestToken', function () { 89 | it('should ok', function (done) { 90 | var api = new API(config.corpid, config.corpsecret); 91 | api.getLatestToken(function (err, token) { 92 | expect(err).not.to.be.ok(); 93 | expect(token).to.only.have.keys('accessToken'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should get mock error', function (done) { 99 | var api = new API(config.corpid, config.corpsecret, 1, function (callback) { 100 | callback(new Error('mock error')); 101 | }); 102 | api.getLatestToken(function (err, token) { 103 | expect(err).to.be.ok(); 104 | expect(err).to.have.property('message', 'mock error'); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should get ok', function (done) { 110 | var api = new API(config.corpid, config.corpsecret, 1, function (callback) { 111 | callback(null, {accessToken: 'token'}); 112 | }); 113 | api.getLatestToken(function (err, token) { 114 | expect(err).not.to.be.ok(); 115 | expect(token).to.have.property('accessToken', 'token'); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /lib/api_department.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var wrapper = util.wrapper; 3 | var postJSON = util.postJSON; 4 | 5 | /** 6 | * 创建部门 7 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=管理部门 8 | * 9 | * Examples: 10 | * ``` 11 | * api.createDepartment(name, opts, callback); 12 | * ``` 13 | * Opts: 14 | * - `parentid`, 父部门id,根部门id为1 15 | * - `order`,在父部门中的次序。从1开始,数字越大排序越靠后 16 | * - `id`,部门ID。用指定部门ID新建部门,不指定此参数时,则自动生成 17 | * 18 | * Callback: 19 | * 20 | * - `err`, 调用失败时得到的异常 21 | * - `result`, 调用正常时得到的对象 22 | * 23 | * Result: 24 | * ``` 25 | * { 26 | * "errcode": 0, 27 | * "errmsg": "created", 28 | * "id": 2 29 | * } 30 | * ``` 31 | * @param {String} name 部门名字 32 | * @param {Object} opts 选项 33 | * @param {Function} callback 回调函数 34 | */ 35 | exports.createDepartment = function (name, opts, callback) { 36 | this.preRequest(this._createDepartment, arguments); 37 | }; 38 | 39 | /*! 40 | * 创建部门的未封装版本 41 | */ 42 | exports._createDepartment = function (name, opts, callback) { 43 | // https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=ACCESS_TOKEN 44 | var url = this.prefix + 'department/create?access_token=' + this.token.accessToken; 45 | var data = { 46 | name: name 47 | }; 48 | if (typeof opts === 'object') { 49 | data.parentid = Number(opts.parentid) || 1; 50 | data.order = Number(opts.order) || 1; 51 | if (opts.id) { 52 | data.id = Number(opts.id); 53 | }; 54 | } else { 55 | data.parentid = Number(opts); 56 | } 57 | 58 | this.request(url, postJSON(data), wrapper(callback)); 59 | }; 60 | 61 | /** 62 | * 更新部门 63 | * 64 | * Examples: 65 | * ``` 66 | * var opts = {name: 'new name', parentid: 1, order: 5}; 67 | * api.updateDepartment(id, opts, callback); 68 | * ``` 69 | * 70 | * Opts: 71 | * - `name`, 新的部门名字。可选 72 | * - `parentid`, 父部门id,根部门id为1。可选 73 | * - `order`,在父部门中的次序。从1开始,数字越大排序越靠后。可选,默认为1 74 | * 75 | * Callback: 76 | * 77 | * - `err`, 调用失败时得到的异常 78 | * - `result`, 调用正常时得到的对象 79 | * 80 | * Result: 81 | * ``` 82 | * { 83 | * "errcode": 0, 84 | * "errmsg": "updated" 85 | * } 86 | * ``` 87 | * @param {Number} id 部门ID 88 | * @param {Object} opts 选项 89 | * @param {Function} callback 回调函数 90 | */ 91 | exports.updateDepartment = function (id, opts, callback) { 92 | this.preRequest(this._updateDepartment, arguments); 93 | }; 94 | 95 | /*! 96 | * 更新部门的未封装版本 97 | */ 98 | exports._updateDepartment = function (id, opts, callback) { 99 | // https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=ACCESS_TOKEN 100 | var url = this.prefix + 'department/update?access_token=' + this.token.accessToken; 101 | var data = { 102 | id: Number(id) 103 | }; 104 | if (typeof opts === 'object') { 105 | if (opts.name) { 106 | data.name = opts.name; 107 | } 108 | if (opts.parentid) { 109 | data.parentid = Number(opts.parentid); 110 | } 111 | if (opts.order) { 112 | data.order = Number(opts.order) || 1; 113 | } 114 | } else { 115 | data.name = opts; 116 | } 117 | 118 | this.request(url, postJSON(data), wrapper(callback)); 119 | }; 120 | 121 | /** 122 | * 删除部门 123 | * 124 | * Examples: 125 | * ``` 126 | * api.deleteDepartment(id, callback); 127 | * api.deleteDepartment([id1, id2], callback); 128 | * ``` 129 | * 130 | * Callback: 131 | * 132 | * - `err`, 调用失败时得到的异常 133 | * - `result`, 调用正常时得到的对象 134 | * 135 | * Result: 136 | * ``` 137 | * { 138 | * "errcode": 0, 139 | * "errmsg": "deleted" 140 | * } 141 | * ``` 142 | * @param {Number|Array} id 部门ID 143 | * @param {Function} callback 回调函数 144 | */ 145 | exports.deleteDepartment = function (id, callback) { 146 | this.preRequest(this._deleteDepartment, arguments); 147 | }; 148 | 149 | /*! 150 | * 删除部门的未封装版本 151 | */ 152 | exports._deleteDepartment = function (id, callback) { 153 | // https://qyapi.weixin.qq.com/cgi-bin/department/delete?access_token=ACCESS_TOKEN&id=1&id=2 154 | var url = this.prefix + 'department/delete?access_token=' + this.token.accessToken; 155 | var opts = {dataType: 'json', data: {id: id}}; 156 | this.request(url, opts, wrapper(callback)); 157 | }; 158 | 159 | /** 160 | * 查看所有部门 161 | * 162 | * Examples: 163 | * ``` 164 | * api.getDepartments(callback); 165 | * ``` 166 | * 167 | * Callback: 168 | * 169 | * - `err`, 调用失败时得到的异常 170 | * - `result`, 调用正常时得到的对象 171 | * 172 | * Result: 173 | * ``` 174 | * { 175 | * "errcode": 0, 176 | * "errmsg": "ok", 177 | * "department": [ 178 | * { 179 | * "id": 1, 180 | * "name": "广州研发中心", 181 | * "parentid": 0 182 | * }, 183 | * { 184 | * "id": 2 185 | * "name": "邮箱产品部", 186 | * "parentid": 1 187 | * } 188 | * ] 189 | * } 190 | * ``` 191 | * @param {Function} callback 回调函数 192 | */ 193 | exports.getDepartments = function (callback) { 194 | this.preRequest(this._getDepartments, arguments); 195 | }; 196 | 197 | /*! 198 | * 获取部门列表的未封装版本 199 | */ 200 | exports._getDepartments = function (callback) { 201 | // https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=ACCESS_TOKEN 202 | var url = this.prefix + 'department/list?access_token=' + this.token.accessToken; 203 | this.request(url, {dataType: 'json'}, wrapper(callback)); 204 | }; 205 | -------------------------------------------------------------------------------- /test/api_media.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var muk = require('muk'); 3 | var urllib = require('urllib'); 4 | 5 | var config = require('./config'); 6 | var API = require('../'); 7 | var path = require('path'); 8 | 9 | describe('media', function () { 10 | var api = new API(config.corpid, config.corpsecret); 11 | before(function (done) { 12 | api.getAccessToken(done); 13 | }); 14 | 15 | 16 | describe('upload media', function () { 17 | var fixture = { 18 | 'Image': path.join(__dirname, './fixture/image.jpg'), 19 | 'Voice': path.join(__dirname, './fixture/test.mp3'), 20 | 'Video': path.join(__dirname, './fixture/movie.mp4'), 21 | 'File': path.join(__dirname, './fixture/pic.jpg') 22 | }; 23 | 24 | before(function () { 25 | muk(api, 'uploadVideo', function (filepath, callback) { 26 | if (filepath === path.join(__dirname, './fixture/inexist.jpg')) { 27 | return process.nextTick(function () { 28 | var err = new Error(); 29 | err.code = 'ENOENT'; 30 | callback(err); 31 | }); 32 | } 33 | var data = { 34 | created_at: '', 35 | media_id: '', 36 | type: 'video' 37 | }; 38 | var res = { 39 | headers: { 40 | 'content-type': 'image/jpeg' 41 | } 42 | }; 43 | process.nextTick(function () { 44 | callback(null, data, res); 45 | }); 46 | }); 47 | }); 48 | 49 | after(function () { 50 | muk.restore(); 51 | }); 52 | 53 | ['Image', 'Voice', 'Video', 'File'].forEach(function (method) { 54 | it('upload' + method + ' should ok', function (done) { 55 | // 上传文件比较慢 56 | this.timeout(60000); 57 | api['upload' + method](fixture[method], function (err, data, res) { 58 | expect(err).to.not.be.ok(); 59 | expect(data.type).to.be(method.toLowerCase()); 60 | if (method === 'Thumb') { 61 | expect(data).to.have.property('thumb_media_id'); 62 | } else { 63 | expect(data).to.have.property('media_id'); 64 | } 65 | expect(data).to.have.property('created_at'); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('upload' + method + ' should not ok', function (done) { 71 | api['upload' + method](path.join(__dirname, './fixture/inexist.jpg'), function (err, data, res) { 72 | expect(err).to.be.ok(); 73 | expect(err).to.have.property('name', 'Error'); 74 | expect(err).to.have.property('code', 'ENOENT'); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('get media with buffer', function () { 82 | before(function () { 83 | muk(urllib, 'request', function (url, args, callback) { 84 | var buffer = new Buffer('Hello world!'); 85 | var res = { 86 | headers: { 87 | 'content-type': 'image/jpeg' 88 | } 89 | }; 90 | process.nextTick(function () { 91 | callback(null, buffer, res); 92 | }); 93 | }); 94 | }); 95 | 96 | after(function () { 97 | muk.restore(); 98 | }); 99 | 100 | it('getMedia with buffer', function (done) { 101 | api.getMedia('media_id', function (err, data, res) { 102 | expect(err).to.not.be.ok(); 103 | expect(data.toString()).to.be('Hello world!'); 104 | done(); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('get media with json', function () { 110 | before(function () { 111 | muk(urllib, 'request', function (url, args, callback) { 112 | var data = JSON.stringify({"errcode":40007, "errmsg":"invalid media_id"}); 113 | var res = { 114 | headers: { 115 | 'content-type': 'application/json' 116 | } 117 | }; 118 | process.nextTick(function () { 119 | callback(null, data, res); 120 | }); 121 | }); 122 | }); 123 | 124 | after(function () { 125 | muk.restore(); 126 | }); 127 | it('getMedia with json', function (done) { 128 | api.getMedia('media_id', function (err, data, res) { 129 | expect(err).to.be.ok(); 130 | expect(err).to.have.property('name', 'WeChatAPIError'); 131 | expect(err).to.have.property('message', 'invalid media_id'); 132 | done(); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('get media with err json', function () { 138 | before(function () { 139 | muk(urllib, 'request', function (url, args, callback) { 140 | var data = '{"errcode":40007, "errmsg":"invalid media_id"'; 141 | var res = { 142 | headers: { 143 | 'content-type': 'application/json' 144 | } 145 | }; 146 | process.nextTick(function () { 147 | callback(null, data, res); 148 | }); 149 | }); 150 | }); 151 | 152 | after(function () { 153 | muk.restore(); 154 | }); 155 | it('getMedia with err json', function (done) { 156 | api.getMedia('media_id', function (err, data, res) { 157 | expect(err).to.be.ok(); 158 | expect(err).to.have.property('name', 'SyntaxError'); 159 | done(); 160 | }); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /lib/api_js.js: -------------------------------------------------------------------------------- 1 | var urllib = require('urllib'); 2 | var util = require('./util'); 3 | var crypto = require('crypto'); 4 | var wrapper = util.wrapper; 5 | 6 | var Ticket = function (ticket, expireTime) { 7 | if (!(this instanceof Ticket)) { 8 | return new Ticket(ticket, expireTime); 9 | } 10 | this.ticket = ticket; 11 | this.expireTime = expireTime; 12 | }; 13 | 14 | Ticket.prototype.isValid = function () { 15 | return !!this.ticket && (new Date().getTime()) < this.expireTime; 16 | }; 17 | 18 | /** 19 | * 多台服务器负载均衡时,ticketToken需要外部存储共享。 20 | * 需要调用此registerTicketHandle来设置获取和保存的自定义方法。 21 | * 22 | * Examples: 23 | * ``` 24 | * api.registerTicketHandle(getTicketToken, saveTicketToken); 25 | * // getTicketToken 26 | * function getTicketToken(callback) { 27 | * settingModel.getItem({key: 'weixin_ticketToken'}, function (err, setting) { 28 | * if (err) return callback(err); 29 | * callback(null, setting.value); 30 | * }); 31 | * } 32 | * // saveTicketToken 33 | * function saveTicketToken(_ticketToken, callback) { 34 | * settingModel.setItem({key:'weixin_ticketToken', value: ticketToken}, function (err) { 35 | * if (err) return callback(err); 36 | * callback(null); 37 | * }); 38 | * } 39 | * ``` 40 | * 41 | * @param {Function} getTicketToken 获取外部ticketToken的函数 42 | * @param {Function} saveTicketToken 存储外部ticketToken的函数 43 | */ 44 | exports.registerTicketHandle = function (getTicketToken, saveTicketToken) { 45 | if (!getTicketToken && !saveTicketToken) { 46 | this.ticketStore = {}; 47 | } 48 | this.getTicketToken = getTicketToken || function (callback) { 49 | callback(null, this.ticketStore); 50 | }; 51 | 52 | this.saveTicketToken = saveTicketToken || function (ticketToken, callback) { 53 | this.ticketStore = ticketToken; 54 | if (process.env.NODE_ENV === 'production') { 55 | console.warn("Dont save ticket in memory, when cluster or multi-computer!"); 56 | } 57 | callback(null); 58 | }; 59 | }; 60 | 61 | /** 62 | * 获取js sdk所需的有效js ticket 63 | * 64 | * Callback: 65 | * - `err`, 异常对象 66 | * - `result`, 正常获取时的数据 67 | * 68 | * Result: 69 | * - `errcode`, 0为成功 70 | * - `errmsg`, 成功为'ok',错误则为详细错误信息 71 | * - `ticket`, js sdk有效票据,如:bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA 72 | * - `expires_in`, 有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket 73 | * 74 | * @param {Function} callback 回调函数 75 | */ 76 | exports.getTicket = function (callback) { 77 | this.preRequest(this._getTicket, arguments); 78 | }; 79 | 80 | exports._getTicket = function (callback) { 81 | var that = this; 82 | var url = this.prefix + 'get_jsapi_ticket?access_token=' + this.token.accessToken; 83 | this.request(url, {dataType: 'json'}, wrapper(function(err, data) { 84 | if (err) { 85 | return callback(err); 86 | } 87 | // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 88 | var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000; 89 | var ticket = new Ticket(data.ticket, expireTime); 90 | that.saveTicketToken(ticket, function (err) { 91 | if (err) { 92 | return callback(err); 93 | } 94 | callback(err, ticket); 95 | }); 96 | })); 97 | }; 98 | 99 | /*! 100 | * 生成随机字符串 101 | */ 102 | var createNonceStr = function () { 103 | return Math.random().toString(36).substr(2, 15); 104 | }; 105 | 106 | /*! 107 | * 生成时间戳 108 | */ 109 | var createTimestamp = function () { 110 | return parseInt(new Date().getTime() / 1000) + ''; 111 | }; 112 | 113 | /*! 114 | * 排序查询字符串 115 | */ 116 | var raw = function (args) { 117 | var keys = Object.keys(args); 118 | keys = keys.sort(); 119 | var newArgs = {}; 120 | keys.forEach(function (key) { 121 | newArgs[key.toLowerCase()] = args[key]; 122 | }); 123 | 124 | var string = ''; 125 | for (var k in newArgs) { 126 | string += '&' + k + '=' + newArgs[k]; 127 | } 128 | return string.substr(1); 129 | }; 130 | 131 | /*! 132 | * 签名算法 133 | * 134 | * @param {String} nonceStr 生成签名的随机串 135 | * @param {String} jsapi_ticket 用于签名的jsapi_ticket 136 | * @param {String} timestamp 时间戳 137 | * @param {String} url 用于签名的url,注意必须与调用JSAPI时的页面URL完全一致 138 | */ 139 | var sign = function (nonceStr, jsapi_ticket, timestamp, url) { 140 | var ret = { 141 | jsapi_ticket: jsapi_ticket, 142 | nonceStr: nonceStr, 143 | timestamp: timestamp, 144 | url: url 145 | }; 146 | var string = raw(ret); 147 | var shasum = crypto.createHash('sha1'); 148 | shasum.update(string); 149 | return shasum.digest('hex'); 150 | }; 151 | 152 | /** 153 | * 获取微信JS SDK Config的所需参数 154 | * 155 | * 注意事项 156 | * 157 | * 1. 签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。 158 | * 2. 签名用的url必须是调用JS接口页面的完整URL。 159 | * 3. 出于安全考虑,开发者必须在服务器端实现签名的逻辑。 160 | * Examples: 161 | * ``` 162 | * var param = { 163 | * debug:false, 164 | * jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage'], 165 | * url: 'http://www.xxx.com' 166 | * }; 167 | * api.getJsConfig(param, callback); 168 | * ``` 169 | * 170 | * Callback: 171 | * - `err`, 调用失败时得到的异常 172 | * - `result`, 调用正常时得到的js sdk config所需参数 173 | * 174 | * @param {Object} param 参数 175 | * @param {Function} callback 回调函数 176 | */ 177 | exports.getJsConfig = function (param, callback) { 178 | var that = this; 179 | that.getTicketToken(function (err, ticket) { 180 | if (err) { 181 | return callback(err); 182 | } 183 | var response = function (ticket) { 184 | var nonceStr = createNonceStr(); 185 | var jsAPITicket = ticket.ticket; 186 | var timestamp = createTimestamp(); 187 | var signature = sign(nonceStr, jsAPITicket, timestamp, param.url); 188 | var result = { 189 | debug: param.debug, 190 | appId: that.corpid, 191 | timestamp: timestamp, 192 | nonceStr: nonceStr, 193 | signature: signature, 194 | jsApiList: param.jsApiList 195 | }; 196 | callback(null, result); 197 | }; 198 | if (ticket && new Ticket(ticket.ticket, ticket.expireTime).isValid()) { 199 | response(ticket); 200 | } else { 201 | that.getTicket(function (err, ticket) { 202 | if (err) { 203 | return callback(err); 204 | } 205 | response(ticket); 206 | }); 207 | } 208 | }); 209 | }; 210 | -------------------------------------------------------------------------------- /lib/api_batch.js: -------------------------------------------------------------------------------- 1 | // 本文件用于实现异步批量任务接口 2 | // 所有的批量操作都是以上传的数据文件(CSV格式)作为基础的, 3 | // 所以在调用一下接口之前需要先调用uploadMedia把数据文件传到微信的服务器上。 4 | var util = require('./util'); 5 | var postJSON = util.postJSON; 6 | var wrapper = util.wrapper; 7 | 8 | /** 9 | * 批量邀请成员 10 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E6%8E%A5%E5%8F%A3#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 11 | * 12 | * Examples: 13 | * ``` 14 | * api.batchInviteUser(to, taskCb, callback); 15 | * ``` 16 | * To: 17 | * ``` 18 | * { 19 | * "touser":"xxx|xxx", 20 | * "toparty":"xxx|xxx", 21 | * "totag":"xxx|xxx", 22 | * "invite_tips":"xxx", 23 | * } 24 | * ``` 25 | * ``` 26 | * TaskCb: 27 | * ``` 28 | * { 29 | * "url": "xxx", 30 | * "token": "xxx", 31 | * "encodingaeskey": "xxx" 32 | * } 33 | * ``` 34 | * 35 | * Callback: 36 | * 37 | * - `err`, 调用失败时得到的异常 38 | * - `result`, 调用正常时得到的对象 39 | * 40 | * Result: 41 | * ``` 42 | * { 43 | * "errcode": 0, 44 | * "errmsg": "ok", 45 | * "jobid": "IoZW03y44Zcwuz-2K6T6rHTcf1uwyVbcYu2aRALKw-U" 46 | * } 47 | * ``` 48 | * @param {Object} to 批量邀请的数据结构 49 | * @param {Object} taskCb 任务执行完毕后的回调结构 50 | * @param {Function} callback 回调函数 51 | */ 52 | exports.batchInviteUser = function (to, taskCb, callback) { 53 | this.preRequest(this._batchInviteUser, arguments); 54 | }; 55 | 56 | exports._batchInviteUser = function (to, taskCb, callback) { 57 | var url = this.prefix + 'batch/inviteuser?access_token=' + this.token.accessToken; 58 | var data = to; 59 | if (taskCb) { 60 | data.callback = taskCb; 61 | } 62 | this.request(url, postJSON(data), wrapper(callback)); 63 | }; 64 | 65 | /** 66 | * 批量更新成员 67 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E6%8E%A5%E5%8F%A3#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 68 | * 69 | * Examples: 70 | * ``` 71 | * api.batchSyncUser(mediaId, taskCb, callback); 72 | * ``` 73 | * 74 | * ``` 75 | * TaskCb: 76 | * ``` 77 | * { 78 | * "url": "xxx", 79 | * "token": "xxx", 80 | * "encodingaeskey": "xxx" 81 | * } 82 | * ``` 83 | * 84 | * Callback: 85 | * 86 | * - `err`, 调用失败时得到的异常 87 | * - `result`, 调用正常时得到的对象 88 | * 89 | * Result: 90 | * ``` 91 | * { 92 | * "errcode": 0, 93 | * "errmsg": "ok", 94 | * "jobid": "IoZW03y44Zcwuz-2K6T6rHTcf1uwyVbcYu2aRALKw-U" 95 | * } 96 | * ``` 97 | * @param {String} mediaId 数据文件的mediaId 98 | * @param {Object} taskCb 任务执行完毕后的回调结构 99 | * @param {Function} callback 回调函数 100 | */ 101 | exports.batchSyncUser = function (mediaId, taskCb, callback) { 102 | this.preRequest(this._batchSyncUser, arguments); 103 | }; 104 | 105 | exports._batchSyncUser = function (mediaId, taskCb, callback) { 106 | var url = this.prefix + 'batch/syncuser?access_token=' + this.token.accessToken; 107 | var data = { 108 | media_id: mediaId 109 | }; 110 | 111 | if (taskCb) { 112 | data.callback = taskCb; 113 | } 114 | 115 | this.request(url, postJSON(data), wrapper(callback)); 116 | }; 117 | 118 | /** 119 | * 全量覆盖成员 120 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E6%8E%A5%E5%8F%A3#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 121 | * 122 | * Examples: 123 | * ``` 124 | * api.batchReplaceUser(mediaId, taskCb, callback); 125 | * ``` 126 | * 127 | * ``` 128 | * TaskCb: 129 | * ``` 130 | * { 131 | * "url": "xxx", 132 | * "token": "xxx", 133 | * "encodingaeskey": "xxx" 134 | * } 135 | * ``` 136 | * 137 | * Callback: 138 | * 139 | * - `err`, 调用失败时得到的异常 140 | * - `result`, 调用正常时得到的对象 141 | * 142 | * Result: 143 | * ``` 144 | * { 145 | * "errcode": 0, 146 | * "errmsg": "ok", 147 | * "jobid": "IoZW03y44Zcwuz-2K6T6rHTcf1uwyVbcYu2aRALKw-U" 148 | * } 149 | * ``` 150 | * @param {String} mediaId 数据文件的mediaId 151 | * @param {Object} taskCb 任务执行完毕后的回调结构 152 | * @param {Function} callback 回调函数 153 | */ 154 | exports.batchReplaceUser = function (mediaId, taskCb, callback) { 155 | this.preRequest(this._batchReplaceUser, arguments); 156 | }; 157 | 158 | exports._batchReplaceUser = function (mediaId, taskCb, callback) { 159 | var url = this.prefix + 'batch/replaceuser?access_token=' + this.token.accessToken; 160 | var data = { 161 | media_id: mediaId 162 | }; 163 | 164 | if (taskCb) { 165 | data.callback = taskCb; 166 | } 167 | 168 | this.request(url, postJSON(data), wrapper(callback)); 169 | }; 170 | 171 | /** 172 | * 全量覆盖部门 173 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E6%8E%A5%E5%8F%A3#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 174 | * 175 | * Examples: 176 | * ``` 177 | * api.batchReplaceParty(mediaId, taskCb, callback); 178 | * ``` 179 | * 180 | * ``` 181 | * TaskCb: 182 | * ``` 183 | * { 184 | * "url": "xxx", 185 | * "token": "xxx", 186 | * "encodingaeskey": "xxx" 187 | * } 188 | * ``` 189 | * 190 | * Callback: 191 | * 192 | * - `err`, 调用失败时得到的异常 193 | * - `result`, 调用正常时得到的对象 194 | * 195 | * Result: 196 | * ``` 197 | * { 198 | * "errcode": 0, 199 | * "errmsg": "ok", 200 | * "jobid": "IoZW03y44Zcwuz-2K6T6rHTcf1uwyVbcYu2aRALKw-U" 201 | * } 202 | * ``` 203 | * @param {Object} mediaId 数据文件的mediaId 204 | * @param {Object} taskCb 任务执行完毕后的回调结构 205 | * @param {Function} callback 回调函数 206 | */ 207 | exports.batchReplaceParty = function (mediaId, taskCb, callback) { 208 | this.preRequest(this._batchReplaceParty, arguments); 209 | }; 210 | 211 | exports._batchReplaceParty = function (mediaId, taskCb, callback) { 212 | var url = this.prefix + 'batch/replaceparty?access_token=' + this.token.accessToken; 213 | var data = { 214 | media_id: mediaId 215 | }; 216 | 217 | if (taskCb) { 218 | data.callback = taskCb; 219 | } 220 | 221 | this.request(url, postJSON(data), wrapper(callback)); 222 | }; 223 | 224 | /** 225 | * 获取批量任务的结果 226 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E6%8E%A5%E5%8F%A3#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 227 | * 228 | * Examples: 229 | * ``` 230 | * api.batchGetResult(jobid, callback); 231 | * ``` 232 | * 233 | * Callback: 234 | * 235 | * - `err`, 调用失败时得到的异常 236 | * - `result`, 调用正常时得到的对象 237 | * 238 | * 返回结果参考微信的官方文档 239 | * @param {String} jobid 启动批量任务时返回的任务id 240 | * @param {Function} callback 回调函数 241 | */ 242 | exports.batchGetResult = function (jobid, callback) { 243 | this.preRequest(this._batchGetResult, arguments); 244 | }; 245 | 246 | exports._batchGetResult = function(jobid, callback) { 247 | var url = this.prefix + 'batch/getresult?access_token=' + this.token.accessToken + '&jobid=' + jobid; 248 | this.request(url, wrapper(callback)); 249 | }; 250 | -------------------------------------------------------------------------------- /lib/api_common.js: -------------------------------------------------------------------------------- 1 | // 本文件用于wechat API,基础文件,主要用于Token的处理和mixin机制 2 | 3 | var urllib = require('urllib'); 4 | var util = require('./util'); 5 | var extend = require('util')._extend; 6 | var wrapper = util.wrapper; 7 | 8 | var AccessToken = function (accessToken) { 9 | if (!(this instanceof AccessToken)) { 10 | return new AccessToken(accessToken); 11 | } 12 | this.accessToken = accessToken; 13 | }; 14 | 15 | /** 16 | * 根据appid和appsecret创建API的构造函数。 17 | * 18 | * 如需跨进程跨机器进行操作Wechat API(依赖access token),access token需要进行全局维护 19 | * 使用策略如下: 20 | * 21 | * 1. 调用用户传入的获取token的异步方法,获得token之后使用 22 | * 2. 使用appid/appsecret获取token。并调用用户传入的保存token方法保存 23 | * 24 | * Examples: 25 | * ``` 26 | * var API = require('wechat-enterprise-api'); 27 | * var api = new API('appid', 'secret', 'agentid'); 28 | * ``` 29 | * 以上即可满足单进程使用。 30 | * 当多进程时,token需要全局维护,以下为保存token的接口。 31 | * ``` 32 | * var api = new API('corpid', 'secret', 'agentid', function (callback) { 33 | * // 传入一个获取全局token的方法 34 | * fs.readFile('access_token.txt', 'utf8', function (err, txt) { 35 | * if (err) {return callback(err);} 36 | * callback(null, JSON.parse(txt)); 37 | * }); 38 | * }, function (token, callback) { 39 | * // 请将token存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等 40 | * // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例 41 | * fs.writeFile('access_token.txt', JSON.stringify(token), callback); 42 | * }); 43 | * ``` 44 | * @param {String} corpid 在公众平台上申请得到的corpid 45 | * @param {String} corpsecret 在公众平台上申请得到的app secret 46 | * @param {String} agentid 企业应用的id 47 | * @param {Function} getToken 可选的。获取全局token对象的方法,多进程模式部署时需在意 48 | * @param {Function} saveToken 可选的。保存全局token对象的方法,多进程模式部署时需在意 49 | */ 50 | var API = function (corpid, corpsecret, agentid, getToken, saveToken) { 51 | this.corpid = corpid; 52 | this.corpsecret = corpsecret; 53 | this.agentid = agentid; 54 | this.store = null; 55 | this.getToken = getToken || function (callback) { 56 | callback(null, this.store); 57 | }; 58 | this.saveToken = saveToken || function (token, callback) { 59 | this.store = token; 60 | if (process.env.NODE_ENV === 'production') { 61 | console.warn('Don\'t save token in memory, when cluster or multi-computer!'); 62 | } 63 | callback(null); 64 | }; 65 | this.prefix = 'https://qyapi.weixin.qq.com/cgi-bin/'; 66 | this.defaults = {}; 67 | this.registerTicketHandle(); 68 | }; 69 | 70 | /** 71 | * 用于设置urllib的默认options 72 | * 73 | * Examples: 74 | * ``` 75 | * api.setOpts({timeout: 15000}); 76 | * ``` 77 | * @param {Object} opts 默认选项 78 | */ 79 | API.prototype.setOpts = function (opts) { 80 | this.defaults = opts; 81 | }; 82 | 83 | /** 84 | * 设置urllib的options 85 | * 86 | */ 87 | API.prototype.request = function (url, opts, callback) { 88 | var options = {}; 89 | extend(options, this.defaults); 90 | if (typeof opts === 'function') { 91 | callback = opts; 92 | opts = {}; 93 | } 94 | for (var key in opts) { 95 | if (key !== 'headers') { 96 | options[key] = opts[key]; 97 | } else { 98 | if (opts.headers) { 99 | options.headers = options.headers || {}; 100 | extend(options.headers, opts.headers); 101 | } 102 | } 103 | } 104 | urllib.request(url, options, callback); 105 | }; 106 | 107 | 108 | /*! 109 | * 根据创建API时传入的appid和appsecret获取access token 110 | * 进行后续所有API调用时,需要先获取access token 111 | * 详细请看: 112 | * 113 | * 应用开发者无需直接调用本API。 114 | * 115 | * Examples: 116 | * ``` 117 | * api.getAccessToken(callback); 118 | * ``` 119 | * Callback: 120 | * 121 | * - `err`, 获取access token出现异常时的异常对象 122 | * - `result`, 成功时得到的响应结果 123 | * 124 | * Result: 125 | * ``` 126 | * {"access_token": "ACCESS_TOKEN"} 127 | * ``` 128 | * @param {Function} callback 回调函数 129 | */ 130 | API.prototype.getAccessToken = function (callback) { 131 | var that = this; 132 | var url = this.prefix + 'gettoken?corpid=' + this.corpid + '&corpsecret=' + this.corpsecret; 133 | this.request(url, {dataType: 'json'}, wrapper(function (err, data) { 134 | if (err) { 135 | return callback(err); 136 | } 137 | var token = AccessToken(data.access_token); 138 | that.saveToken(token, function (err) { 139 | if (err) { 140 | return callback(err); 141 | } 142 | callback(err, token); 143 | }); 144 | })); 145 | return this; 146 | }; 147 | 148 | /*! 149 | * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用。 150 | * 无需依赖getAccessToken为前置调用。 151 | * 应用开发者无需直接调用此API。 152 | * 153 | * Examples: 154 | * ``` 155 | * api.preRequest(method, arguments); 156 | * ``` 157 | * @param {Function} method 需要封装的方法 158 | * @param {Array} args 方法需要的参数 159 | */ 160 | API.prototype.preRequest = function (method, args, retryed) { 161 | var that = this; 162 | var callback = args[args.length - 1]; 163 | // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。 164 | that.getToken(function (err, token) { 165 | if (err) { 166 | return callback(err); 167 | } 168 | // 有token并且token有效直接调用 169 | if (token) { 170 | // 暂时保存token 171 | that.token = AccessToken(token.accessToken); 172 | if (!retryed) { 173 | var retryHandle = function (err, data, res) { 174 | // 42001 重试 175 | if (data && data.errcode && data.errcode === 42001) { 176 | return that.preRequest(method, args, true); 177 | } 178 | callback(err, data, res); 179 | }; 180 | // 替换callback 181 | var newargs = Array.prototype.slice.call(args, 0, -1); 182 | newargs.push(retryHandle); 183 | method.apply(that, newargs); 184 | } else { 185 | method.apply(that, args); 186 | } 187 | } else { 188 | // 使用appid/appsecret获取token 189 | that.getAccessToken(function (err, token) { 190 | // 如遇错误,通过回调函数传出 191 | if (err) { 192 | return callback(err); 193 | } 194 | // 暂时保存token 195 | that.token = token; 196 | method.apply(that, args); 197 | }); 198 | } 199 | }); 200 | }; 201 | 202 | /** 203 | * 获取最新的token。 204 | * 205 | * - 如果还没有请求过token,则发起获取Token请求。 206 | * - 如果请求过,则调用getToken从获取之前保存的token 207 | * 208 | * Examples: 209 | * ``` 210 | * api.getLatestToken(callback); 211 | * ``` 212 | * Callback: 213 | * 214 | * - `err`, 获取access token出现异常时的异常对象 215 | * - `token`, 获取的token 216 | * 217 | * @param {Function} callback 回调函数 218 | */ 219 | API.prototype.getLatestToken = function (callback) { 220 | var that = this; 221 | // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。 222 | that.getToken(function (err, token) { 223 | if (err) { 224 | return callback(err); 225 | } 226 | // 有token 227 | if (token) { 228 | callback(null, AccessToken(token.accessToken)); 229 | } else { 230 | // 使用corpid/corpsecret获取token 231 | that.getAccessToken(callback); 232 | } 233 | }); 234 | }; 235 | 236 | /** 237 | * 用于支持对象合并。将对象合并到API.prototype上,使得能够支持扩展 238 | * Examples: 239 | * ``` 240 | * // 媒体管理(上传、下载) 241 | * API.mixin(require('./lib/api_media')); 242 | * ``` 243 | * @param {Object} obj 要合并的对象 244 | */ 245 | API.mixin = function (obj) { 246 | for (var key in obj) { 247 | if (API.prototype.hasOwnProperty(key)) { 248 | throw new Error('Don\'t allow override existed prototype method. method: '+ key); 249 | } 250 | API.prototype[key] = obj[key]; 251 | } 252 | }; 253 | 254 | API.AccessToken = AccessToken; 255 | 256 | module.exports = API; 257 | -------------------------------------------------------------------------------- /lib/api_tag.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | var postJSON = util.postJSON; 3 | var wrapper = util.wrapper; 4 | 5 | /** 6 | * 创建标签 7 | * 8 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=管理标签 9 | * Examples: 10 | * ``` 11 | * api.createTag(name, callback); 12 | * api.createTag(name, tagid, callback); 13 | * api.createTag(name, { tagid: theId }, callback); 14 | * ``` 15 | * 16 | * Callback: 17 | * 18 | * - `err`, 调用失败时得到的异常 19 | * - `result`, 调用正常时得到的对象 20 | * 21 | * Result: 22 | * ``` 23 | * { 24 | * "errcode": 0, 25 | * "errmsg": "created", 26 | * "tagid": "1" 27 | * } 28 | * ``` 29 | * @param {String} name 标签名字 30 | * @param {String|Number|Object} opts 标签的额外参数(目前就tagid) 31 | * @param {Function} callback 回调函数 32 | */ 33 | exports.createTag = function (name, opts, callback) { 34 | this.preRequest(this._createTag, arguments); 35 | }; 36 | 37 | /*! 38 | * 创建标签的未封装版本 39 | */ 40 | exports._createTag = function (name, opts, callback) { 41 | // https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=ACCESS_TOKEN 42 | var url = this.prefix + 'tag/create?access_token=' + this.token.accessToken; 43 | var data = { 44 | tagname: name 45 | }; 46 | if (typeof opts === 'function') { 47 | callback = opts; 48 | } else { 49 | if (typeof opts === 'object') { 50 | var id = opts.tagid || opts.id; 51 | if (id) { 52 | data.tagid = Number(id); 53 | } 54 | } else { 55 | data.tagid = Number(opts); 56 | } 57 | } 58 | this.request(url, postJSON(data), wrapper(callback)); 59 | }; 60 | 61 | /** 62 | * 更新标签名字 63 | * 64 | * Examples: 65 | * ``` 66 | * api.updateTagName(id, name, callback); 67 | * ``` 68 | * 69 | * Callback: 70 | * 71 | * - `err`, 调用失败时得到的异常 72 | * - `result`, 调用正常时得到的对象 73 | * 74 | * Result: 75 | * ``` 76 | * { 77 | * "errcode": 0, 78 | * "errmsg": "updated" 79 | * } 80 | * ``` 81 | * @param {String} id 标签ID 82 | * @param {String} name 标签名称。最长64个字符 83 | * @param {Function} callback 回调函数 84 | */ 85 | exports.updateTagName = function (id, name, callback) { 86 | this.preRequest(this._updateTagName, arguments); 87 | }; 88 | 89 | /*! 90 | * 更新标签名字的未封装版本 91 | */ 92 | exports._updateTagName = function (id, name, callback) { 93 | // https://qyapi.weixin.qq.com/cgi-bin/tag/update?access_token=ACCESS_TOKEN 94 | var url = this.prefix + 'tag/update?access_token=' + this.token.accessToken; 95 | var data = { 96 | tagid: id, 97 | tagname: name 98 | }; 99 | this.request(url, postJSON(data), wrapper(callback)); 100 | }; 101 | 102 | /** 103 | * 删除标签 104 | * 105 | * Examples: 106 | * ``` 107 | * api.deleteTag(id, callback); 108 | * ``` 109 | * 110 | * Callback: 111 | * 112 | * - `err`, 调用失败时得到的异常 113 | * - `result`, 调用正常时得到的对象 114 | * 115 | * Result: 116 | * ``` 117 | * { 118 | * "errcode": 0, 119 | * "errmsg": "deleted" 120 | * } 121 | * ``` 122 | * @param {Number} id 标签ID 123 | * @param {Function} callback 回调函数 124 | */ 125 | exports.deleteTag = function (id, callback) { 126 | this.preRequest(this._deleteTag, arguments); 127 | }; 128 | 129 | /*! 130 | * 删除标签的未封装版本 131 | */ 132 | exports._deleteTag = function (id, callback) { 133 | // https://qyapi.weixin.qq.com/cgi-bin/tag/delete?access_token=ACCESS_TOKEN&tagid=1 134 | var url = this.prefix + 'tag/delete?access_token=' + this.token.accessToken; 135 | var opts = { 136 | dataType: 'json', 137 | data: { 138 | tagid: id 139 | } 140 | }; 141 | this.request(url, opts, wrapper(callback)); 142 | }; 143 | 144 | /** 145 | * 获取标签列表 146 | * 147 | * Examples: 148 | * ``` 149 | * api.listTags(callback); 150 | * ``` 151 | * 152 | * Callback: 153 | * 154 | * - `err`, 调用失败时得到的异常 155 | * - `result`, 调用正常时得到的对象 156 | * 157 | * Result: 158 | * ``` 159 | * { 160 | * "errcode": 0, 161 | * "errmsg": "deleted" 162 | * } 163 | * ``` 164 | * @param {Function} callback 回调函数 165 | */ 166 | exports.listTags = function (callback) { 167 | this.preRequest(this._listTags, arguments); 168 | }; 169 | 170 | /*! 171 | * 获取标签列表的未封装版本 172 | */ 173 | exports._listTags = function (callback) { 174 | var url = this.prefix + 'tag/list?access_token=' + this.token.accessToken; 175 | var opts = { 176 | dataType: 'json', 177 | data: {} 178 | }; 179 | this.request(url, opts, wrapper(callback)); 180 | }; 181 | /** 182 | * 获取标签成员 183 | * 184 | * Examples: 185 | * ``` 186 | * api.getTagUsers(id, callback); 187 | * ``` 188 | * 189 | * Callback: 190 | * 191 | * - `err`, 调用失败时得到的异常 192 | * - `result`, 调用正常时得到的对象 193 | * 194 | * Result: 195 | * ``` 196 | * { 197 | * "errcode": 0, 198 | * "errmsg": "ok", 199 | * "userlist": [ 200 | * { 201 | * "userid": "zhangsan", 202 | * "name": "李四" 203 | * } 204 | * ] 205 | * } 206 | * ``` 207 | * @param {Number} id 标签ID 208 | * @param {Function} callback 回调函数 209 | */ 210 | exports.getTagUsers = function (id, callback) { 211 | this.preRequest(this._getTagUsers, arguments); 212 | }; 213 | 214 | /*! 215 | * 获取标签成员的未封装版本 216 | */ 217 | exports._getTagUsers = function (id, callback) { 218 | // https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token=ACCESS_TOKEN&tagid=1 219 | var url = this.prefix + 'tag/get?access_token=' + this.token.accessToken; 220 | var opts = { 221 | dataType: 'json', 222 | data: { 223 | tagid: id 224 | } 225 | }; 226 | this.request(url, opts, wrapper(callback)); 227 | }; 228 | 229 | /** 230 | * 增加标签成员 231 | * 232 | * Examples: 233 | * ``` 234 | * var userIdList = ['id1', 'id2']; 235 | * api.addTagUsers(id, userIdList, callback); 236 | * ``` 237 | * 238 | * Callback: 239 | * 240 | * - `err`, 调用失败时得到的异常 241 | * - `result`, 调用正常时得到的对象 242 | * 243 | * Result: 244 | * a)正确时返回 245 | * ``` 246 | * { 247 | * "errcode": 0, 248 | * "errmsg": "deleted" 249 | * } 250 | * ``` 251 | * b)若部分userid非法,则返回 252 | * ``` 253 | * { 254 | * "errcode": 0, 255 | * "errmsg": "invalid userlist failed" 256 | * "invalidlist":"usr1|usr2|usr" 257 | * } 258 | * ``` 259 | * c)当包含的userid全部非法时返回 260 | * ``` 261 | * { 262 | * "errcode": 40031, 263 | * "errmsg": "all list invalid" 264 | * } 265 | * ``` 266 | * @param {Number} id 标签ID 267 | * @param {Array} userIdList 用户ID列表 268 | * @param {Function} callback 回调函数 269 | */ 270 | exports.addTagUsers = function (id, userIdList, callback) { 271 | this.preRequest(this._addTagUsers, arguments); 272 | }; 273 | 274 | /*! 275 | * 增加标签成员的未封装版本 276 | */ 277 | exports._addTagUsers = function (id, userIdList, callback) { 278 | // https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN 279 | var url = this.prefix + 'tag/addtagusers?access_token=' + this.token.accessToken; 280 | var data = { 281 | tagid: id, 282 | userlist: userIdList 283 | }; 284 | this.request(url, postJSON(data), wrapper(callback)); 285 | }; 286 | 287 | /** 288 | * 删除标签成员 289 | * 290 | * Examples: 291 | * ``` 292 | * var userIdList = ['id1', 'id2']; 293 | * api.deleteTagUsers(id, userIdList, callback); 294 | * ``` 295 | * 296 | * Callback: 297 | * 298 | * - `err`, 调用失败时得到的异常 299 | * - `result`, 调用正常时得到的对象 300 | * 301 | * Result: 302 | * a)正确时返回 303 | * ``` 304 | * { 305 | * "errcode": 0, 306 | * "errmsg": "deleted" 307 | * } 308 | * ``` 309 | * b)若部分userid非法,则返回 310 | * ``` 311 | * { 312 | * "errcode": 0, 313 | * "errmsg": "invalid userlist failed" 314 | * "invalidlist":"usr1|usr2|usr" 315 | * } 316 | * ``` 317 | * c)当包含的userid全部非法时返回 318 | * ``` 319 | * { 320 | * "errcode": 40031, 321 | * "errmsg": "all list invalid" 322 | * } 323 | * ``` 324 | * @param {Number} id 标签ID 325 | * @param {Array} userIdList 用户ID数组 326 | * @param {Function} callback 回调函数 327 | */ 328 | exports.deleteTagUsers = function (id, userIdList, callback) { 329 | this.preRequest(this._deleteTagUsers, arguments); 330 | }; 331 | 332 | /*! 333 | * 删除标签成员的未封装版本 334 | */ 335 | exports._deleteTagUsers = function (id, userIdList, callback) { 336 | // https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN 337 | var url = this.prefix + 'tag/deltagusers?access_token=' + this.token.accessToken; 338 | var data = { 339 | tagid: id, 340 | userlist: userIdList 341 | }; 342 | this.request(url, postJSON(data), wrapper(callback)); 343 | }; 344 | -------------------------------------------------------------------------------- /lib/api_material.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var formstream = require('formstream'); 4 | var util = require('./util'); 5 | var wrapper = util.wrapper; 6 | 7 | /** 8 | * 上传永久素材,分别有图片(image)、语音(voice)、视频(video)和普通文件(file) 9 | * 详情请见: 10 | * Examples: 11 | * ``` 12 | * api.addMaterial(agentid, 'filepath', type, callback); 13 | * ``` 14 | * Callback: 15 | * 16 | * - `err`, 调用失败时得到的异常 17 | * - `result`, 调用正常时得到的对象 18 | * 19 | * Result: 20 | * ``` 21 | * {"errcode":0,"errmsg":"ok","media_id": "2-G6nrLmr5EFSDC3MMfasdfb_-zK1dDdzmd0p7"} 22 | * ``` 23 | * Shortcut: 24 | * 25 | * - `exports.addImage(agentid, filepath, callback);` 26 | * - `exports.addVoice(agentid, filepath, callback);` 27 | * - `exports.addVideo(agentid, filepath, callback);` 28 | * - `exports.addFile(agentid, filepath, callback);` 29 | * 30 | * @param {String} agentid 应用的id 31 | * @param {String} filepath 文件路径 32 | * @param {String} type 媒体类型,可用值有image、voice、video、file 33 | * @param {Function} callback 回调函数 34 | */ 35 | exports.addMaterial = function (agentid, filepath, type, callback) { 36 | this.preRequest(this._addMaterial, arguments); 37 | }; 38 | 39 | /*! 40 | * 上传永久素材的未封装版本 41 | */ 42 | exports._addMaterial = function (agentid, filepath, type, callback) { 43 | var that = this; 44 | fs.stat(filepath, function (err, stat) { 45 | if (err) { 46 | return callback(err); 47 | } 48 | var form = formstream(); 49 | form.file('media', filepath, path.basename(filepath), stat.size); 50 | var url = that.prefix + 'material/add_material?access_token=' + that.token.accessToken + '&type=' + type + '&agentid=' + agentid; 51 | var opts = { 52 | dataType: 'json', 53 | type: 'POST', 54 | timeout: 60000, // 60秒超时 55 | headers: form.headers(), 56 | stream: form 57 | }; 58 | that.request(url, opts, wrapper(callback)); 59 | }); 60 | }; 61 | 62 | ['image', 'voice', 'video', 'file'].forEach(function (type) { 63 | var method = 'add' + type[0].toUpperCase() + type.substring(1); 64 | exports[method] = function (agentid, filepath, callback) { 65 | this.addMaterial(agentid, filepath, type, callback); 66 | }; 67 | }); 68 | 69 | /** 70 | * 上传永久图文素材 71 | * 详情请见: 72 | * Examples: 73 | * ``` 74 | * api.addMPNews(agentid, mpnews, callback); 75 | * ``` 76 | * Callback: 77 | * 78 | * - `err`, 调用失败时得到的异常 79 | * - `result`, 调用正常时得到的对象 80 | * 81 | * Result: 82 | * ``` 83 | * {"errcode": "0","errmsg": "ok","media_id": "2-G6nrLmr5EC3MMfasdfb_-zK1dDdzmd0p7"} 84 | * ``` 85 | * 86 | * @param {String} agentid 应用的id 87 | * @param {String} mpnews 图文消息的结构 88 | * @param {Function} callback 回调函数 89 | */ 90 | exports.addMPNews = function (agentid, mpnews, callback) { 91 | this.preRequest(this._addMPNews, arguments); 92 | }; 93 | 94 | /*! 95 | * 上传永久图文素材的未封装版本 96 | */ 97 | exports._addMPNews = function (agentid, mpnews, callback) { 98 | var that = this; 99 | var post_data = { 100 | agentid: agentid, 101 | mpnews: mpnews 102 | }; 103 | var url = that.prefix + 'material/add_mpnews?access_token=' + that.token.accessToken; 104 | this.request(url, postJSON(post_data), wrapper(callback)); 105 | }; 106 | 107 | /** 108 | * 更新永久图文素材 109 | * 详情请见: 110 | * Examples: 111 | * ``` 112 | * api.updateMPNews(agentid, media_id, mpnews, callback); 113 | * ``` 114 | * Callback: 115 | * 116 | * - `err`, 调用失败时得到的异常 117 | * - `result`, 调用正常时得到的对象 118 | * 119 | * Result: 120 | * ``` 121 | * {"errcode": "0","errmsg": "ok"} 122 | * ``` 123 | * 124 | * @param {String} agentid 应用的id 125 | * @param {String} media_id 素材id 126 | * @param {String} mpnews 图文消息的结构 127 | * @param {Function} callback 回调函数 128 | */ 129 | exports.updateMPNews = function (agentid, media_id, mpnews, callback) { 130 | this.preRequest(this._updateMPNews, arguments); 131 | }; 132 | 133 | /*! 134 | * 更新永久图文素材的未封装版本 135 | */ 136 | exports._updateMPNews = function (agentid, media_id, mpnews, callback) { 137 | var that = this; 138 | var post_data = { 139 | agentid: agentid, 140 | media_id: media_id, 141 | mpnews: mpnews 142 | }; 143 | var url = that.prefix + 'material/update_mpnews?access_token=' + that.token.accessToken; 144 | this.request(url, postJSON(post_data), wrapper(callback)); 145 | }; 146 | 147 | /** 148 | * 获取永久素材 149 | * 详情请见: 150 | * Examples: 151 | * ``` 152 | * api.getMaterial(agentid, 'media_id', callback); 153 | * ``` 154 | * Callback: 155 | * 156 | * - `err`, 调用失败时得到的异常 157 | * - `result`, 调用正常时得到的文件Buffer对象 158 | * - `res`, HTTP响应对象 159 | * 160 | * @param {String} agentid 应用的id 161 | * @param {String} mediaId 媒体文件的ID 162 | * @param {Function} callback 回调函数 163 | */ 164 | exports.getMaterial = function (agentid, mediaId, callback) { 165 | this.preRequest(this._getMaterial, arguments); 166 | }; 167 | 168 | /*! 169 | * 获取永久素材的未封装版本 170 | */ 171 | exports._getMaterial = function (agentid, mediaId, callback) { 172 | var url = this.prefix + 'material/get?access_token=' + this.token.accessToken + '&media_id=' + mediaId + '&agentid=' + agentid; 173 | this.request(url, {}, wrapper(function (err, data, res) { 174 | // handle some err 175 | if (err) { 176 | return callback(err); 177 | } 178 | var contentType = res.headers['content-type']; 179 | if (contentType === 'application/json') { 180 | var ret; 181 | try { 182 | ret = JSON.parse(data); 183 | if (ret.errcode) { 184 | err = new Error(ret.errmsg); 185 | err.name = 'WeChatAPIError'; 186 | } 187 | } catch (ex) { 188 | callback(ex, data, res); 189 | return; 190 | } 191 | callback(err, ret, res); 192 | } else { 193 | // 输出Buffer对象 194 | callback(null, data, res); 195 | } 196 | })); 197 | }; 198 | 199 | /** 200 | * 删除永久素材 201 | * 详情请见: 202 | * Examples: 203 | * ``` 204 | * api.delMaterial(agentid, 'media_id', callback); 205 | * ``` 206 | * Callback: 207 | * 208 | * - `err`, 调用失败时得到的异常 209 | * - `result`, 调用正常时得到的文件Buffer对象 210 | * - `res`, HTTP响应对象 211 | * 212 | * Result: 213 | * ``` 214 | * {"errcode": "0","errmsg": "deleted"} 215 | * ``` 216 | * @param {String} agentid 应用的id 217 | * @param {String} mediaId 媒体文件的ID 218 | * @param {Function} callback 回调函数 219 | */ 220 | exports.delMaterial = function (agentid, mediaId, callback) { 221 | this.preRequest(this._delMaterial, arguments); 222 | }; 223 | 224 | /*! 225 | * 删除永久素材的未封装版本 226 | */ 227 | exports._delMaterial = function (agentid, mediaId, callback) { 228 | var url = this.prefix + 'material/del?access_token=' + this.token.accessToken + '&media_id=' + mediaId + '&agentid=' + agentid; 229 | this.request(url, {}, wrapper(callback)); 230 | }; 231 | 232 | /** 233 | * 获取素材总数 234 | * 详情请见: 235 | * Examples: 236 | * ``` 237 | * api.countMaterial(agentid, callback); 238 | * ``` 239 | * Callback: 240 | * 241 | * - `err`, 调用失败时得到的异常 242 | * - `result`, 调用正常时得到的文件Buffer对象 243 | * - `res`, HTTP响应对象 244 | * 245 | * Result: 246 | * ``` 247 | * {"errcode": "0","errmsg": "ok", "total_count": 37,"image_count": 12, "voice_count": 10, "video_count": 3, "file_count": 3, "mpnews_count": 6} 248 | * ``` 249 | * @param {String} agentid 应用的id 250 | * @param {String} mediaId 媒体文件的ID 251 | * @param {Function} callback 回调函数 252 | */ 253 | exports.countMaterial = function (agentid, mediaId, callback) { 254 | this.preRequest(this._countMaterial, arguments); 255 | }; 256 | 257 | /*! 258 | * 获取素材总数的未封装版本 259 | */ 260 | exports._countMaterial = function (agentid, mediaId, callback) { 261 | var url = this.prefix + 'material/get_count?access_token=' + this.token.accessToken + '&agentid=' + agentid; 262 | this.request(url, {}, wrapper(callback)); 263 | }; 264 | 265 | /** 266 | * 获取素材列表 267 | * 详情请见: 268 | * type 可为图片(image)、语音(voice)、视频(video)、普通文件(file)、图文消息(mpnews) 269 | * Examples: 270 | * ``` 271 | * api.batchgetMaterial(agentid, type, offset, count, callback); 272 | * ``` 273 | * Callback: 274 | * 275 | * - `err`, 调用失败时得到的异常 276 | * - `result`, 调用正常时得到的文件Buffer对象 277 | * - `res`, HTTP响应对象 278 | * 279 | * Result: 280 | * ``` 281 | * {"errcode": "0","errmsg": "ok", "total_count": 37,"image_count": 12, "voice_count": 10, "video_count": 3, "file_count": 3, "mpnews_count": 6} 282 | * ``` 283 | * @param {String} agentid 应用的id 284 | * @param {String} type 媒体类型 可以为图文(mpnews)、图片(image)、音频(voice)、视频(video)、文件(file) 285 | * @param {String} offset 从该类型素材的该偏移位置开始返回,0表示从第一个素材返回 286 | * @param {String} count 返回素材的数量,取值在1到50之间 287 | * @param {Function} callback 回调函数 288 | */ 289 | exports.batchgetMaterial = function (agentid, type, offset, count, callback) { 290 | this.preRequest(this._batchgetMaterial, arguments); 291 | }; 292 | 293 | /*! 294 | * 获取素材列表的未封装版本 295 | */ 296 | exports._batchgetMaterial = function (agentid, type, offset, count, callback) { 297 | var url = this.prefix + 'material/batchget?access_token=' + this.token.accessToken; 298 | var post_data = { 299 | "type": type, 300 | "agentid": agentid, 301 | "offset": parseInt(offset || 0), 302 | "count": parseInt(count || 10) 303 | } 304 | this.request(url, postJSON(post_data), wrapper(callback)); 305 | }; 306 | 307 | 308 | -------------------------------------------------------------------------------- /lib/api_user.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | var util = require('./util'); 3 | var wrapper = util.wrapper; 4 | var postJSON = util.postJSON; 5 | var make = util.make; 6 | 7 | /** 8 | * 创建成员 9 | * 详细请看:http://qydev.weixin.qq.com/wiki/index.php?title=管理成员 10 | * 11 | * Examples: 12 | * ``` 13 | * api.createUser(user, callback); 14 | * ``` 15 | * User: 16 | * ``` 17 | * { 18 | * "userid": "zhangsan", 19 | * "name": "张三", 20 | * "department": [1, 2], 21 | * "position": "产品经理", 22 | * "mobile": "15913215421", 23 | * "gender": 1, 24 | * "tel": "62394", 25 | * "email": "zhangsan@gzdev.com", 26 | * "weixinid": "zhangsan4dev" 27 | * } 28 | * ``` 29 | * 30 | * Callback: 31 | * 32 | * - `err`, 调用失败时得到的异常 33 | * - `result`, 调用正常时得到的对象 34 | * 35 | * Result: 36 | * ``` 37 | * { 38 | * "errcode": 0, 39 | * "errmsg": "created" 40 | * } 41 | * ``` 42 | * @param {Object} user 成员信息 43 | * @param {Function} callback 回调函数 44 | */ 45 | exports.createUser = function (user, callback) { 46 | this.preRequest(this._createUser, arguments); 47 | }; 48 | 49 | /*! 50 | * 创建成员的未封装版本 51 | */ 52 | exports._createUser = function (user, callback) { 53 | // https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=ACCESS_TOKEN 54 | var url = this.prefix + 'user/create?access_token=' + this.token.accessToken; 55 | this.request(url, postJSON(user), wrapper(callback)); 56 | }; 57 | 58 | /** 59 | * 更新成员 60 | * 61 | * Examples: 62 | * ``` 63 | * api.updateUser(user, callback); 64 | * ``` 65 | * User: 66 | * ``` 67 | * { 68 | * "userid": "zhangsan", 69 | * "name": "李四", 70 | * "department": [1], 71 | * "position": "后台工程师", 72 | * "mobile": "15913215421", 73 | * "gender": 1, 74 | * "tel": "62394", 75 | * "email": "zhangsan@gzdev.com", 76 | * "weixinid": "lisifordev", 77 | * "enable": 1 78 | * } 79 | * ``` 80 | * Callback: 81 | * 82 | * - `err`, 调用失败时得到的异常 83 | * - `result`, 调用正常时得到的对象 84 | * 85 | * Result: 86 | * ``` 87 | * { 88 | * "errcode": 0, 89 | * "errmsg": "updated" 90 | * } 91 | * ``` 92 | * @param {Object} user 成员信息 93 | * @param {Function} callback 回调函数 94 | */ 95 | exports.updateUser = function (user, callback) { 96 | this.preRequest(this._updateUser, arguments); 97 | }; 98 | 99 | /*! 100 | * 更新成员的未封装版本 101 | */ 102 | exports._updateUser = function (user, callback) { 103 | // https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=ACCESS_TOKEN 104 | var url = this.prefix + 'user/update?access_token=' + this.token.accessToken; 105 | this.request(url, postJSON(user), wrapper(callback)); 106 | }; 107 | 108 | /** 109 | * 删除成员 110 | * 111 | * Examples: 112 | * ``` 113 | * api.deleteUser(id, callback); 114 | * ``` 115 | * 116 | * Callback: 117 | * 118 | * - `err`, 调用失败时得到的异常 119 | * - `result`, 调用正常时得到的对象 120 | * 121 | * Result: 122 | * ``` 123 | * { 124 | * "errcode": 0, 125 | * "errmsg": "deleted" 126 | * } 127 | * ``` 128 | * @param {Number} id 成员ID 129 | * @param {Function} callback 回调函数 130 | */ 131 | exports.deleteUser = function (id, callback) { 132 | this.preRequest(this._deleteUser, arguments); 133 | }; 134 | 135 | /*! 136 | * 删除成员的未封装版本 137 | */ 138 | exports._deleteUser = function (id, callback) { 139 | // https://qyapi.weixin.qq.com/cgi-bin/user/delete?access_token=ACCESS_TOKEN&userid=lisi 140 | var url = this.prefix + 'user/delete?access_token=' + this.token.accessToken; 141 | var opts = {dataType: 'json', data: {userid: id}}; 142 | this.request(url, opts, wrapper(callback)); 143 | }; 144 | 145 | /** 146 | * 批量删除成员 147 | * 148 | * Examples: 149 | * ``` 150 | * api.deleteUsers(["zhangsan", "lisi"], callback); 151 | * ``` 152 | * 153 | * Callback: 154 | * 155 | * - `err`, 调用失败时得到的异常 156 | * - `result`, 调用正常时得到的对象 157 | * 158 | * Result: 159 | * ``` 160 | * { 161 | * "errcode": 0, 162 | * "errmsg": "deleted" 163 | * } 164 | * ``` 165 | * @param {Array} users 待删除的用户 166 | * @param {Function} callback 回调函数 167 | */ 168 | make(exports, 'deleteUsers', function (users, callback) { 169 | // https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN 170 | var url = this.prefix + 'user/batchdelete?access_token=' + this.token.accessToken; 171 | var opts = { 172 | "useridlist": users 173 | }; 174 | this.request(url, postJSON(opts), wrapper(callback)); 175 | }); 176 | 177 | /** 178 | * 获取成员 179 | * 180 | * Examples: 181 | * ``` 182 | * api.getUser(id, callback); 183 | * ``` 184 | * 185 | * Callback: 186 | * 187 | * - `err`, 调用失败时得到的异常 188 | * - `result`, 调用正常时得到的对象 189 | * 190 | * Result: 191 | * ``` 192 | * { 193 | * "errcode": 0, 194 | * "errmsg": "ok", 195 | * "userid": "zhangsan", 196 | * "name": "李四", 197 | * "department": [1, 2], 198 | * "position": "后台工程师", 199 | * "mobile": "15913215421", 200 | * "gender": 1, 201 | * "tel": "62394", 202 | * "email": "zhangsan@gzdev.com", 203 | * "weixinid": "lisifordev", 204 | * "avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0", 205 | * "status": 1 206 | * } 207 | * ``` 208 | * @param {Number} id 成员ID 209 | * @param {Function} callback 回调函数 210 | */ 211 | exports.getUser = function (id, callback) { 212 | this.preRequest(this._getUser, arguments); 213 | }; 214 | 215 | /*! 216 | * 获取成员的未封装版本 217 | */ 218 | exports._getUser = function (id, callback) { 219 | // https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=lisi 220 | var url = this.prefix + 'user/get?access_token=' + this.token.accessToken; 221 | var opts = {dataType: 'json', data: {userid: id}}; 222 | this.request(url, opts, wrapper(callback)); 223 | }; 224 | 225 | /** 226 | * 获取部门成员 227 | * 228 | * Examples: 229 | * ``` 230 | * api.getDepartmentUsers(departmentId, fetchChild, status, callback); 231 | * ``` 232 | * 233 | * Callback: 234 | * 235 | * - `err`, 调用失败时得到的异常 236 | * - `result`, 调用正常时得到的对象 237 | * 238 | * Result: 239 | * ``` 240 | * { 241 | * "errcode": 0, 242 | * "errmsg": "ok", 243 | * "userlist": [ 244 | * { 245 | * "userid": "zhangsan", 246 | * "name": "李四" 247 | * } 248 | * ] 249 | * } 250 | * ``` 251 | * @param {Number} departmentId 部门ID 252 | * @param {Number} fetchChild 值:1/0,是否递归获取子部门下面的成员 253 | * @param {Number} status 0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加 254 | * @param {Function} callback 回调函数 255 | */ 256 | exports.getDepartmentUsers = function (departmentId, fetchChild, status, callback) { 257 | this.preRequest(this._getDepartmentUsers, arguments); 258 | }; 259 | 260 | /*! 261 | * 获取部门成员的未封装版本 262 | */ 263 | exports._getDepartmentUsers = function (departmentId, fetchChild, status, callback) { 264 | // https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN&department_id=1&fetch_child=0&status=0 265 | var url = this.prefix + 'user/simplelist?access_token=' + this.token.accessToken; 266 | var opts = { 267 | dataType: 'json', 268 | data: { 269 | department_id: departmentId, 270 | fetch_child: fetchChild, 271 | status: status 272 | } 273 | }; 274 | this.request(url, opts, wrapper(callback)); 275 | }; 276 | 277 | /** 278 | * 获取部门成员(详情) 279 | * 280 | * Examples: 281 | * ``` 282 | * api.getDepartmentUsersDetail(departmentId, fetchChild, status, callback); 283 | * ``` 284 | * 285 | * Callback: 286 | * 287 | * - `err`, 调用失败时得到的异常 288 | * - `result`, 调用正常时得到的对象 289 | * 290 | * Result: 291 | * ``` 292 | * { 293 | * "errcode": 0, 294 | * "errmsg": "ok", 295 | * "userlist": [ 296 | * { 297 | * "userid": "zhangsan", 298 | * "name": "李四", 299 | * "department": [1, 2], 300 | * "position": "后台工程师", 301 | * "mobile": "15913215421", 302 | * "email": "zhangsan@gzdev.com", 303 | * "weixinid": "lisifordev", 304 | * "avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0", 305 | * "status": 1, 306 | * "extattr": {"attrs":[{"name":"爱好","value":"旅游"},{"name":"卡号","value":"1234567234"}]} 307 | * } 308 | * ] 309 | * } 310 | * ``` 311 | * @param {Number} departmentId 部门ID 312 | * @param {Number} fetchChild 值:1/0,是否递归获取子部门下面的成员 313 | * @param {Number} status 0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加 314 | * @param {Function} callback 回调函数 315 | */ 316 | exports.getDepartmentUsersDetail = function (departmentId, fetchChild, status, callback) { 317 | this.preRequest(this._getDepartmentUsersDetail, arguments); 318 | }; 319 | 320 | /*! 321 | * 获取部门成员(详情)的未封装版本 322 | */ 323 | exports._getDepartmentUsersDetail = function (departmentId, fetchChild, status, callback) { 324 | // https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=1&fetch_child=0&status=0 325 | var url = this.prefix + 'user/list?access_token=' + this.token.accessToken; 326 | var opts = { 327 | dataType: 'json', 328 | data: { 329 | department_id: departmentId, 330 | fetch_child: fetchChild, 331 | status: status 332 | } 333 | }; 334 | this.request(url, opts, wrapper(callback)); 335 | }; 336 | 337 | /** 338 | * 邀请成员关注 339 | * 340 | * 详情:http://qydev.weixin.qq.com/wiki/index.php?title=%E7%AE%A1%E7%90%86%E6%88%90%E5%91%98#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8 341 | * Examples: 342 | * ``` 343 | * api.inviteUser(id, invite_tips, callback); 344 | * ``` 345 | * 346 | * Callback: 347 | * 348 | * - `err`, 调用失败时得到的异常 349 | * - `result`, 调用正常时得到的对象 350 | * 351 | * Result: 352 | * ``` 353 | * { 354 | * "errcode": 0, 355 | * "errmsg": "ok", 356 | * "type":1 357 | * } 358 | * ``` 359 | * @param {String} id userid 360 | * @param {String} invite_tips 邀请的一句话 361 | * @param {Function} callback 回调函数 362 | */ 363 | exports.inviteUser = function (id, invite_tips, callback) { 364 | this.preRequest(this._inviteUser, arguments); 365 | }; 366 | 367 | /*! 368 | * 邀请成员关注的未封装版本 369 | */ 370 | exports._inviteUser = function (id, invite_tips, callback) { 371 | // https://qyapi.weixin.qq.com/cgi-bin/invite/send?access_token=ACCESS_TOKEN 372 | var url = this.prefix + 'invite/send?access_token=' + this.token.accessToken; 373 | var opts = { 374 | userid: id, 375 | invite_tips: invite_tips 376 | }; 377 | this.request(url, postJSON(opts), wrapper(callback)); 378 | }; 379 | 380 | /** 381 | * 根据Code获取用户ID 382 | * 383 | * 详情:http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息 384 | * Examples: 385 | * ``` 386 | * api.getUserIdByCode(code, callback); 387 | * ``` 388 | * 389 | * Callback: 390 | * 391 | * - `err`, 调用失败时得到的异常 392 | * - `result`, 调用正常时得到的对象 393 | * 394 | * Result: 395 | * ``` 396 | * { 397 | * "UserId": "USERID" 398 | * } 399 | * ``` 400 | * @param {String} code OAuth授权获取的code 401 | * @param {Function} callback 回调函数 402 | */ 403 | exports.getUserIdByCode = function (code, callback) { 404 | this.preRequest(this._getUserIdByCode, arguments); 405 | }; 406 | 407 | /*! 408 | * 获取成员的未封装版本 409 | */ 410 | exports._getUserIdByCode = function (code, callback) { 411 | // https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE&agentid=AGENTID 412 | var url = this.prefix + 'user/getuserinfo?access_token=' + this.token.accessToken; 413 | var opts = { 414 | dataType: 'json', 415 | data: { 416 | code: code, 417 | agentid: this.agentid 418 | } 419 | }; 420 | this.request(url, opts, wrapper(callback)); 421 | }; 422 | 423 | /** 424 | * 获取授权页面的URL地址 425 | * @param {String} redirect 授权后要跳转的地址 426 | * @param {String} state 开发者可提供的数据 427 | * @param {String} scope 作用范围,值为snsapi_userinfo和snsapi_base,前者用于弹出,后者用于跳转 428 | */ 429 | exports.getAuthorizeURL = function (redirect, state, scope) { 430 | var url = 'https://open.weixin.qq.com/connect/oauth2/authorize'; 431 | var info = { 432 | appid: this.corpid, 433 | redirect_uri: redirect, 434 | response_type: 'code', 435 | scope: scope || 'snsapi_base', 436 | state: state || '' 437 | }; 438 | 439 | return url + '?' + querystring.stringify(info) + '#wechat_redirect'; 440 | }; 441 | --------------------------------------------------------------------------------