├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── dingtalk_crypt.js ├── dingtalk_enterprise.js ├── dingtalk_sso.js ├── dingtalk_suite.js └── dingtalk_suite_callback.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DingTalk ISV 2 | 钉钉套件主动调用**API**, 自带**cache**。 3 | 4 | ##安装 5 | `npm install dingtalk-isv` 6 | 7 | ##ISV套件回调URL处理 8 | express中间件。自动验证回调URL有效性。 9 | 10 | 构造函数: 11 | 12 | ```js 13 | var DingIsv = require('dingtalk-isv'); 14 | var config = { 15 | token: 'xxxxxxxxx', 16 | encodingAESKey: 'xxxxxxxxxxxxxxxxxxx', 17 | suiteid: 'xxxxxxxxxxxx', //第一次验证没有不用填 18 | saveTicket: function(data, callback){//可选,和主动调用API: dingtalk_suite 配合使用。 19 | //data:{value: ticket字符串, expires:到期时间,钉钉回调时间戳 + 20分钟} 20 | //..dosomething 21 | } 22 | } 23 | 24 | app.post('/dingtalk/isv/receive', DingIsv.SuiteCallBack(config, 25 | function(message, req, res, next){ 26 | console.log('message', message); 27 | switch (message.EventType) { 28 | case 'tmp_auth_code': //企业号临时授权码 29 | /*企业号临时授权码,需调用接口置换永久授权码 30 | { AuthCode: '6b4294d637a0387eb36e6785451ff845', EventType: 'tmp_auth_code',SuiteKey: 'suitexpiycccccccccchj',TimeStamp: '1452665779818' }*/ 31 | 32 | res.reply(); 33 | break; 34 | 35 | case 'change_auth': 36 | /*授权变更消息*/ 37 | res.reply(); 38 | break; 39 | case 'suite_relieve': 40 | /*解除授权消息 41 | { AuthCorpId: 'ding5bfeb97afcccb984', EventType: 'suite_relieve', SuiteKey: 'suitexpiycccccccccchj',TimeStamp: '1452665774168' }*/ 42 | 43 | res.reply(); 44 | break; 45 | 46 | case 'suite_ticket': 47 | /*ticket,间隔20分。如果有config.saveTicket 不会触发。 48 | { EventType: 'suite_ticket', SuiteKey: 'suitexpiycccccccccchj',SuiteTicket: 'wrEooJqhQlNcWU327mtr20yzWkPtea9LOm0P8w2M3MDjRPUYY5Tu9fspDhZ8HPXeP5yzKuorHIQ0P9GSU5evAc',TimeStamp: '1452328049089'}*/ 49 | 50 | res.reply(); 51 | break; 52 | default: 53 | res.json({errcode: 1000, errmsg: 'error, ddtalk unknow EventType'}); 54 | } 55 | })); 56 | ``` 57 | 58 | 59 | ##ISV套件API操作示例 60 | 构造函数: 61 | ```js 62 | var DingIsv = require('dingtalk-isv'); 63 | var conf = { 64 | suiteid: 'suitexpiygdnxxxxx', 65 | secret: 'C1oXyeJUgH_QXEHYJS4-Um-zxfxxxxxxxxxxxxxxxxxx-6np3fXskv5dGs', 66 | getTicket: function(){ 67 | //从数据库中取出Tikcet,返回的data样式为:{value: 'xxxxxxx', expires:1452735301543} 68 | //ticket从 dingtalk_suite_callback 处获得 69 | return new Promise(function(reslove,reject){ 70 | //..dosomething 71 | }) 72 | }, 73 | 74 | getToken: function(){ 75 | //从数据库中取出Token,返回的data样式为:{value: 'xxxxxxx', expires:1452735301543} 76 | return new Promise(function(reslove,reject){ 77 | //..dosomething 78 | }) 79 | }, 80 | 81 | saveToken: function(data){ 82 | //存储Token到数据库中,data样式为:{value: 'xxxxxxx', expires:1452735301543//过期时间} 83 | //..dosomething 84 | } 85 | } 86 | var api = new DingIsv.Suite(conf); 87 | ``` 88 | 89 | ##方法 90 | #### 获取企业号永久授权码 91 | ```js 92 | api.getPermanentCode(tmp_auth_code).then(function(result){ }) 93 | ``` 94 | tmp_auth_code字符串,从DingIsv.SuiteCallBack处获得。 95 | #### 获取企业号Token 96 | ```js 97 | //auth_corpid和permanent_code由上面接口获得。 98 | api.getCorpToken(auth_corpid, permanent_code).then(function(result){ }) 99 | ``` 100 | #### 获取企业号信息 101 | ```js 102 | api.getAuthInfo(auth_corpid, permanent_code).then(function(result){ }) 103 | ``` 104 | #### 获取企业号应用 105 | ```js 106 | api.getAgent(agentid, auth_corpid, permanent_code).then(function(result){ }) 107 | ``` 108 | #### 激活授权套件 109 | ```js 110 | api.activateSuite(auth_corpid, permanent_code).then(function(result){ }) 111 | ``` 112 | #### 为授权方的企业单独设置IP白名单 113 | ```js 114 | //ip_whitelist为数组格式:["1.2.3.4","5.6.*.*"] 115 | api.setCorpIpwhitelist(auth_corpid, ip_whitelist).then(function(result){ }) 116 | ``` 117 | 118 | 119 | 120 | # dingtalk sso 121 | 钉钉免登接口,ISV和企业号通用。 122 | 123 | ##示例 124 | 构造函数: 125 | ```js 126 | var DingIsv = require('dingtalk-isv'); 127 | var conf = { 128 | corpid: 'dingxxxxxxxxxxxxxxx', 129 | SSOSecret:'C1oXyeJUgH_QXEHYJS4-Um-zxfxxxxxxxxxxxxxxxxxx-6np3fXskv5dGs' 130 | } 131 | //ISV的corpid,SSOSecret在 http://console.d.aliyun.com/#/dingding/env 查看。 132 | var api = new DingIsv.SSO(conf); 133 | 134 | ``` 135 | ##方法 136 | ### 通过CODE(免登授权码)换取用户身份 137 | ```js 138 | api.getSSOUserInfoByCode(code).then(function(result){ }) 139 | ``` 140 | ### 生成授权链接 141 | ```js 142 | api.generateAuthUrl(redirect_url).then(function(result){ }) 143 | ``` 144 | 145 | 146 | 147 | 148 | 149 | # dingtalk enterprise 150 | 钉钉企业号**API**,自带**cache**,并自带**ISV**套件操纵接口。 151 | ##示例 152 | ####config: 153 | ```js 154 | var DingIsv = require('dingtalk-isv'); 155 | 156 | var config = { 157 | corpid : 'xxxxxxxxxxxxxxxx', //ISV套件控制的话,可不填 158 | secret : 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx', //ISV套件控制的话,可不填 159 | getToken : function(){ 160 | //从数据库中取出Token,返回的data样式为:{value: 'xxxxxxx', expires:1452735301543} 161 | //return '' 162 | }, 163 | 164 | saveToken : function(data){ 165 | //存储Token到数据库中,data样式为:{value: 'xxxxxxx', expires:1452735301543//过期时间} 166 | //save ... 167 | }, 168 | 169 | getJsApiTicket : function(){ 170 | //从数据库中取出JsApiTicket,返回的data样式为:{value: 'xxxxxxx', expires:1452735301543} 171 | //return ... 172 | }, 173 | 174 | saveJsApiTicket : function(data, callback){ 175 | //存储JsApiTicket到数据库中,data样式为:{value: 'xxxxxxx', expires:1452735301543//过期时间} 176 | //save ... 177 | } 178 | }; 179 | ``` 180 | ###创建企业号API: 181 | ```js 182 | var api = new DingIsv.Enterprise(config); 183 | ``` 184 | ###用ISV套件操作企业号?OK 185 | 只需要两个参数: 186 | ```js 187 | //newSuiteApi: 一个dingtalk_suite实例。 188 | var dingEnterprise = new DingIsv.Enterprise.fromSuite(dingSuite); 189 | //只需传入corpid, 和企业号的永久授权码就能控制企业号。 190 | var api = dingEnterprise.ctrl(corpid, permanent_code); 191 | ``` 192 | 如果你获取永久授权码的同时,获得了token_cache,可以加上第三个参数,这样可以省一次数据库查询。 193 | ```js 194 | //token为Object格式 key为: value , expires 195 | var api = dingEnterprise.ctrl(corpid, permanent_code, token_cache); 196 | ``` 197 | 如果你获取永久授权码的同时,获得了token_cache和jsapi_ticket_cache,可以加上第四个参数,这样可以省两次数据库查询。 198 | ```js 199 | //token和jsapi_ticket_cache为Object格式 key为: value , expires 200 | var api = dingEnterprise.ctrl(corpid, permanent_code, token_cache, jsapi_ticket_cache); 201 | ``` 202 | 203 | 204 | ##接口方法 205 | ###主要方法 206 | ####api.getLatestToken().then(function(result){ }); 207 | 获得最新token。 208 | ```js 209 | //例: 210 | api.getLatestToken().then(function(token){ 211 | //token格式为:{value: 'xxxxxxx', expires:1452735301543//过期时间} 212 | console.log('token',token); 213 | }); 214 | ``` 215 | ####api.getUrlSign(url); 216 | 生成url授权参数,用于前端jsConfig. 217 | ```js 218 | //例: 219 | api.getUrlSign('http://www.test.com/path').then(function(result){ 220 | /* 221 | result格式为:{ 222 | signature: '23sadfasdfasdf', 223 | timeStamp:'24234234234234', 224 | nonceStr:'asfdasdfasdfasfdx' 225 | } 226 | */ 227 | console.log('result',result); 228 | }); 229 | ``` 230 | ####api.get(ddApiPath, opts).then(function(result){ }); 231 | 代理get方法。使有此方法可调用钉钉官方企业号文档的get接口,而不用管token。 232 | ```js 233 | //例: 234 | //获取部门列表 235 | //钉钉文档:http://ddtalk.github.io/dingTalkDoc/?spm=a3140.7785475.0.0.p5bAUd#获取部门列表 236 | api.get('/department/list').then(function(result){ 237 | console.log('result', result); 238 | }); 239 | 240 | //获取部门详情 241 | //钉钉文档:http://ddtalk.github.io/dingTalkDoc/?spm=a3140.7785475.0.0.p5bAUd#获取部门详情 242 | api.get('/department/get', {id:2}).then(function(result){ 243 | console.log('result', result); 244 | }); 245 | ``` 246 | ####api.post(ddApiPath, opts).then(function(result){ }); 247 | 代理post方法。使有此方法可调用钉钉官方企业号文档的post接口,而不用管token。 248 | 249 | 用法同api.get。 250 | 251 | ###其它封装的一些方法。 252 | ####部门 253 | ```js 254 | //获得部门列表 255 | api.getDepartments().then(function(result){ }); 256 | 257 | //获得部门详情 258 | api.getDepartmentDetail(id).then(function(result){ }); 259 | 260 | //创建部门 261 | api.createDepartment(name, opts).then(function(result){ }); 262 | //例 263 | //名字,父id 264 | api.createDepartment('部门一', 1).then(function(result){ }); 265 | //名字,详细配置 266 | api.createDepartment('部门一', {parentid: 1, order:1}).then(function(result){ }); 267 | 268 | //更新部门 269 | api.updateDepartment(id, opts).then(function(result){ }); 270 | 271 | //删除部门 272 | api.deleteDepartment(id).then(function(result){ }); 273 | 274 | ``` 275 | ####微应用 276 | ```js 277 | api.createMicroApp(data).then(function(result){ }); 278 | 279 | ``` 280 | ####消息 281 | ```js 282 | api.sendToConversation().then(function(result){ }); 283 | 284 | api.send(agentid, msg).then(function(result){ }); 285 | 286 | ``` 287 | ####用户 288 | ```js 289 | //获取部门用户 290 | api.getDepartmentUsers(id).then(function(result){ }); 291 | 292 | //获取部门用户详细 293 | api.getDepartmentUsersDetail(id).then(function(result){ }); 294 | 295 | //获取用户信息 296 | api.getUser(id).then(function(result){ }); 297 | 298 | //通过code获取用户一些信息(App登录用)。 299 | api.getUserInfoByCode(code).then(function(result){ }); 300 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/22. 3 | */ 4 | 5 | exports.Suite = require('./lib/dingtalk_suite'); 6 | exports.SuiteCallBack = require('./lib/dingtalk_suite_callback'); 7 | exports.SSO = require('./lib/dingtalk_sso'); 8 | exports.Enterprise = require('./lib/dingtalk_enterprise'); 9 | -------------------------------------------------------------------------------- /lib/dingtalk_crypt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/21. 3 | */ 4 | var crypto = require('crypto'); 5 | 6 | /** 7 | * 基于PKCS#7算法的加解密接口 8 | * 9 | */ 10 | var PKCS7Encoder = { 11 | /** 12 | * 删除解密后明文的补位字符 13 | * 14 | * @param {String} text 解密后的明文 15 | */ 16 | decode: function (text) { 17 | var pad = text[text.length - 1]; 18 | if (pad < 1 || pad > 32) { 19 | pad = 0; 20 | } 21 | return text.slice(0, text.length - pad); 22 | }, 23 | /** 24 | * 对需要加密的明文进行填充补位 25 | * 26 | * @param {String} text 需要进行填充补位操作的明文 27 | */ 28 | encode: function (text) { 29 | var blockSize = 32; 30 | var textLength = text.length; 31 | //计算需要填充的位数 32 | var amountToPad = blockSize - (textLength % blockSize); 33 | var result = new Buffer(amountToPad); 34 | result.fill(amountToPad); 35 | return Buffer.concat([text, result]); 36 | } 37 | }; 38 | 39 | 40 | /** 41 | * 加解密信息构造函数 42 | * 43 | * @param {String} token 设置的Token 44 | * @param {String} encodingAESKey 设置的EncodingAESKey 45 | * @param {String} id suiteid 46 | */ 47 | var DingTalkCrypt = function (token, encodingAESKey, id) { 48 | if (!token || !encodingAESKey || !id) { 49 | throw new Error('please check arguments'); 50 | } 51 | this.token = token; 52 | this.id = id; 53 | var AESKey = new Buffer(encodingAESKey + '=', 'base64'); 54 | if (AESKey.length !== 32) { 55 | throw new Error('encodingAESKey invalid'); 56 | } 57 | this.key = AESKey; 58 | this.iv = AESKey.slice(0, 16); 59 | }; 60 | 61 | /** 62 | * 获取签名 63 | * 64 | * @param {String} timestamp 时间戳 65 | * @param {String} nonce 随机数 66 | * @param {String} encrypt 加密后的文本 67 | */ 68 | DingTalkCrypt.prototype.getSignature = function(timestamp, nonce, encrypt) { 69 | var shasum = crypto.createHash('sha1'); 70 | var arr = [this.token, timestamp, nonce, encrypt].sort(); 71 | shasum.update(arr.join('')); 72 | return shasum.digest('hex'); 73 | }; 74 | 75 | /** 76 | * 对密文进行解密 77 | * 78 | * @param {String} text 待解密的密文 79 | */ 80 | DingTalkCrypt.prototype.decrypt = function(text) { 81 | // 创建解密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节 82 | var decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv); 83 | decipher.setAutoPadding(false); 84 | var deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()]); 85 | 86 | deciphered = PKCS7Encoder.decode(deciphered); 87 | // 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID] 88 | // 去除16位随机数 89 | var content = deciphered.slice(16); 90 | var length = content.slice(0, 4).readUInt32BE(0); 91 | 92 | return { 93 | message: content.slice(4, length + 4).toString(), 94 | id: content.slice(length + 4).toString() 95 | }; 96 | }; 97 | 98 | /** 99 | * 对明文进行加密 100 | * 101 | * @param {String} text 待加密的明文 102 | */ 103 | DingTalkCrypt.prototype.encrypt = function (text) { 104 | // 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID] 105 | // 获取16B的随机字符串 106 | var randomString = crypto.pseudoRandomBytes(16); 107 | 108 | var msg = new Buffer(text); 109 | 110 | // 获取4B的内容长度的网络字节序 111 | var msgLength = new Buffer(4); 112 | msgLength.writeUInt32BE(msg.length, 0); 113 | 114 | var id = new Buffer(this.id); 115 | 116 | var bufMsg = Buffer.concat([randomString, msgLength, msg, id]); 117 | 118 | // 对明文进行补位操作 119 | var encoded = PKCS7Encoder.encode(bufMsg); 120 | 121 | // 创建加密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节 122 | var cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv); 123 | cipher.setAutoPadding(false); 124 | 125 | var cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()]); 126 | 127 | // 返回加密数据的base64编码 128 | return cipheredMsg.toString('base64'); 129 | }; 130 | 131 | module.exports = DingTalkCrypt; 132 | 133 | 134 | 135 | //var dingCrypt = new DingTalkCrypt('along', 'zvb9pr3w6hpol1re9sk85d37me4i7liazikiunslci8', 'suite4xxxxxxxxxxxxxxx'); 136 | //console.log('da59f9e658057569616f9fdb26f3e16ec5b6a904' === dingCrypt.getSignature('1466505128368', '3nH71pLV', 'D6vOBD1kWeyb+bzC1oJNdEzm6Owrb7HPS8P01omJXyzyk5/u/e4OfH1YXHNgJ1snZb0ZIg/4HA6aePhhl2lxtsw8nJVQKi+A9GDb3qIw0YKuUdBQGFC50gPodlqS3Rdz4FLdEkOwyS+BxNXVFfzdTqB+JrtYN1ifrrMm78qGMap59HlNNiAye3/xkGo4Kq3iZmXQPBtp4KS1YzvpmMueoQ==')); 137 | // 138 | //var result = dingCrypt.decrypt('D6vOBD1kWeyb+bzC1oJNdEzm6Owrb7HPS8P01omJXyzyk5/u/e4OfH1YXHNgJ1snZb0ZIg/4HA6aePhhl2lxtsw8nJVQKi+A9GDb3qIw0YKuUdBQGFC50gPodlqS3Rdz4FLdEkOwyS+BxNXVFfzdTqB+JrtYN1ifrrMm78qGMap59HlNNiAye3/xkGo4Kq3iZmXQPBtp4KS1YzvpmMueoQ=='); 139 | //console.log(JSON.parse(result.message)) 140 | // 141 | -------------------------------------------------------------------------------- /lib/dingtalk_enterprise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/24. 3 | */ 4 | 5 | var agent = require('superagent'); 6 | var crypto = require('crypto'); 7 | var BASE_URL = 'https://oapi.dingtalk.com'; 8 | var TOKEN_EXPIRES_IN = 1000 * 60 * 60 * 2 - 10000 //1小时59分50秒.防止网络延迟 9 | 10 | var Api = function(conf) { 11 | if (typeof conf === 'string') { 12 | this.token_cache = { 13 | value: conf, 14 | expires: Infinity 15 | }; 16 | 17 | if(arguments[1]){ 18 | this.jsapi_ticket_cache = { 19 | value: arguments[1], 20 | expires: Infinity 21 | }; 22 | } 23 | 24 | } else { 25 | this.corpid = conf.corpid; 26 | this.secret = conf.secret; 27 | this.token_cache = null; 28 | this.jsapi_ticket_cache = null; 29 | this.getJsApiTicket = conf.getJsApiTicket || function(){ return Promise.resolve(null) }; 30 | this.saveJsApiTicket = conf.saveJsApiTicket || function(){ return Promise.resolve(null) }; 31 | 32 | this.getToken = conf.getToken || function(){ return Promise.resolve(this.token_cache);}; 33 | this.saveToken = conf.saveToken || function(token) { 34 | this.token_cache = token; 35 | if (process.env.NODE_ENV === 'production') { 36 | console.warn('Don\'t save token in memory, when cluster or multi-computer!'); 37 | } 38 | Promise.resolve(this.token_cache); 39 | }; 40 | this.token_expires_in = conf.token_expires_in || TOKEN_EXPIRES_IN; 41 | } 42 | } 43 | 44 | Api.prototype._get_access_token = function() { 45 | var self = this; 46 | return new Promise(function (resolve, reject) { 47 | agent.get(BASE_URL + '/gettoken') 48 | .query({ 49 | corpid: self.corpid, 50 | corpsecret: self.secret 51 | }).end(wrapper(resolve, reject)); 52 | }) 53 | }; 54 | 55 | Api.prototype.getLatestToken = function() { 56 | var self = this; 57 | if (!self.token_cache) { 58 | return self.getToken().then(function (token) { 59 | self.token_cache = token || {expires: 0}; 60 | return self.getLatestToken(); 61 | }); 62 | } else { 63 | var now = Date.now(); 64 | if (self.token_cache.expires <= now) { 65 | return self._get_access_token().then(function (token) { 66 | self.token_cache = {value: token.access_token, expires: now + self.token_expires_in}; 67 | self.saveToken(self.token_cache); 68 | return Promise.resolve(self.token_cache); 69 | }); 70 | } else { 71 | return Promise.resolve(self.token_cache); 72 | } 73 | } 74 | } 75 | 76 | //代理:get方法 77 | Api.prototype.get = function(path, data){ 78 | return this.getLatestToken().then(function (token) { 79 | data.access_token = token.value; 80 | return new Promise(function (resolve, reject) { 81 | agent.get(BASE_URL + path).query(data).end(wrapper(resolve, reject)); 82 | }) 83 | }); 84 | } 85 | 86 | //代理:post方法 87 | Api.prototype.post = function(path, data){ 88 | return this.getLatestToken().then(function (token) { 89 | return new Promise(function (resolve, reject) { 90 | agent.post(BASE_URL + path) 91 | .query({access_token: token.value}) 92 | .send(data) 93 | .end(wrapper(resolve, reject)); 94 | }) 95 | }); 96 | } 97 | 98 | //=============================== 部门 =============================== 99 | 100 | Api.prototype.getDepartments = function() { 101 | var self = this; 102 | return new Promise(function (resolve, reject) { 103 | self.getLatestToken().then(function (token) { 104 | agent.get(BASE_URL + '/department/list') 105 | .query({access_token: token.value}) 106 | .end(wrapper(resolve, reject)); 107 | }); 108 | }) 109 | } 110 | 111 | Api.prototype.getDepartmentDetail = function(id) { 112 | var self = this; 113 | return new Promise(function (resolve, reject) { 114 | self.getLatestToken().then(function (token) { 115 | agent.get(BASE_URL + '/department/get') 116 | .query({id: id,access_token: token.value}) 117 | .end(wrapper(resolve, reject)); 118 | }); 119 | }) 120 | } 121 | 122 | Api.prototype.createDepartment = function(name, opts) { 123 | var self = this; 124 | return new Promise(function (resolve, reject) { 125 | self.getLatestToken().then(function (token) { 126 | if (typeof opts === 'object') { 127 | opts.name = name; 128 | opts.parentid = opts.parentid || 1; 129 | } else { 130 | opts = { 131 | name: name, 132 | parentid: opts 133 | } 134 | } 135 | agent.post(BASE_URL + '/department/create') 136 | .query({access_token: token.value}) 137 | .send(opts) 138 | .end(wrapper(resolve, reject)); 139 | }); 140 | }) 141 | } 142 | 143 | Api.prototype.updateDepartment = function(id, opts) { 144 | var self = this; 145 | return new Promise(function (resolve, reject) { 146 | self.getLatestToken().then(function(token) { 147 | if (typeof opts === 'object') { 148 | opts.id = id; 149 | } else { 150 | opts = {name: opts, id: id} 151 | } 152 | agent.post(BASE_URL + '/department/update') 153 | .query({ access_token: token.value }) 154 | .send(opts) 155 | .end(wrapper (resolve,reject)); 156 | }); 157 | }) 158 | } 159 | 160 | Api.prototype.deleteDepartment = function(id) { 161 | var self = this; 162 | return new Promise(function (resolve, reject) { 163 | self.getLatestToken().then(function (token) { 164 | agent.get(BASE_URL + '/department/delete') 165 | .query({id: id, access_token: token.value}) 166 | .end(wrapper(resolve, reject)); 167 | }); 168 | }) 169 | } 170 | 171 | //=============================== 微应用 =============================== 172 | 173 | Api.prototype.createMicroApp = function(data) { 174 | var self = this; 175 | return new Promise(function (resolve, reject) { 176 | self.getLatestToken().then(function (token) { 177 | agent.post(BASE_URL + '/microapp/create') 178 | .query({access_token: token.value}) 179 | .send(data) 180 | .end(wrapper(resolve, reject)); 181 | }); 182 | }) 183 | }; 184 | 185 | //=============================== 消息 =============================== 186 | // 187 | Api.prototype.sendToConversation = function(agentid, options) { 188 | var self = this; 189 | return new Promise(function (resolve, reject) { 190 | self.getLatestToken().then(function (token) { 191 | options.agentid = agentid + ''; 192 | agent.post(BASE_URL + '/message/send_to_conversation') 193 | .query({access_token: token.value}) 194 | .send(options) 195 | .end(wrapper(resolve, reject)); 196 | }); 197 | }) 198 | }; 199 | 200 | Api.prototype.send = function(agentid, options) { 201 | var self = this; 202 | return new Promise(function (resolve, reject) { 203 | self.getLatestToken().then(function (token) { 204 | options.agentid = agentid + ''; 205 | agent.post(BASE_URL + '/message/send') 206 | .query({ access_token: token.value }) 207 | .send(options) 208 | .end(wrapper(resolve, reject)); 209 | }); 210 | }) 211 | }; 212 | 213 | //=============================== 用户 =============================== 214 | 215 | Api.prototype.getDepartmentUsers = function(id) { 216 | var self = this; 217 | return new Promise(function (resolve, reject) { 218 | self.getLatestToken().then(function (token) { 219 | agent.get(BASE_URL + '/user/simplelist') 220 | .query({department_id: id, access_token: token.value}) 221 | .end(wrapper(resolve, reject)); 222 | }); 223 | }) 224 | } 225 | 226 | Api.prototype.getDepartmentUsersDetail = function(id) { 227 | var self = this; 228 | return new Promise(function (resolve, reject) { 229 | self.getLatestToken().then(function (token) { 230 | agent.get(BASE_URL + '/user/list') 231 | .query({department_id: id, access_token: token.value}) 232 | .end(wrapper(resolve, reject)); 233 | }); 234 | }) 235 | } 236 | 237 | 238 | Api.prototype.getUser = function(id) { 239 | var self = this; 240 | return new Promise(function (resolve, reject) { 241 | self.getLatestToken().then(function(token) { 242 | agent.get(BASE_URL + '/user/get') 243 | .query({userid: id, access_token: token.value }) 244 | .end(wrapper (resolve,reject)); 245 | }); 246 | }) 247 | } 248 | 249 | //登录 250 | Api.prototype.getUserInfoByCode = function(code) { 251 | var self = this; 252 | return new Promise(function (resolve, reject) { 253 | self.getLatestToken().then(function (token) { 254 | agent.get(BASE_URL + '/user/getuserinfo') 255 | .query({code: code, access_token: token.value}) 256 | .end(wrapper(resolve, reject)); 257 | }); 258 | }) 259 | }; 260 | 261 | 262 | 263 | //=============================== jsApi Ticket =============================== 264 | 265 | Api.prototype._get_jsApi_ticket = function() { 266 | var self = this; 267 | return self.getLatestToken().then(function (token) { 268 | return new Promise(function (resolve, reject) { 269 | agent.get(BASE_URL + '/get_jsapi_ticket') 270 | .query({type: 'jsapi', access_token: token.value}) 271 | .end(wrapper(resolve, reject)); 272 | }) 273 | }) 274 | }; 275 | 276 | 277 | Api.prototype.getLatestJsApiTicket = function() { 278 | var self = this; 279 | if (!self.jsapi_ticket_cache) { 280 | return self.getJsApiTicket().then(function (data) { 281 | self.jsapi_ticket_cache = data || {expires: 0}; 282 | return self.getLatestJsApiTicket(); 283 | }); 284 | } else { 285 | var now = Date.now(); 286 | if (self.jsapi_ticket_cache.expires <= now) { 287 | return self._get_jsApi_ticket().then(function(data) { 288 | self.jsapi_ticket_cache = {value: data.ticket, expires: now + self.token_expires_in}; 289 | self.saveJsApiTicket(data); 290 | return self.jsapi_ticket_cache; 291 | }) 292 | } else { 293 | return Promise.resolve(this.jsapi_ticket_cache); 294 | } 295 | } 296 | } 297 | 298 | 299 | var createNonceStr = function() { 300 | return Math.random().toString(36).substr(2, 15); 301 | }; 302 | 303 | var raw = function (args) { 304 | var keys = Object.keys(args); 305 | keys = keys.sort(); 306 | var newArgs = {}; 307 | keys.forEach(function (key) { 308 | newArgs[key] = args[key]; 309 | }); 310 | 311 | var string = ''; 312 | for (var k in newArgs) { 313 | string += '&' + k + '=' + newArgs[k]; 314 | } 315 | return string.substr(1); 316 | }; 317 | 318 | var sign = function(ret) { 319 | var string = raw(ret); 320 | var shasum = crypto.createHash('sha1'); 321 | shasum.update(string); 322 | return shasum.digest('hex'); 323 | }; 324 | 325 | 326 | 327 | /*Api.prototype.generate = function(param, callback){ 328 | }*/ 329 | 330 | Api.prototype.getUrlSign = function(url) { 331 | var self = this; 332 | return self.getLatestJsApiTicket().then(function (data) { 333 | var result = { 334 | noncestr: createNonceStr(), 335 | jsapi_ticket: data.value, 336 | timestamp: Date.now(), 337 | url: url 338 | } 339 | 340 | var signature = sign(result); 341 | result = { 342 | signature: signature, 343 | timeStamp: result.timestamp.toString(), 344 | nonceStr: result.noncestr 345 | } 346 | return result; 347 | }); 348 | 349 | } 350 | 351 | //=============================== ISV Suite Ctrl =============================== 352 | 353 | Api.fromSuite = function(newSuiteApi, conf) { 354 | for (var i in conf) { 355 | this[i] = conf[i]; 356 | } 357 | this.newSuiteApi = newSuiteApi; 358 | } 359 | 360 | Api.fromSuite.prototype.ctrl = function(corpid, permanent_code, token_cache, jsapi_ticket_cache) { 361 | this.corpid = corpid; 362 | this.token_cache = token_cache; 363 | this.jsapi_ticket_cache = jsapi_ticket_cache; 364 | 365 | var api = new Api(this); 366 | var newSuiteApi = this.newSuiteApi; 367 | api._get_access_token = function(){ 368 | return newSuiteApi.getCorpToken(corpid, permanent_code); 369 | } 370 | return api; 371 | } 372 | 373 | 374 | 375 | //对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 376 | function wrapper (resolve,reject) { 377 | return function (err, data) { 378 | if (err) { 379 | err.name = 'DingTalkAPI' + err.name; 380 | return reject(err); 381 | } 382 | data = data.body; 383 | if (data.errcode) { 384 | err = new Error(data.errmsg); 385 | err.name = 'DingTalkAPIError'; 386 | err.code = data.errcode; 387 | return reject(err, data); 388 | } 389 | resolve(data); 390 | }; 391 | }; 392 | 393 | module.exports = Api; 394 | -------------------------------------------------------------------------------- /lib/dingtalk_sso.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/23. 3 | */ 4 | var agent = require('superagent'); 5 | var SSO_BASE_URL = 'https://oapi.dingtalk.com'; 6 | 7 | var Api = function(conf) { 8 | this.SSOSecret = conf.SSOSecret; 9 | this.corpid = conf.corpid; 10 | } 11 | 12 | Api.prototype.getSSOToken = function() { 13 | var self = this; 14 | return new Promise(function (reslove, reject) { 15 | agent.get(SSO_BASE_URL + '/sso/gettoken').query({ 16 | corpid: self.corpid, 17 | corpsecret: self.SSOSecret 18 | }).end(wrapper(reslove, reject)); 19 | }) 20 | }; 21 | 22 | //登录 23 | Api.prototype.getSSOUserInfoByCode = function(code) { 24 | var self = this; 25 | return new Promise(function (reslove, reject) { 26 | self.getSSOToken().then(function (token) { 27 | agent.get(SSO_BASE_URL + '/sso/getuserinfo') 28 | .query({ 29 | code: code, 30 | access_token: token.access_token 31 | }) 32 | .end(wrapper(reslove, reject)); 33 | }); 34 | }) 35 | }; 36 | 37 | //生成授权链接 38 | Api.prototype.generateAuthUrl = function(redirect_url) { 39 | return 'https://oa.dingtalk.com/omp/api/micro_app/admin/landing?corpid=' + this.corpid + '&redirect_url=' + redirect_url; 40 | }; 41 | 42 | //对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 43 | function wrapper (resolve,reject) { 44 | return function (err, data) { 45 | if (err) { 46 | err.name = 'DingTalkAPI' + err.name; 47 | return reject(err); 48 | } 49 | data = data.body; 50 | if (data.errcode) { 51 | err = new Error(data.errmsg); 52 | err.name = 'DingTalkAPIError'; 53 | err.code = data.errcode; 54 | return reject(err, data); 55 | } 56 | resolve(data); 57 | }; 58 | }; 59 | 60 | module.exports = Api; -------------------------------------------------------------------------------- /lib/dingtalk_suite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/22. 3 | */ 4 | var agent = require('superagent'); 5 | var BASE_URL = 'https://oapi.dingtalk.com/service'; 6 | var SSO_BASE_URL = 'https://oapi.dingtalk.com'; 7 | var TICKET_EXPIRES_IN = 1000 * 60 * 20 //20分钟 8 | var TOKEN_EXPIRES_IN = 1000 * 60 * 60 * 2 - 10000 //1小时59分50秒.防止网络延迟 9 | 10 | 11 | var Api = function(conf) { 12 | this.suite_key = conf.suiteid; 13 | this.suite_secret = conf.secret; 14 | this.ticket_expires_in = TICKET_EXPIRES_IN; 15 | this.token_expires_in = conf.token_expires_in || TOKEN_EXPIRES_IN; 16 | 17 | this.getTicket = conf.getTicket; 18 | this.getToken = conf.getToken; 19 | this.saveToken = conf.saveToken; 20 | 21 | this.ticket_cache = { 22 | expires: 0, 23 | value: null 24 | }; 25 | this.token_cache = null; 26 | 27 | } 28 | 29 | Api.prototype.getLatestTicket = function() { 30 | var now = Date.now(); 31 | if (this.ticket_cache.expires <= now) { 32 | return this.getTicket(); 33 | } else { 34 | return Promise.resolve(this.ticket_cache); 35 | } 36 | } 37 | 38 | Api.prototype._get_access_token = function(callback) { 39 | var self = this; 40 | return this.getLatestTicket().then(function(ticket) { 41 | var data = { 42 | suite_key: self.suite_key, 43 | suite_secret: self.suite_secret, 44 | suite_ticket: ticket.value 45 | }; 46 | return new Promise(function (resolve, reject) { 47 | agent.post(BASE_URL + '/get_suite_token').send(data).end(wrapper(resolve, reject)); 48 | }) 49 | }); 50 | }; 51 | 52 | Api.prototype.getLatestToken = function () { 53 | var self = this; 54 | if (!self.token_cache) { 55 | return self.getToken().then(function (token) { 56 | if (!token) { 57 | var now = Date.now(); 58 | return self._get_access_token().then(function (token) { 59 | self.token_cache = { 60 | value: token.suite_access_token, 61 | expires: now + self.token_expires_in 62 | }; 63 | self.saveToken(self.token_cache); 64 | return self.token_cache; 65 | }); 66 | } 67 | self.token_cache = token; 68 | return self.getLatestToken(); 69 | }); 70 | } else { 71 | 72 | var now = Date.now(); 73 | if (self.token_cache.expires <= now) { 74 | return self._get_access_token().then(function (token) { 75 | self.token_cache = { 76 | value: token.suite_access_token, 77 | expires: now + self.token_expires_in 78 | }; 79 | self.saveToken(self.token_cache); 80 | return self.token_cache; 81 | }); 82 | } else { 83 | return Promise.resolve(this.token_cache); 84 | } 85 | } 86 | } 87 | 88 | Api.prototype.getPermanentCode = function(tmp_auth_code) { 89 | var self = this; 90 | return self.getLatestToken().then(function (token) { 91 | return new Promise(function (resolve, reject) { 92 | agent.post(BASE_URL + '/get_permanent_code').query({suite_access_token: token.value}).send({tmp_auth_code: tmp_auth_code}).end(wrapper(resolve, reject)); 93 | }) 94 | }); 95 | } 96 | 97 | Api.prototype.getCorpToken = function(auth_corpid, permanent_code) { 98 | var self = this; 99 | return self.getLatestToken().then(function(token){ 100 | return new Promise(function (resolve, reject) { 101 | agent.post(BASE_URL + '/get_corp_token').query({suite_access_token: token.value}).send({ 102 | auth_corpid: auth_corpid, 103 | permanent_code: permanent_code 104 | }).end(wrapper(resolve, reject)); 105 | }) 106 | }) 107 | } 108 | 109 | Api.prototype.getAuthInfo = function(auth_corpid, permanent_code) { 110 | var self = this; 111 | return self.getLatestToken().then(function(token) { 112 | return new Promise(function (resolve, reject) { 113 | agent.post(BASE_URL + '/get_auth_info') 114 | .query({suite_access_token: token.value}) 115 | .send({suite_key: self.suite_key, auth_corpid: auth_corpid, permanent_code: permanent_code}) 116 | .end(wrapper(resolve, reject)); 117 | }) 118 | }); 119 | } 120 | 121 | Api.prototype.getAgent = function(agentid, auth_corpid, permanent_code) { 122 | var self = this; 123 | return self.getLatestToken().then(function (token) { 124 | return new Promise(function (resolve, reject) { 125 | agent.post(BASE_URL + '/get_agent') 126 | .query({suite_access_token: token.value}) 127 | .send({ 128 | suite_key: self.suite_key, 129 | auth_corpid: auth_corpid, 130 | permanent_code: permanent_code, 131 | agentid: agentid 132 | }) 133 | .end(wrapper(resolve, reject)); 134 | }) 135 | }); 136 | } 137 | 138 | Api.prototype.activateSuite = function(auth_corpid, permanent_code) { 139 | var self = this; 140 | return self.getLatestToken().then(function (token) { 141 | return new Promise(function (resolve, reject) { 142 | agent.post(BASE_URL + '/activate_suite') 143 | .query({suite_access_token: token.value}) 144 | .send({suite_key: self.suite_key, auth_corpid: auth_corpid, permanent_code: permanent_code}) 145 | .end(wrapper(resolve, reject)); 146 | }) 147 | }); 148 | } 149 | 150 | Api.prototype.setCorpIpwhitelist = function(auth_corpid, ip_whitelist) { 151 | var self = this; 152 | return self.getLatestToken().then(function (token) { 153 | return new Promise(function (resolve, reject) { 154 | agent.post(BASE_URL + '/set_corp_ipwhitelist') 155 | .query({suite_access_token: token.value}) 156 | .send({suite_key: self.suite_key, auth_corpid: auth_corpid, ip_whitelist: ip_whitelist}) 157 | .end(wrapper(resolve, reject)); 158 | }) 159 | }); 160 | } 161 | 162 | //对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 163 | function wrapper (resolve,reject) { 164 | return function (err, data) { 165 | if (err) { 166 | err.name = 'DingTalkAPI' + err.name; 167 | return reject(err); 168 | } 169 | data = data.body; 170 | if (data.errcode) { 171 | err = new Error(data.errmsg); 172 | err.name = 'DingTalkAPIError'; 173 | err.code = data.errcode; 174 | return reject(err, data); 175 | } 176 | resolve(data); 177 | }; 178 | }; 179 | 180 | module.exports = Api; -------------------------------------------------------------------------------- /lib/dingtalk_suite_callback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by along on 16/6/21. 3 | */ 4 | var DingTalkCrypt = require('./dingtalk_crypt'); 5 | 6 | 7 | //钉钉文档:http://ddtalk.github.io/dingTalkDoc/?spm=a3140.7785475.0.0.p5bAUd#2-回调接口(分为五个回调类型) 8 | module.exports = function(config, callback) { 9 | var dingCrypt = new DingTalkCrypt(config.token, config.encodingAESKey, config.suiteid || 'suite4xxxxxxxxxxxxxxx'); 10 | 11 | var TICKET_EXPIRES_IN = config.ticket_expires_in || 1000 * 60 * 20 //20分钟 12 | return function(req, res, next) { 13 | 14 | var signature = req.query.signature; 15 | var timestamp = req.query.timestamp; 16 | var nonce = req.query.nonce; 17 | var encrypt = req.body.encrypt; 18 | 19 | if (signature !== dingCrypt.getSignature(timestamp, nonce, encrypt)) { 20 | return res.status(401).end('Invalid signature'); 21 | } 22 | 23 | var result = dingCrypt.decrypt(encrypt); 24 | var message = JSON.parse(result.message); 25 | 26 | if (message.EventType === 'check_update_suite_url' || message.EventType === 'check_create_suite_url') { //创建套件第一步,验证有效性。 27 | var Random = message.Random; 28 | result = _jsonWrapper(timestamp, nonce, Random); 29 | res.json(result); 30 | 31 | } else { 32 | res.reply = function() { //返回加密后的success 33 | result = _jsonWrapper(timestamp, nonce, 'success'); 34 | res.json(result); 35 | } 36 | 37 | if (config.saveTicket && message.EventType === 'suite_ticket') { 38 | var data = { 39 | value: message.SuiteTicket, 40 | expires: Number(message.TimeStamp) + TICKET_EXPIRES_IN 41 | } 42 | config.saveTicket(data); 43 | res.reply(); 44 | }else{ 45 | callback(message, req, res, next); 46 | } 47 | }; 48 | } 49 | 50 | function _jsonWrapper(timestamp, nonce, text) { 51 | var encrypt = dingCrypt.encrypt(text); 52 | var msg_signature = dingCrypt.getSignature(timestamp, nonce, encrypt); //新签名 53 | return { 54 | msg_signature: msg_signature, 55 | encrypt: encrypt, 56 | timeStamp: timestamp, 57 | nonce: nonce 58 | }; 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dingtalk-isv", 3 | "version": "1.0.4", 4 | "main": "index", 5 | "keywords": [ 6 | "dingtalk", 7 | "suite", 8 | "ISV", 9 | "nodejs" 10 | ], 11 | "author": { 12 | "name": "along" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/baipgydx729/dingtalk-isv.git" 17 | }, 18 | "dependencies": { 19 | "superagent": "1.6.1" 20 | }, 21 | "licenses": "MIT", 22 | "email": "bpd729@gmail.com", 23 | "description": "钉钉套件回调URL处理、钉钉套件主动调用API", 24 | "bugs": { 25 | "url": "https://github.com/baipgydx729/dingtalk-isv/issues" 26 | } 27 | } --------------------------------------------------------------------------------