From 3906b27e0a8ce9bc1e6d54ee52fc65590c58c4f2 Mon Sep 17 00:00:00 2001 From: pluralcafe-docker Date: Wed, 22 Aug 2018 07:10:06 +0000 Subject: Use insecure mode for docker --- index.js | 2 +- node-mastodon/LICENSE | 22 +++ node-mastodon/README.md | 100 +++++++++++++ node-mastodon/lib/helpers.js | 65 ++++++++ node-mastodon/lib/mastodon.js | 340 ++++++++++++++++++++++++++++++++++++++++++ node-mastodon/lib/settings.js | 2 + 6 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 node-mastodon/LICENSE create mode 100644 node-mastodon/README.md create mode 100644 node-mastodon/lib/helpers.js create mode 100644 node-mastodon/lib/mastodon.js create mode 100644 node-mastodon/lib/settings.js diff --git a/index.js b/index.js index 5f99134..268481e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -var mastodon = require('mastodon'); +var mastodon = require('./node-mastodon/lib/mastodon'); var pg = require('pg'); var DB_USER = process.env.DB_USER || 'ambassador'; diff --git a/node-mastodon/LICENSE b/node-mastodon/LICENSE new file mode 100644 index 0000000..29b5e63 --- /dev/null +++ b/node-mastodon/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright for portions of project node-mastodon are held by Tolga Tezel as part of project Twit. +All other copyright for project node-mastodon are held by Jessica Hayley, 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node-mastodon/README.md b/node-mastodon/README.md new file mode 100644 index 0000000..2b30fa7 --- /dev/null +++ b/node-mastodon/README.md @@ -0,0 +1,100 @@ +# node-mastodon + +[Mastodon](https://github.com/tootsuite/mastodon/) API Client for node + +# Installing + +``` +npm install mastodon +``` + +## Usage: + +```javascript +var Masto = require('mastodon') + +var M = new Masto({ + access_token: '...', + timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests. + api_url: 'https://gay.crime.team/api/v1/', // optional, defaults to https://mastodon.social/api/v1/ +}) +``` + +# node-mastodon API: + +## `var M = new Masto(config)` + +Create a `Mastodon` instance that can be used to make requests to Mastodon's APIs. Currently only supports oauth2 access tokens (no username/password auth) for security reasons. + +I advise that you use the [oauth](https://www.npmjs.com/package/oauth) package to get the user's access_token. More information about how to do that is [on the wiki](https://github.com/jessicahayley/node-mastodon/wiki/Getting-an-access_token-with-the-oauth-package). +You'll need to [register your app](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#oauth-apps) on Mastodon first as well. + +If authenticating with user context, `config` should be an object of the form: +``` +{ + access_token: '...' +} +``` + +## `M.get(path, [params], callback)` +GET any of the REST API endpoints. + +**path** + +The endpoint to hit. + +**params** + +(Optional) parameters for the request. + +**callback** + +`function (err, data, response)` + +- `data` is the parsed data received from Mastodon. +- `response` is the [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) received from Mastodon. + +## `M.post(path, [params], callback)` + +POST any of the REST API endpoints. Same usage as `T.get()`. + +## `M.getAuth()` +Get the client's authentication tokens. + +## `M.setAuth(tokens)` +Update the client's authentication tokens. + +------- + +# Examples + +### Reading the home timeline +```javascript +M.get('timelines/home', {}).then(resp => console.log(resp.data)) +``` + +### Upload an image and attach it to a tweet +```javascript +var id; +M.post('media', { file: fs.createReadStream('path/to/image.png') }).then(resp => { + id = resp.data.id; + M.post('statuses', { status: '#selfie', media_ids: [id] }) +}) +``` + +------- + +# Advanced + +You may specify an array of trusted certificate fingerprints if you want to only trust a specific set of certificates. +When an HTTP response is received, it is verified that the certificate was signed, and the peer certificate's fingerprint must be one of the values you specified. By default, the node.js trusted "root" CAs will be used. + +eg. +```js +var M = new Masto({ + access_token: '...', + trusted_cert_fingerprints: [ + '66:EA:47:62:D9:B1:4F:1A:AE:89:5F:68:BA:6B:8E:BB:F8:1D:BF:8E', + ] +}) +``` diff --git a/node-mastodon/lib/helpers.js b/node-mastodon/lib/helpers.js new file mode 100644 index 0000000..8833968 --- /dev/null +++ b/node-mastodon/lib/helpers.js @@ -0,0 +1,65 @@ +var querystring = require('querystring'); +var request = require('request'); + +/** + * For each `/:param` fragment in path, move the value in params + * at that key to path. If the key is not found in params, throw. + * Modifies both params and path values. + * + * @param {Objet} params Object used to build path. + * @param {String} path String to transform. + * @return {Undefined} + * + */ +exports.moveParamsIntoPath = function (params, path) { + var rgxParam = /\/:(\w+)/g + var missingParamErr = null + + path = path.replace(rgxParam, function (hit) { + var paramName = hit.slice(2) + var suppliedVal = params[paramName] + if (!suppliedVal) { + throw new Error('Mastodon: Params object is missing a required parameter for this request: `'+paramName+'`') + } + var retVal = '/' + suppliedVal + delete params[paramName] + return retVal + }) + return path +} + +/** + * When Mastodon returns a response that looks like an error response, + * use this function to attach the error info in the response body to `err`. + * + * @param {Error} err Error instance to which body info will be attached + * @param {Object} body JSON object that is the deserialized HTTP response body received from Mastodon + * @return {Undefined} + */ +exports.attachBodyInfoToError = function (err, body) { + err.mastodonReply = body; + if (!body) { + return + } + if (body.error) { + // the body itself is an error object + err.message = body.error + err.allErrors = err.allErrors.concat([body]) + } else if (body.errors && body.errors.length) { + // body contains multiple error objects + err.message = body.errors[0].message; + err.code = body.errors[0].code; + err.allErrors = err.allErrors.concat(body.errors) + } +} + +exports.makeMastodonError = function (message) { + var err = new Error() + if (message) { + err.message = message + } + err.code = null + err.allErrors = [] + err.mastodonReply = null + return err +} diff --git a/node-mastodon/lib/mastodon.js b/node-mastodon/lib/mastodon.js new file mode 100644 index 0000000..7f5c8c1 --- /dev/null +++ b/node-mastodon/lib/mastodon.js @@ -0,0 +1,340 @@ +// +// Mastodon API Wrapper +// +var assert = require('assert'); +var Promise = require('bluebird'); +var request = require('request'); +var util = require('util'); +var helpers = require('./helpers'); +var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON; + +var DEFAULT_REST_ROOT = 'https://mastodon.social/api/v1/'; + +var required_for_user_auth = [ + 'access_token', +]; + +// +// Mastodon +// +var Mastodon = function (config) { + if (!(this instanceof Mastodon)) { + return new Mastodon(config); + } + + var self = this + var credentials = { + access_token : config.access_token + } + + this.apiUrl = config.api_url || DEFAULT_REST_ROOT; + + this._validateConfigOrThrow(config); + this.config = config; + this._mastodon_time_minus_local_time_ms = 0; +} + +Mastodon.prototype.get = function (path, params, callback) { + return this.request('GET', path, params, callback) +} + +Mastodon.prototype.post = function (path, params, callback) { + return this.request('POST', path, params, callback) +} + +Mastodon.prototype.patch = function (path, params, callback) { + return this.request('PATCH', path, params, callback) +} + +Mastodon.prototype.delete = function (path, params, callback) { + return this.request('DELETE', path, params, callback) +} + +Mastodon.prototype.request = function (method, path, params, callback) { + var self = this; + assert(method == 'GET' || method == 'POST' || method == 'PATCH' || 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 (err) { + if (callback && typeof callback === 'function') { + callback(err, null, null); + } + reject(err); + } + + self._buildReqOpts(method, path, params, function (err, reqOpts) { + if (err) { + _returnErrorToUser(err); + return + } + + var mastoOptions = (params && params.masto_options) || {}; + + process.nextTick(function () { + // ensure all HTTP i/o occurs after the user has a chance to bind their event handlers + self._doRestApiRequest(reqOpts, mastoOptions, method, function (err, parsedBody, resp) { + if (err) { + _returnErrorToUser(err); + return + } + self._updateClockOffsetFromResponse(resp); + + if (self.config.trusted_cert_fingerprints) { + if (!resp.socket.authorized) { + // The peer certificate was not signed by one of the authorized CA's. + var authErrMsg = resp.socket.authorizationError.toString(); + var err = helpers.makeMastodonError('The peer certificate was not signed; ' + authErrMsg); + _returnErrorToUser(err); + return; + } + var fingerprint = resp.socket.getPeerCertificate().fingerprint; + var trustedFingerprints = self.config.trusted_cert_fingerprints; + if (trustedFingerprints.indexOf(fingerprint) === -1) { + var errMsg = util.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.', + trustedFingerprints.join(','), fingerprint); + var err = new Error(errMsg); + _returnErrorToUser(err); + return; + } + } + + if (callback && typeof callback === 'function') { + callback(err, parsedBody, resp); + } + + resolve({ data: parsedBody, resp: resp }); + return; + }) + }) + }); + }); +} + +Mastodon.prototype._updateClockOffsetFromResponse = function (resp) { + var self = this; + if (resp && resp.headers && resp.headers.date && + new Date(resp.headers.date).toString() !== 'Invalid Date' + ) { + var mastodonTimeMs = new Date(resp.headers.date).getTime() + self._mastodon_time_minus_local_time_ms = mastodonTimeMs - 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 + * @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()` + */ +Mastodon.prototype._buildReqOpts = function (method, path, params, callback) { + var self = this + if (!params) { + params = {} + } + var finalParams = params; + delete finalParams.masto_options + + // the options object passed to `request` used to perform the HTTP request + var reqOpts = { + headers: { + 'Accept': '*/*', + 'User-Agent': 'node-mastodon-client', + 'Authorization': 'Bearer ' + self.config.access_token + }, + gzip: true, + encoding: null, + rejectUnauthorized: false, + insecure: true, + } + + if (typeof self.config.timeout_ms !== 'undefined') { + reqOpts.timeout = self.config.timeout_ms; + } + + try { + // finalize the `path` value by building it using user-supplied params + path = helpers.moveParamsIntoPath(finalParams, path) + } catch (e) { + callback(e, null, null) + return + } + + if (path.match(/^https?:\/\//i)) { + // This is a full url request + reqOpts.url = path + } else { + // This is a REST API request. + reqOpts.url = this.apiUrl + path; + } + + if (finalParams.file) { + // If we're sending a file + reqOpts.headers['Content-type'] = 'multipart/form-data'; + reqOpts.formData = finalParams; + } else { + // Non-file-upload params should be url-encoded + if (Object.keys(finalParams).length > 0) { + reqOpts.url += this.formEncodeParams(finalParams); + } + } + + callback(null, reqOpts); + return; +} + +/** + * Make HTTP request to Mastodon REST API. + * @param {Object} reqOpts options object passed to `request()` + * @param {Object} mastoOptions + * @param {String} method "GET", "POST", or "DELETE" + * @param {Function} callback user's callback + * @return {Undefined} + */ +Mastodon.prototype._doRestApiRequest = function (reqOpts, mastoOptions, method, callback) { + var request_method = request[method.toLowerCase()]; + var req = request_method(reqOpts); + + var body = ''; + var response = null; + + var onRequestComplete = 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 = helpers.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 === '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 = helpers.makeMastodonError('Mastodon API Error') + err.statusCode = response ? response.statusCode: null; + helpers.attachBodyInfoToError(err, body); + callback(err, body, response); + return + } + + // success case - no errors in HTTP response body + callback(err, body, response) + } + + req.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 + req.on('data', function (chunk) { + body += chunk.toString('utf8') + }) + // we're done reading the response + req.on('end', function () { + onRequestComplete() + }) + }) + + req.on('error', function (err) { + // transport-level error occurred - likely a socket error + if (mastoOptions.retry && + STATUS_CODES_TO_ABORT_ON.indexOf(err.statusCode) !== -1 + ) { + // retry the request since retries were specified and we got a status code we should retry on + self.request(method, path, params, callback); + return; + } else { + // pass the transport-level error to the caller + err.statusCode = null + err.code = null + err.allErrors = []; + helpers.attachBodyInfoToError(err, body) + callback(err, body, response); + return; + } + }) +} + +Mastodon.prototype.formEncodeParams = function (params, noQuestionMark) { + var encoded = ''; + for (var key in params) { + var value = params[key]; + if (encoded === '') { + if (!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); +} + +Mastodon.prototype.setAuth = function (auth) { + var self = this + var configKeys = [ + 'access_token' + ]; + + // update config + configKeys.forEach(function (k) { + if (auth[k]) { + self.config[k] = auth[k] + } + }) + this._validateConfigOrThrow(self.config); +} + +Mastodon.prototype.getAuth = function () { + return this.config +} + +// +// Check that the required auth credentials are present in `config`. +// @param {Object} config Object containing credentials for REST API auth +// +Mastodon.prototype._validateConfigOrThrow = function (config) { + //check config for proper format + if (typeof config !== 'object') { + throw new TypeError('config must be object, got ' + typeof config) + } + + if (typeof config.timeout_ms !== 'undefined' && isNaN(Number(config.timeout_ms))) { + throw new TypeError('Mastodon config `timeout_ms` must be a Number. Got: ' + config.timeout_ms + '.'); + } + + var auth_type = 'user auth' + var required_keys = required_for_user_auth + + required_keys.forEach(function (req_key) { + if (!config[req_key]) { + var err_msg = util.format('Mastodon config must include `%s` when using %s.', req_key, auth_type) + throw new Error(err_msg) + } + }) +} + +module.exports = Mastodon diff --git a/node-mastodon/lib/settings.js b/node-mastodon/lib/settings.js new file mode 100644 index 0000000..f6d5411 --- /dev/null +++ b/node-mastodon/lib/settings.js @@ -0,0 +1,2 @@ +// set of status codes where we don't attempt reconnecting to Mastodon +exports.STATUS_CODES_TO_ABORT_ON = [ 400, 401, 403, 404, 406, 410, 422 ]; -- cgit