summary refs log tree commit diff
diff options
context:
space:
mode:
authorpluralcafe-docker <docker@plural.cafe>2018-08-22 07:57:04 +0000
committerpluralcafe-docker <docker@plural.cafe>2018-08-22 07:57:04 +0000
commite530b55b8dc55f4c8974d2999b7e572b19941c4c (patch)
tree5ecfcd1a1a8cfff43cf43948998b089253df10d6
parent331a28d01d4e35805200d8d6236ec81d105e9ccd (diff)
Don't use node-mastodon and instead use fetch
-rw-r--r--Dockerfile2
-rw-r--r--index.js51
-rw-r--r--node-mastodon/LICENSE22
-rw-r--r--node-mastodon/README.md100
-rw-r--r--node-mastodon/lib/helpers.js65
-rw-r--r--node-mastodon/lib/mastodon.js340
-rw-r--r--node-mastodon/lib/settings.js2
-rw-r--r--package.json2
8 files changed, 30 insertions, 554 deletions
diff --git a/Dockerfile b/Dockerfile
index 8fc870d..1869e59 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,7 +21,7 @@ RUN echo "@edge https://uk.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
  && addgroup -g ${GID} barkeep \
  && adduser -h /barkeep -s /bin/sh -D -G barkeep -u ${UID} barkeep
 
-COPY --chown=barkeep:barkeep package.json index.js node-mastodon /barkeep/
+COPY --chown=barkeep:barkeep package.json index.js /barkeep/
 
 RUN yarn install
 
diff --git a/index.js b/index.js
index cf58ca8..7b607ca 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,4 @@
-var mastodon = require('./lib/mastodon');
+var fetch = require('node-fetch');
 var pg = require('pg');
 
 var DB_USER = process.env.DB_USER || 'ambassador';
@@ -137,43 +137,48 @@ function cycle() {
   });
 }
 
-var M = new mastodon({
-  access_token: AMBASSADOR_TOKEN,
-  api_url: INSTANCE_HOST + '/api/v1'
-});
-
 function whoami(f) {
-  M.get('/accounts/verify_credentials', function(err, result) {
-    if (err) {
-      console.error('error getting current user id');
-      throw err;
+  fetch(INSTANCE_HOST + '/api/v1/accounts/verify_credentials', {
+    headers: {
+      'Authorization': 'Bearer ' + AMBASSADOR_TOKEN
     }
+  })
+  .then(res => res.json())
+  .then(result => {
     if (result.id === undefined) {
       console.error('verify_credentials result is undefined');
-      throw "verify_credentials failed";
+      process.exit(1);
     }
     console.log('Authenticated as ' + result.id + ' (' + result.display_name + ')');
     return f(result.id);
   })
+  .catch(err => {
+    console.error(err);
+    process.exit(1);
+  });
 }
 
 function boost(rows) {
   rows.forEach(function(row) {
     console.log('boosting status #' + row.id);
-    M.post('/statuses/' + row.id + '/reblog', function(err, result) {
-      if (err) {
-        if (err.message === 'Validation failed: Reblog of status already exists') {
-          return console.log('Warning: tried to boost #' + row.id + ' but it had already been boosted by this account.');
-        }
-
-        if (err.message === 'This action is not allowed') {
-          return console.log('Warning: tried to boost #' + row.id + ' but the action was not allowed.');
-        }
-
-        return console.log(err);
+    fetch(INSTANCE_HOST + '/api/v1/statuses/' + row.id + '/reblog', {
+      headers: {
+        'Authorization': 'Bearer ' + AMBASSADOR_TOKEN
+      },
+      body: ''
+    })
+    .then(res => res.json())
+    .then(result => {
+      if (result.message === 'Validation failed: Reblog of status already exists') {
+        console.log('Warning: tried to boost #' + row.id + ' but it had already been boosted by this account.');
+      } else if(result.message === 'This action is not allowed') {
+        console.log('Warning: tried to boost #' + row.id + ' but the action was not allowed.');
       }
+    }).catch(err => {
+      console.error(err);
+      process.exit(1);
     });
-  })
+  }
 }
 
 cycle();
diff --git a/node-mastodon/LICENSE b/node-mastodon/LICENSE
deleted file mode 100644
index 29b5e63..0000000
--- a/node-mastodon/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-(The MIT License)
-
-Copyright for portions of project node-mastodon are held by Tolga Tezel <tolgatezel11@gmail.com> 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
deleted file mode 100644
index 2b30fa7..0000000
--- a/node-mastodon/README.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# 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
deleted file mode 100644
index 8833968..0000000
--- a/node-mastodon/lib/helpers.js
+++ /dev/null
@@ -1,65 +0,0 @@
-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
deleted file mode 100644
index 7f5c8c1..0000000
--- a/node-mastodon/lib/mastodon.js
+++ /dev/null
@@ -1,340 +0,0 @@
-//
-//  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
deleted file mode 100644
index f6d5411..0000000
--- a/node-mastodon/lib/settings.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// 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 ];
diff --git a/package.json b/package.json
index 8957541..2439014 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
   "author": "@mykola@a.weirder.earth",
   "license": "MIT",
   "dependencies": {
-    "mastodon": "^1.2.2",
+    "node-fetch": "^2.2.0",
     "pg": "^7.4.3",
     "pm2": "^2.10.4"
   },