-
Notifications
You must be signed in to change notification settings - Fork 663
/
client.js
216 lines (180 loc) · 6.2 KB
/
client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/**
*
*/
var ConsoleTransport = require('winston').transports.Console;
var EventEmitter = require('eventemitter3');
var async = require('async');
var bind = require('lodash').bind;
var inherits = require('inherits');
var retry = require('retry');
var urlJoin = require('url-join');
var winston = require('winston');
var WEB_CLIENT_EVENTS = require('./events/client').WEB;
var helpers = require('./helpers');
var requestsTransport = require('./transports/request').requestTransport;
/**
* Base client for both the RTM and web APIs.
* @param {string} token The Slack API token to use with this client.
* @param {Object} opts
* @param {String} opts.slackAPIUrl The Slack API URL.
* @param {String} opts.userAgent The user-agent to use, defaults to node-slack.
* @param {Function} opts.transport Function to call to make an HTTP call to the Slack API.
* @param {string=} opts.logLevel The log level for the logger.
* @param {Function=} opts.logger Function to use for log calls, takes (logLevel, logString) params.
* @param {Number} opts.maxRequestConcurrency The max # of concurrent requests to make to Slack's
* API's, defaults to 5.
* @constructor
*/
function BaseAPIClient(token, opts) {
EventEmitter.call(this);
/**
* @type {string}
* @private
*/
this._token = token;
/** @type {string} */
this.slackAPIUrl = opts.slackAPIUrl || 'https://slack.com/api/';
/** @type {Function} */
this.transport = opts.transport || requestsTransport;
/** @type {string} */
this.userAgent = opts.userAgent || 'node-slack';
/**
*
* @type {Object}
* @private
*/
this._requestQueue = async.priorityQueue(
bind(this._callTransport, this),
opts.maxRequestConcurrency
);
/**
* The logger function attached to this client.
* @type {Function}
*/
this.logger = opts.logger || new winston.Logger({
level: opts.logLevel || 'info',
transports: [new ConsoleTransport()],
});
this._createFacets();
}
inherits(BaseAPIClient, EventEmitter);
BaseAPIClient.prototype.emit = function emit() {
BaseAPIClient.super_.prototype.emit.apply(this, arguments);
this.logger.debug(arguments);
};
/**
* Initializes each of the API facets.
* @protected
*/
BaseAPIClient.prototype._createFacets = function _createFacets() {
};
/**
* Attaches a data-store to the client instance.
* @param {SlackDataStore} dataStore
*/
BaseAPIClient.prototype.registerDataStore = function registerDataStore(dataStore) {
this.dataStore = dataStore;
};
/**
* Calls the supplied transport function and processes the results.
*
* This will also manage 429 responses and retry failed operations.
*
* @param {object} task The arguments to pass to the transport.
* @param {function} queueCb Callback to signal to the request queue that the request has completed.
* @protected
*/
BaseAPIClient.prototype._callTransport = function _callTransport(task, queueCb) {
// TODO(leah): Add some logging to this function as it's kind of complex
var args = task.args;
var cb = task.cb;
var _this = this;
var retryOp = retry.operation(this.retryConfig);
var handleTransportResponse = function handleTransportResponse(err, headers, statusCode, body) {
var headerSecs;
var headerMs;
var httpErr;
var jsonResponse;
var jsonParseErr;
if (err) {
if (!retryOp.retry(err)) {
cb(retryOp.mainError(), null);
} else {
return;
}
}
// NOTE: this assumes that non-200 codes simply won't happen, as the Slack API policy is to
// return a 200 with an error property
if (statusCode !== 200) {
// There are only a couple of possible bad cases here:
// - 429: the application is being rate-limited. The client is designed to automatically
// respect this
// - 4xx or 5xx: something bad, but probably recoverable, has happened, so requeue the
// request
if (statusCode === 429) {
_this._requestQueue.pause();
headerSecs = parseInt(headers['Retry-After'], 10);
headerMs = headerSecs * 1000;
setTimeout(function retryRateLimitedRequest() {
// Don't retry limit requests that were rejected due to retry-after
_this.transport(args, handleTransportResponse);
_this._requestQueue.resume();
}, headerMs);
_this.emit(WEB_CLIENT_EVENTS.RATE_LIMITED, headerSecs);
} else {
// If this is reached, it means an error outside the normal error logic was received. These
// should be very unusual as standard errors come back with a 200 code and an "error"
// property.
//
// Given that, assume that something really weird happened and retry the request as normal.
httpErr = new Error('Unable to process request, received bad ' + statusCode + ' error');
if (!retryOp.retry(httpErr)) {
cb(httpErr, null);
} else {
return;
}
}
} else {
try {
jsonResponse = JSON.parse(body);
} catch (parseErr) {
// TODO(leah): Emit an event here?
jsonParseErr = new Error('unable to parse Slack API Response');
}
try {
cb(jsonParseErr, jsonResponse);
} catch (callbackErr) {
// Never retry requests that fail in the callback
_this.logger.error(callbackErr);
}
}
// This is always an empty callback, even if there's an error, as it's used to signal the
// request queue that a request has completed processing, and nothing else.
queueCb();
};
retryOp.attempt(function attemptTransportCall() {
_this.transport(args, handleTransportResponse);
});
};
/**
* Makes a call to the Slack API.
*
* @param {String} endpoint The API endpoint to send to.
* @param {Object=} optData The data send to the Slack API.
* @param {function} optCb The callback to run on completion.
*/
BaseAPIClient.prototype.makeAPICall = function makeAPICall(endpoint, optData, optCb) {
var apiCallArgs = helpers.getAPICallArgs(this._token, optData, optCb);
var args = {
url: urlJoin(this.slackAPIUrl, endpoint),
data: apiCallArgs.data,
headers: {
'User-Agent': this.userAgent,
},
};
this._requestQueue.push({
args: args,
cb: apiCallArgs.cb,
});
};
module.exports = BaseAPIClient;