| 1 | | /*! |
| 2 | | * top - lib/client.js |
| 3 | | * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com> |
| 4 | | * MIT Licensed |
| 5 | | */ |
| 6 | | |
| 7 | | /** |
| 8 | | * Module dependencies. |
| 9 | | */ |
| 10 | | |
| 11 | 1 | var urllib = require('urllib'); |
| 12 | 1 | var crypto = require('crypto'); |
| 13 | 1 | var moment = require('moment'); |
| 14 | 1 | var Agent = require('agentkeepalive'); |
| 15 | 1 | var HttpsAgent = Agent.HttpsAgent; |
| 16 | | |
| 17 | 1 | exports.Client = Client; |
| 18 | | |
| 19 | | /** |
| 20 | | * API Client. |
| 21 | | * |
| 22 | | * @param {Object} options, must set `appkey` and `appsecret`. |
| 23 | | * @constructor |
| 24 | | */ |
| 25 | 1 | function Client(options) { |
| 26 | 5 | if (!(this instanceof Client)) { |
| 27 | 0 | return new Client(options); |
| 28 | | } |
| 29 | 5 | options = options || {}; |
| 30 | 5 | if (!options.appkey || !options.appsecret) { |
| 31 | 3 | throw new Error('appkey or appsecret need!'); |
| 32 | | } |
| 33 | 2 | this.REST_URL = options.REST_URL || 'http://gw.api.taobao.com/router/rest'; |
| 34 | 2 | this.appkey = options.appkey; |
| 35 | 2 | this.appsecret = options.appsecret; |
| 36 | 2 | var isHttps = this.REST_URL.indexOf('https://') === 0; |
| 37 | 2 | if (isHttps) { |
| 38 | 0 | this.agent = new HttpsAgent(); |
| 39 | | } else { |
| 40 | 2 | this.agent = new Agent(); |
| 41 | | } |
| 42 | 2 | this.agent.maxSockets = 100; |
| 43 | | } |
| 44 | | |
| 45 | | /** |
| 46 | | * Get taobao user by nick. |
| 47 | | * |
| 48 | | * @param {Object} params, `nick` or `session` must set one. |
| 49 | | * - {String} fields, (required)fields of user. |
| 50 | | * - {String} nick, (optional)user nick name. |
| 51 | | * - {String} session, (optional)session id. |
| 52 | | * @param {Function(err, user)} callback |
| 53 | | * @public |
| 54 | | */ |
| 55 | 1 | Client.prototype.taobao_user_get = function (params, callback) { |
| 56 | 3 | var keys = [ 'fields' ]; |
| 57 | 3 | if (!params.session) { |
| 58 | 3 | keys.push('nick'); |
| 59 | | } |
| 60 | 3 | checkRequired(params, keys); |
| 61 | 2 | this.invoke('taobao.user.get', params, ['user_get_response', 'user'], null, 'GET', callback); |
| 62 | | }; |
| 63 | | |
| 64 | | /** |
| 65 | | * Get taobao users by nick list. |
| 66 | | * |
| 67 | | * @param {Object} params |
| 68 | | * - {String} fields, (required)fields of user. |
| 69 | | * - {String} nicks, (required)user nick name list, separate by (,), e.g.: mk2,aerdeng |
| 70 | | * @param {Function(err, users)} callback |
| 71 | | * @public |
| 72 | | */ |
| 73 | 1 | Client.prototype.taobao_users_get = function (params, callback) { |
| 74 | 5 | checkRequired(params, [ 'fields', 'nicks' ]); |
| 75 | 4 | this.invoke('taobao.users.get', params, |
| 76 | | ['users_get_response', 'users', 'user'], [], 'GET', callback); |
| 77 | | }; |
| 78 | | |
| 79 | | /** |
| 80 | | * tmall.selected.items.search 天ç«ç±»ç®ç²¾éåååº |
| 81 | | * |
| 82 | | * @see http://api.taobao.com/apidoc/api.htm?path=cid:10240-apiId:11116 |
| 83 | | * @param {Object} params |
| 84 | | * - {Number|String} cid |
| 85 | | * @param {Function(err, items)} callback |
| 86 | | * - {Array} items [{ |
| 87 | | * cid: 1101, |
| 88 | | * num_iid: 13088700250, |
| 89 | | * shop_id: 59227746, |
| 90 | | * item_score: "67.33659988217163" |
| 91 | | * }, ...] |
| 92 | | * @public |
| 93 | | */ |
| 94 | 1 | Client.prototype.tmall_selected_items_search = function (params, callback) { |
| 95 | 1 | checkRequired(params, ['cid']); |
| 96 | 1 | this.invoke('tmall.selected.items.search', params, |
| 97 | | ['tmall_selected_items_search_response', 'item_list', 'selected_item'], [], |
| 98 | | 'GET', callback); |
| 99 | | }; |
| 100 | | |
| 101 | 1 | Client.prototype.taobao_item_get = function (params, callback) { |
| 102 | 1 | checkRequired(params, ['num_iid', 'fields']); |
| 103 | 1 | this.invoke('taobao.item.get', params, |
| 104 | | ['item_get_response', 'item'], {}, 'GET', callback); |
| 105 | | }; |
| 106 | | |
| 107 | | /** |
| 108 | | * Get taobao shop by nick. |
| 109 | | * |
| 110 | | * @see http://api.taobao.com/apidoc/api.htm?spm=0.0.0.34.b8913b&path=cid:9-apiId:68 |
| 111 | | * @param {Object} params |
| 112 | | * - {String} fields, (required)fields of user. |
| 113 | | * - {String} nick, (optional)user nick name. |
| 114 | | * - {String} session, (optional)session id. |
| 115 | | * @param {Function(err, user)} callback |
| 116 | | * @public |
| 117 | | */ |
| 118 | 1 | Client.prototype.taobao_shop_get = function (params, callback) { |
| 119 | 2 | checkRequired(params, ['nick', 'fields']); |
| 120 | 2 | this.invoke('taobao.shop.get', params, |
| 121 | | ['shop_get_response', 'shop'], null, 'GET', callback); |
| 122 | | }; |
| 123 | | |
| 124 | | /** |
| 125 | | * Invoke an api by method name. |
| 126 | | * |
| 127 | | * @param {String} method, method name |
| 128 | | * @param {Object} params |
| 129 | | * @param {Array} reponseNames, e.g. ['tmall_selected_items_search_response', 'tem_list', 'selected_item'] |
| 130 | | * @param {Object} defaultResponse |
| 131 | | * @param {String} type |
| 132 | | * @param {Function(err, response)} callback |
| 133 | | */ |
| 134 | 1 | Client.prototype.invoke = function (method, params, reponseNames, defaultResponse, type, callback) { |
| 135 | 12 | params.method = method; |
| 136 | 12 | this.request(params, type, function (err, result) { |
| 137 | 12 | if (err) { |
| 138 | 2 | return callback(err); |
| 139 | | } |
| 140 | 10 | var response = result; |
| 141 | 10 | if (reponseNames && reponseNames.length > 0) { |
| 142 | 10 | for (var i = 0; i < reponseNames.length; i++) { |
| 143 | 20 | var name = reponseNames[i]; |
| 144 | 20 | response = response[name]; |
| 145 | 20 | if (response === undefined) { |
| 146 | 4 | break; |
| 147 | | } |
| 148 | | } |
| 149 | | } |
| 150 | 10 | if (response === undefined) { |
| 151 | 4 | response = defaultResponse; |
| 152 | | } |
| 153 | 10 | callback(null, response); |
| 154 | | }); |
| 155 | | }; |
| 156 | | |
| 157 | | /** |
| 158 | | * Request API. |
| 159 | | * |
| 160 | | * @param {Object} params |
| 161 | | * @param {String} [type='GET'] |
| 162 | | * @param {Function(err, result)} callback |
| 163 | | * @public |
| 164 | | */ |
| 165 | 1 | Client.prototype.request = function (params, type, callback) { |
| 166 | 15 | checkRequired(params, 'method'); |
| 167 | 14 | if (typeof type === 'function') { |
| 168 | 2 | callback = type; |
| 169 | 2 | type = null; |
| 170 | | } |
| 171 | 14 | var args = { |
| 172 | | timestamp: this.timestamp(), |
| 173 | | format: 'json', |
| 174 | | app_key: this.appkey, |
| 175 | | v: '2.0', |
| 176 | | sign_method: 'md5' |
| 177 | | }; |
| 178 | 14 | for (var k in params) { |
| 179 | 39 | args[k] = params[k]; |
| 180 | | } |
| 181 | 14 | args.sign = this.sign(args); |
| 182 | 14 | type = type || 'GET'; |
| 183 | 14 | urllib.request(this.REST_URL, {type: type, data: args, agent: this.agent}, |
| 184 | | function (err, buffer) { |
| 185 | 14 | var data; |
| 186 | 14 | if (buffer) { |
| 187 | 13 | try { |
| 188 | 13 | data = JSON.parse(buffer); |
| 189 | | } catch (e) { |
| 190 | 1 | err = e; |
| 191 | 1 | e.data = buffer.toString(); |
| 192 | 1 | data = null; |
| 193 | | } |
| 194 | | } |
| 195 | 14 | if (data && data.error_response && !data.error_response.sub_msg) { |
| 196 | | // sub_msg error, let caller handle it. |
| 197 | | // request() only handle root error, like code 40, 41 error. |
| 198 | 1 | var msg = data.error_response.msg; |
| 199 | 1 | err = new Error(data.error_response.code + ': ' + msg); |
| 200 | 1 | err.code = data.error_response.code; |
| 201 | 1 | err.data = buffer.toString(); |
| 202 | 1 | data = null; |
| 203 | | } |
| 204 | 14 | callback(err, data); |
| 205 | | }, this); |
| 206 | | }; |
| 207 | | |
| 208 | | /** |
| 209 | | * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format. |
| 210 | | * @return {String} |
| 211 | | */ |
| 212 | 1 | Client.prototype.timestamp = function () { |
| 213 | 15 | return moment().format('YYYY-MM-DD HH:mm:ss'); |
| 214 | | }; |
| 215 | | |
| 216 | | /** |
| 217 | | * Sign API request. |
| 218 | | * see http://open.taobao.com/doc/detail.htm?id=111#s6 |
| 219 | | * |
| 220 | | * @param {Object} params |
| 221 | | * @return {String} sign string |
| 222 | | */ |
| 223 | 1 | Client.prototype.sign = function (params) { |
| 224 | 15 | var sorted = Object.keys(params).sort(); |
| 225 | 15 | var basestring = this.appsecret; |
| 226 | | // (md5(secretkey1value1key2value2...secret)) |
| 227 | 15 | for (var i = 0, l = sorted.length; i < l; i++) { |
| 228 | 117 | var k = sorted[i]; |
| 229 | 117 | basestring += k + params[k]; |
| 230 | | } |
| 231 | 15 | basestring += this.appsecret; |
| 232 | 15 | return md5(basestring); |
| 233 | | }; |
| 234 | | |
| 235 | 1 | function checkRequired(params, keys) { |
| 236 | 27 | if (!Array.isArray(keys)) { |
| 237 | 15 | keys = [keys]; |
| 238 | | } |
| 239 | 27 | for (var i = 0, l = keys.length; i < l; i++) { |
| 240 | 38 | var k = keys[i]; |
| 241 | 38 | if (params[k] === undefined) { |
| 242 | 3 | throw new Error('`' + k + '` required'); |
| 243 | | } |
| 244 | | } |
| 245 | | } |
| 246 | | |
| 247 | | /** |
| 248 | | * MD5 hex upper case string. |
| 249 | | * |
| 250 | | * @param {String|Buffer} s |
| 251 | | * @return {String} |
| 252 | | */ |
| 253 | 1 | function md5(s) { |
| 254 | 15 | var hash = crypto.createHash('md5'); |
| 255 | 15 | hash.update(Buffer.isBuffer(s) ? s : new Buffer(s)); |
| 256 | 15 | return hash.digest('hex').toUpperCase(); |
| 257 | | } |