UNPKG

mastodon-api

Version:

Mastodon API library with streaming support

422 lines (369 loc) 18.7 kB
'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _assert = require('assert'); var _assert2 = _interopRequireDefault(_assert); var _util = require('util'); var _util2 = _interopRequireDefault(_util); var _oauth = require('oauth'); var _request = require('request'); var _request2 = _interopRequireDefault(_request); var _helpers = require('./helpers'); var _helpers2 = _interopRequireDefault(_helpers); var _streamingApiConnection = require('./streaming-api-connection'); var _streamingApiConnection2 = _interopRequireDefault(_streamingApiConnection); var _settings = require('./settings'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Mastodon = function () { function Mastodon(config) { _classCallCheck(this, Mastodon); this.apiUrl = config.api_url || _settings.DEFAULT_REST_ROOT; Mastodon._validateConfigOrThrow(config); this.config = config; this._mastodon_time_minus_local_time_ms = 0; } _createClass(Mastodon, [{ key: 'get', value: function get(path, params, callback) { return this.request('GET', path, params, callback); } }, { key: 'post', value: function post(path, params, callback) { return this.request('POST', path, params, callback); } }, { key: 'delete', value: function _delete(path, params, callback) { return this.request('DELETE', path, params, callback); } }, { key: 'request', value: function request(method, path, params, callback) { var self = this; (0, _assert2.default)(method === 'GET' || method === 'POST' || method === 'DELETE'); // if no `params` is specified but a callback is, use default params if (typeof params === 'function') { callback = params; params = {}; } return new Promise(function (resolve, reject) { var _returnErrorToUser = function _returnErrorToUser(err) { if (callback && typeof callback === 'function') { callback(err, null, null); } reject(err); }; self._buildRequestOptions(method, path, params, function (err, requestOptions) { if (err) { _returnErrorToUser(err); return; } var mastodonOptions = params && params.masto_options || {}; process.nextTick(function () { // ensure all HTTP i/o occurs after the user // has a chance to bind their event handlers Mastodon._doRESTAPIRequest(requestOptions, mastodonOptions, method, function (reqerr, parsedBody, response) { self._updateClockOffsetFromResponse(response); if (self.config.trusted_cert_fingerprints) { if (!response.socket.authorized) { // The peer certificate was not signed // by one of the authorized CA's. var authErrMsg = response.socket.authorizationError.toString(); var merr = _helpers2.default.makeMastodonError('The peer certificate was not signed; ' + authErrMsg); _returnErrorToUser(merr); return; } var fingerprint = response.socket.getPeerCertificate().fingerprint; var trustedFingerprints = self.config.trusted_cert_fingerprints; if (!trustedFingerprints.includes(fingerprint)) { var errMsg = _util2.default.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.', trustedFingerprints.join(','), fingerprint); var _merr = new Error(errMsg); _returnErrorToUser(_merr); return; } } if (callback && typeof callback === 'function') { callback(reqerr, parsedBody, response); } resolve({ data: parsedBody, resp: response }); }); }); }); }); } }, { key: '_updateClockOffsetFromResponse', value: function _updateClockOffsetFromResponse(response) { if (response && response.headers && response.headers.date) { var date = new Date(response.headers.date); if (date.toString() === 'Invalid Date') return; this._mastodon_time_minus_local_time_ms = date.getTime() - Date.now(); } } /** * Builds and returns an options object ready to pass to `request()` * @param {String} method "GET", "POST", or "DELETE" * @param {String} path REST API resource uri (eg. "statuses/destroy/:id") * @param {Object} params user's params object * @param {Function} callback * @returns {Undefined} * * Calls `callback` with Error, Object * where Object is an options object ready to pass to `request()`. * * Returns error raised (if any) by `helpers.moveParamsIntoPath()` */ }, { key: '_buildRequestOptions', value: function _buildRequestOptions(method, path, params, callback) { var finalParams = params || {}; delete finalParams.mastodon_options; // the options object passed to `request` used to perform the HTTP request var requestOptions = { headers: { Accept: '*/*', 'User-Agent': 'node-mastodon-client', Authorization: 'Bearer ' + this.config.access_token }, gzip: true, encoding: null }; if (typeof this.config.timeout_ms !== 'undefined') { requestOptions.timeout_ms = this.config.timeout_ms; } try { // finalize the `path` value by building it using user-supplied params path = _helpers2.default.moveParamsIntoPath(finalParams, path); } catch (e) { callback(e, null, null); return; } if (path.match(/^https?:\/\//i)) { // This is a full url request requestOptions.url = path; } else { // This is a REST API request. requestOptions.url = '' + this.apiUrl + path; } if (finalParams.file) { // If we're sending a file requestOptions.headers['Content-type'] = 'multipart/form-data'; requestOptions.formData = finalParams; } else if (Object.keys(finalParams).length > 0) { // Non-file-upload params should be url-encoded requestOptions.url += Mastodon.formEncodeParams(finalParams); } callback(null, requestOptions); } /** * Make HTTP request to Mastodon REST API. * * @param {Object} requestOptions * @param {Object} mastodonOptions * @param {String} method "GET", "POST", or "DELETE" * @param {Function} callback * @private */ }, { key: 'stream', value: function stream(path, params) { var mastodonOptions = params && params.mastodon_options || {}; var streamingConnection = new _streamingApiConnection2.default(); this._buildRequestOptions('GET', path, params, function (err, requestOptions) { if (err) { // surface this on the streamingConnection instance // (where a user may register their error handler) streamingConnection.emit('error', err); return; } // set the properties required to start the connection streamingConnection.requestOptions = requestOptions; streamingConnection.mastodonOptions = mastodonOptions; process.nextTick(function () { streamingConnection.start(); }); }); return streamingConnection; } }, { key: 'auth', set: function set(auth) { var self = this; _settings.REQUIRED_KEYS_FOR_AUTH.forEach(function (k) { if (auth[k]) { self.config[k] = auth[k]; } }); }, get: function get() { return this.config; } }], [{ key: '_doRESTAPIRequest', value: function _doRESTAPIRequest(requestOptions, mastodonOptions, method, callback) { var requestMethod = _request2.default[method.toLowerCase()]; var request = requestMethod(requestOptions); var body = ''; var response = void 0; request.on('response', function (res) { response = res; // read data from `request` object which contains the decompressed HTTP response body, // `response` is the unmodified http.IncomingMessage object // which may contain compressed data request.on('data', function (chunk) { body += chunk.toString('utf8'); }); // we're done reading the response request.on('end', function () { if (body !== '') { try { body = JSON.parse(body); } catch (jsonDecodeError) { // there was no transport-level error, // but a JSON object could not be decoded from the request body // surface this to the caller var err = _helpers2.default.makeMastodonError('JSON decode error: Mastodon HTTP response body was not valid JSON'); err.statusCode = response ? response.statusCode : null; err.allErrors.concat({ error: jsonDecodeError.toString() }); callback(err, body, response); return; } } if ((typeof body === 'undefined' ? 'undefined' : _typeof(body)) === 'object' && (body.error || body.errors)) { // we got a Mastodon API-level error response // place the errors in the HTTP response body // into the Error object and pass control to caller var _err = _helpers2.default.makeMastodonError('Mastodon API Error'); _err.statusCode = response ? response.statusCode : null; _err = _helpers2.default.attachBodyInfoToError(_err, body); callback(_err, body, response); return; } // success case - no errors in HTTP response body callback(null, body, response); }); request.on('error', function (err) { // transport-level error occurred - likely a socket error if (mastodonOptions.retry && _settings.STATUS_CODES_TO_ABORT_ON.includes(err.statusCode)) { // retry the request since retries were specified // and we got a status code we should retry on // FIXME // this.request(method, path, params, callback); } else { // pass the transport-level error to the caller err.statusCode = null; err.code = null; err.allErrors = []; err = _helpers2.default.attachBodyInfoToError(err, body); callback(err, body, response); } }); }); } }, { key: 'formEncodeParams', value: function formEncodeParams(params, noQuestionMark) { var encoded = ''; Object.keys(params).forEach(function (key) { var value = params[key]; if (encoded === '' && !noQuestionMark) { encoded = '?'; } else { encoded += '&'; } if (Array.isArray(value)) { value.forEach(function (v) { encoded += encodeURIComponent(key) + '[]=' + encodeURIComponent(v) + '&'; }); } else { encoded += encodeURIComponent(key) + '=' + encodeURIComponent(value); } }); return encoded; } }, { key: '_validateConfigOrThrow', value: function _validateConfigOrThrow(config) { if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) !== 'object') { throw new TypeError('config must be object, got ' + (typeof config === 'undefined' ? 'undefined' : _typeof(config)) + '.'); } if (typeof config.timeout_ms !== 'undefined' && isNaN(Number(config.timeout_ms))) { throw new TypeError('config parameter \'timeout_ms\' must be a Number, got ' + config.timeout_ms + '.'); } _settings.REQUIRED_KEYS_FOR_AUTH.forEach(function (reqKey) { if (!config[reqKey]) { throw new Error('Mastodon config must include \'' + reqKey + '\' when using \'user_auth\''); } }); } }, { key: 'createOAuthApp', value: function createOAuthApp() { var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _settings.DEFAULT_OAUTH_APPS_ENDPOINT; var clientName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'mastodon-node'; var scopes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'read write follow'; var redirectUri = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'urn:ietf:wg:oauth:2.0:oob'; return new Promise(function (resolve, reject) { _request2.default.post({ url: url, form: { client_name: clientName, redirect_uris: redirectUri, scopes: scopes } }, function (err, res, body) { if (err) { reject(err); return; } try { body = JSON.parse(body); } catch (e) { reject(new Error('Error parsing body ' + body)); } resolve(body); }); }); } }, { key: 'getAuthorizationUrl', value: function getAuthorizationUrl(clientId, clientSecret) { var baseUrl = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _settings.DEFAULT_REST_BASE; var scope = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'read write follow'; var redirectUri = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'urn:ietf:wg:oauth:2.0:oob'; return new Promise(function (resolve) { var oauth = new _oauth.OAuth2(clientId, clientSecret, baseUrl, null, '/oauth/token'); var url = oauth.getAuthorizeUrl({ redirect_uri: redirectUri, response_type: 'code', client_id: clientId, scope: scope }); resolve(url); }); } }, { key: 'getAccessToken', value: function getAccessToken(clientId, clientSecret, authorizationCode) { var baseUrl = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _settings.DEFAULT_REST_BASE; var redirectUri = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'urn:ietf:wg:oauth:2.0:oob'; return new Promise(function (resolve, reject) { var oauth = new _oauth.OAuth2(clientId, clientSecret, baseUrl, null, '/oauth/token'); oauth.getOAuthAccessToken(authorizationCode, { grant_type: 'authorization_code', redirect_uri: redirectUri }, function (err, accessToken /* , refreshToken, res */) { if (err) { reject(err); return; } resolve(accessToken); }); }); } }]); return Mastodon; }(); module.exports = Mastodon;