diff --git a/.eslintignore b/.eslintignore
index 588708bfc..50c6db9f9 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,34 +1,39 @@
+**/*.html
dist
docs
-**/*.html
+node_modules
plugins/*
-!plugins/talk-plugin-facebook-auth
+public
+
+!plugins/talk-plugin-akismet
!plugins/talk-plugin-auth
-!plugins/talk-plugin-respect
-!plugins/talk-plugin-offtopic
-!plugins/talk-plugin-like
-!plugins/talk-plugin-mod
-!plugins/talk-plugin-love
-!plugins/talk-plugin-viewing-options
-!plugins/talk-plugin-comment-content
-!plugins/talk-plugin-permalink
-!plugins/talk-plugin-featured-comments
-!plugins/talk-plugin-sort-newest
-!plugins/talk-plugin-sort-oldest
-!plugins/talk-plugin-sort-most-replied
-!plugins/talk-plugin-sort-most-liked
-!plugins/talk-plugin-sort-most-loved
-!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
-!plugins/talk-plugin-member-since
-!plugins/talk-plugin-ignore-user
-!plugins/talk-plugin-moderation-actions
-!plugins/talk-plugin-toxic-comments
-!plugins/talk-plugin-remember-sort
+!plugins/talk-plugin-comment-content
!plugins/talk-plugin-deep-reply-count
-!plugins/talk-plugin-subscriber
+!plugins/talk-plugin-facebook-auth
+!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-flag-details
+!plugins/talk-plugin-ignore-user
+!plugins/talk-plugin-like
+!plugins/talk-plugin-love
+!plugins/talk-plugin-member-since
+!plugins/talk-plugin-mod
+!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-notifications
!plugins/talk-plugin-notifications-reply
+!plugins/talk-plugin-offtopic
+!plugins/talk-plugin-permalink
+!plugins/talk-plugin-remember-sort
+!plugins/talk-plugin-respect
+!plugins/talk-plugin-sort-most-liked
+!plugins/talk-plugin-sort-most-loved
+!plugins/talk-plugin-sort-most-replied
+!plugins/talk-plugin-sort-most-respected
+!plugins/talk-plugin-sort-newest
+!plugins/talk-plugin-sort-oldest
+!plugins/talk-plugin-subscriber
+!plugins/talk-plugin-toxic-comments
+!plugins/talk-plugin-viewing-options
+
public
-node_modules
+node_modules
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0aeffcaf4..d6fe0880c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ browserstack.err
plugins.json
plugins/*
+!plugins/talk-plugin-akismet
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js
index 71d2ed1a3..b1f68bac2 100644
--- a/client/coral-admin/src/components/UserDetail.js
+++ b/client/coral-admin/src/components/UserDetail.js
@@ -25,6 +25,7 @@ import {
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
+import t from 'coral-framework/services/i18n';
class UserDetail extends React.Component {
rejectThenReload = async info => {
@@ -152,27 +153,27 @@ class UserDetail extends React.Component {
>
{suspended ? (
unsuspendUser({ id: user.id })}>
- Remove Suspension
+ {t('user_detail.remove_suspension')}
) : (
- Suspend User
+ {t('user_detail.suspend')}
)}
{banned ? (
unbanUser({ id: user.id })}>
- Remove Ban
+ {t('user_detail.remove_ban')}
) : (
- Ban User
+ {t('user_detail.ban')}
)}
@@ -190,14 +191,18 @@ class UserDetail extends React.Component {
-
- Member Since:
+
+ {t('user_detail.member_since')}:
+
{new Date(user.created_at).toLocaleString()}
{user.profiles.map(({ id }) => (
-
- Email:
+
+ {t('user_detail.email')}:
+
{id}{' '}
-
- Total Comments
+
+ {t('user_detail.total_comments')}
+
{totalComments}
-
- Reject Rate
+
+ {t('user_detail.reject_rate')}
+
{rejectedPercent.toFixed(1)}%
-
- Reports
+
+ {t('user_detail.reports')}
+
- All
+ {t('user_detail.all')}
- Rejected
+ {t('user_detail.rejected')}
- Account History
+ {t('user_detail.account_history')}
diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js
index c06e0739b..07e780e3e 100644
--- a/client/coral-admin/src/containers/Layout.js
+++ b/client/coral-admin/src/containers/Layout.js
@@ -75,7 +75,7 @@ class LayoutContainer extends React.Component {
);
} else if (loggedIn) {
return (
-
+
This page is for team use only. Please contact an administrator if
you want to join this team.
diff --git a/client/coral-admin/src/routes/Moderation/components/ViewOptions.js b/client/coral-admin/src/routes/Moderation/components/ViewOptions.js
index 3faa56d55..5735abc9e 100644
--- a/client/coral-admin/src/routes/Moderation/components/ViewOptions.js
+++ b/client/coral-admin/src/routes/Moderation/components/ViewOptions.js
@@ -19,12 +19,12 @@ class ViewOptions extends React.Component {
'talk-admin-moderation-view-options-headline'
)}
>
- View Options
+ {t('admin_sidebar.view_options')}
-
- Sort Comments
+ {t('admin_sidebar.sort_comments')}
{
+ // If we haven't check the spam yet, make sure to include `checkSpam=true` in the mutation.
+ // Otherwise post comment without checking the spam.
+ if (!this.checked) {
+ input.checkSpam = true;
+ this.checked = true;
+ }
+ });
+
+ this.spamPostHook = this.props.registerHook('postSubmit', result => {
+ const actions = result.createComment.actions;
+ if (
+ actions &&
+ actions.some(
+ ({ __typename, reason }) =>
+ __typename === 'FlagAction' && reason === 'SPAM_COMMENT'
+ )
+ ) {
+ this.props.notify('error', t('talk-plugin-akismet.still_spam'));
+ }
+
+ // Reset `checked` after comment was successfully posted.
+ this.checked = false;
+ });
+ }
+
+ componentWillUnmount() {
+ this.props.unregisterHook(this.spamPreHook);
+ this.props.unregisterHook(this.spamPostHook);
+ }
+
+ render() {
+ return null;
+ }
+}
+
+CheckSpamHook.propTypes = {
+ notify: PropTypes.func.isRequired,
+ registerHook: PropTypes.func.isRequired,
+ unregisterHook: PropTypes.func.isRequired,
+};
diff --git a/plugins/talk-plugin-akismet/client/components/SpamDetail.js b/plugins/talk-plugin-akismet/client/components/SpamDetail.js
new file mode 100644
index 000000000..dafdb5a86
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/components/SpamDetail.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { CommentDetail } from 'plugin-api/beta/client/components';
+import PropTypes from 'prop-types';
+import { t } from 'plugin-api/beta/client/services';
+
+const SpamLabel = () => (
+
+);
+
+SpamLabel.propTypes = {
+ comment: PropTypes.shape({
+ actions: PropTypes.array,
+ spam: PropTypes.spam,
+ }),
+};
+
+export default SpamLabel;
diff --git a/plugins/talk-plugin-akismet/client/components/SpamLabel.js b/plugins/talk-plugin-akismet/client/components/SpamLabel.js
new file mode 100644
index 000000000..c2033ff39
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/components/SpamLabel.js
@@ -0,0 +1,9 @@
+import React from 'react';
+import { FlagLabel } from 'plugin-api/beta/client/components/ui';
+import { t } from 'plugin-api/beta/client/services';
+
+const SpamLabel = () => (
+ {t('talk-plugin-akismet.spam')}
+);
+
+export default SpamLabel;
diff --git a/plugins/talk-plugin-akismet/client/containers/CheckSpamHook.js b/plugins/talk-plugin-akismet/client/containers/CheckSpamHook.js
new file mode 100644
index 000000000..f580b9632
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/containers/CheckSpamHook.js
@@ -0,0 +1,8 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'plugin-api/beta/client/hocs';
+import { notify } from 'plugin-api/beta/client/actions/notification';
+import CheckSpamHook from '../components/CheckSpamHook';
+
+const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
+
+export default connect(null, mapDispatchToProps)(CheckSpamHook);
diff --git a/plugins/talk-plugin-akismet/client/containers/SpamCommentDetail.js b/plugins/talk-plugin-akismet/client/containers/SpamCommentDetail.js
new file mode 100644
index 000000000..a133be062
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/containers/SpamCommentDetail.js
@@ -0,0 +1,12 @@
+import { compose } from 'react-apollo';
+import { excludeIf } from 'plugin-api/beta/client/hocs';
+import SpamDetail from './SpamDetail';
+import { isSpam } from '../utils';
+
+const enhance = compose(
+ excludeIf(
+ ({ comment: { spam, actions } }) => spam === null || isSpam(actions)
+ )
+);
+
+export default enhance(SpamDetail);
diff --git a/plugins/talk-plugin-akismet/client/containers/SpamCommentFlagDetail.js b/plugins/talk-plugin-akismet/client/containers/SpamCommentFlagDetail.js
new file mode 100644
index 000000000..a49fd89ca
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/containers/SpamCommentFlagDetail.js
@@ -0,0 +1,12 @@
+import { compose } from 'react-apollo';
+import { excludeIf } from 'plugin-api/beta/client/hocs';
+import SpamDetail from './SpamDetail';
+import { isSpam } from '../utils';
+
+const enhance = compose(
+ excludeIf(
+ ({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
+ )
+);
+
+export default enhance(SpamDetail);
diff --git a/plugins/talk-plugin-akismet/client/containers/SpamDetail.js b/plugins/talk-plugin-akismet/client/containers/SpamDetail.js
new file mode 100644
index 000000000..419a3c7eb
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/containers/SpamDetail.js
@@ -0,0 +1,21 @@
+import { compose, gql } from 'react-apollo';
+import { withFragments } from 'plugin-api/beta/client/hocs';
+import SpamDetail from '../components/SpamDetail';
+
+const enhance = compose(
+ withFragments({
+ comment: gql`
+ fragment TalkSpamComments_SpamDetail_Comment on Comment {
+ spam
+ actions {
+ __typename
+ ... on FlagAction {
+ reason
+ }
+ }
+ }
+ `,
+ })
+);
+
+export default enhance(SpamDetail);
diff --git a/plugins/talk-plugin-akismet/client/containers/SpamLabel.js b/plugins/talk-plugin-akismet/client/containers/SpamLabel.js
new file mode 100644
index 000000000..7af7f9f49
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/containers/SpamLabel.js
@@ -0,0 +1,22 @@
+import { compose, gql } from 'react-apollo';
+import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
+import SpamLabel from '../components/SpamLabel';
+import { isSpam } from '../utils';
+
+const enhance = compose(
+ withFragments({
+ comment: gql`
+ fragment TalkSpamComments_SpamLabel_Comment on Comment {
+ actions {
+ __typename
+ ... on FlagAction {
+ reason
+ }
+ }
+ }
+ `,
+ }),
+ excludeIf(({ comment: { actions } }) => !isSpam(actions))
+);
+
+export default enhance(SpamLabel);
diff --git a/plugins/talk-plugin-akismet/client/index.js b/plugins/talk-plugin-akismet/client/index.js
new file mode 100644
index 000000000..0eca7ab85
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/index.js
@@ -0,0 +1,15 @@
+import translations from './translations.yml';
+import CheckSpamHook from './containers/CheckSpamHook';
+import SpamLabel from './containers/SpamLabel';
+import SpamCommentDetail from './containers/SpamCommentDetail';
+import SpamCommentFlagDetail from './containers/SpamCommentFlagDetail';
+
+export default {
+ translations,
+ slots: {
+ commentInputDetailArea: [CheckSpamHook],
+ adminCommentLabels: [SpamLabel],
+ adminCommentMoreDetails: [SpamCommentDetail],
+ adminCommentMoreFlagDetails: [SpamCommentFlagDetail],
+ },
+};
diff --git a/plugins/talk-plugin-akismet/client/translations.yml b/plugins/talk-plugin-akismet/client/translations.yml
new file mode 100644
index 000000000..af7217831
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/translations.yml
@@ -0,0 +1,15 @@
+en:
+ error:
+ COMMENT_IS_SPAM: |
+ The language in this comment looks like spam. You can
+ edit the comment or submit it anyway for moderator review.
+ talk-plugin-akismet:
+ spam: "Spam"
+ spam_comment: "Spam"
+ detected: "Detected by Akismet"
+ still_spam: |
+ Thank you. Our moderation team will review your comment shortly.
+ flags:
+ reasons:
+ comment:
+ spam_comment: "Detected Spam"
\ No newline at end of file
diff --git a/plugins/talk-plugin-akismet/client/utils.js b/plugins/talk-plugin-akismet/client/utils.js
new file mode 100644
index 000000000..1fcee5d3d
--- /dev/null
+++ b/plugins/talk-plugin-akismet/client/utils.js
@@ -0,0 +1,6 @@
+export function isSpam(actions) {
+ return actions.some(
+ action =>
+ action.__typename === 'FlagAction' && action.reason === 'SPAM_COMMENT'
+ );
+}
diff --git a/plugins/talk-plugin-akismet/config.js b/plugins/talk-plugin-akismet/config.js
new file mode 100644
index 000000000..dfe528b5f
--- /dev/null
+++ b/plugins/talk-plugin-akismet/config.js
@@ -0,0 +1,12 @@
+const config = {
+ KEY: process.env.TALK_AKISMET_API_KEY,
+ SITE: process.env.TALK_AKISMET_SITE,
+};
+
+if (process.env.NODE_ENV !== 'test' && (!config.KEY || !config.SITE)) {
+ throw new Error(
+ 'Please set the TALK_AKISMET_API_KEY and TALK_AKISMET_SITE environment variable to use talk-plugin-akismet-comments. Visit https://akismet.com/ to get started.'
+ );
+}
+
+module.exports = config;
diff --git a/plugins/talk-plugin-akismet/errors.js b/plugins/talk-plugin-akismet/errors.js
new file mode 100644
index 000000000..b93d178b9
--- /dev/null
+++ b/plugins/talk-plugin-akismet/errors.js
@@ -0,0 +1,13 @@
+const { APIError } = require('errors');
+
+// ErrSpam is sent during a `CreateComment` mutation where
+// `input.checkSpam` is set to true and the comment contains
+// detected spam as determined by the akismet service.
+const ErrSpam = new APIError('Comment is spam', {
+ status: 400,
+ translation_key: 'COMMENT_IS_SPAM',
+});
+
+module.exports = {
+ ErrSpam,
+};
diff --git a/plugins/talk-plugin-akismet/index.js b/plugins/talk-plugin-akismet/index.js
new file mode 100644
index 000000000..4b0e7f997
--- /dev/null
+++ b/plugins/talk-plugin-akismet/index.js
@@ -0,0 +1,126 @@
+const debug = require('debug')('talk:plugin:akismet');
+const { ErrSpam } = require('./errors');
+const akismet = require('akismet-api');
+const { get, merge } = require('lodash');
+const { KEY, SITE } = require('./config');
+const client = akismet.client({
+ key: KEY,
+ blog: SITE,
+});
+
+let enabled = true;
+
+// TODO: when using a developer key, this is possible, the plus plan does not
+// allow us to check the key.
+// let enabled = false;
+// client.verifyKey((err, valid) => {
+// if (err) {
+// throw err;
+// }
+
+// if (valid) {
+// enabled = true;
+// } else {
+// throw new Error('Akismet key is invalid');
+// }
+// });
+
+module.exports = {
+ typeDefs: `
+ input CreateCommentInput {
+
+ # If true, the mutation will fail when the
+ # body contains detected spam.
+ checkSpam: Boolean
+ }
+
+ type Comment {
+ spam: Boolean
+ }
+ `,
+ hooks: {
+ RootMutation: {
+ createComment: {
+ async pre(_, { input }, { loaders, parent: req }) {
+ // If the key validation failed, then we can't run with the client.
+ if (!enabled) {
+ debug('not enabled, passing');
+ return;
+ }
+
+ let spam = false;
+ try {
+ const user_ip = get(req, 'ip', false);
+ if (!user_ip) {
+ debug('no ip on request');
+ return;
+ }
+
+ // Get some headers from the request.
+ const user_agent = req.get('User-Agent');
+ if (!user_agent || user_agent.length === 0) {
+ debug('no user agent on request');
+ return;
+ }
+
+ const referrer = req.get('Referrer');
+ if (!referrer || referrer.length === 0) {
+ debug('no referrer on request');
+ return;
+ }
+
+ // Get the Asset that the comment is being made against.
+ const asset = await loaders.Assets.getByID.load(input.asset_id);
+ if (!asset) {
+ debug('asset not found for new comment');
+ return;
+ }
+
+ // Send off the comment to Akismet to check to see what they say.
+ spam = await client.checkSpam({
+ user_ip,
+ user_agent,
+ referrer,
+ permalink: asset.url,
+ comment_type: 'comment',
+ comment_content: input.body,
+ is_test: true,
+ });
+
+ debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
+ } catch (err) {
+ console.trace(err);
+ return;
+ }
+
+ // Attach scores to metadata.
+ input.metadata = merge({}, input.metadata || {}, {
+ akismet: spam,
+ });
+
+ if (spam) {
+ if (input.checkSpam) {
+ throw ErrSpam;
+ }
+
+ // Attach reason information for the flag being added.
+ input.status = 'SYSTEM_WITHHELD';
+ input.actions =
+ input.actions && input.actions.length >= 0 ? input.actions : [];
+ input.actions.push({
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'SPAM_COMMENT',
+ metadata: {},
+ });
+ }
+ },
+ },
+ },
+ },
+ resolvers: {
+ Comment: {
+ spam: comment => get(comment, 'metadata.akismet', null),
+ },
+ },
+};
diff --git a/plugins/talk-plugin-akismet/package.json b/plugins/talk-plugin-akismet/package.json
new file mode 100644
index 000000000..04df3c8eb
--- /dev/null
+++ b/plugins/talk-plugin-akismet/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@coralproject/talk-plugin-akismet",
+ "pluginName": "talk-plugin-akismet",
+ "version": "0.0.1",
+ "description": "Provides support for preventing spam with the Akismet API",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "akismet-api": "^4.0.1"
+ }
+}
diff --git a/plugins/talk-plugin-akismet/yarn.lock b/plugins/talk-plugin-akismet/yarn.lock
new file mode 100644
index 000000000..ac97e590f
--- /dev/null
+++ b/plugins/talk-plugin-akismet/yarn.lock
@@ -0,0 +1,141 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+akismet-api@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/akismet-api/-/akismet-api-4.0.1.tgz#1c771442f09316847132aa16171bb4fb708b6519"
+ dependencies:
+ bluebird "^3.1.1"
+ superagent "^3.8.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+bluebird@^3.1.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+combined-stream@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+component-emitter@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+cookiejar@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+debug@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+extend@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+form-data@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+formidable@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
+
+inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+methods@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+
+mime-types@^2.1.12:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ dependencies:
+ mime-db "~1.30.0"
+
+mime@^1.4.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+qs@^6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+readable-stream@^2.0.5:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+superagent@^3.8.0:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403"
+ dependencies:
+ component-emitter "^1.2.0"
+ cookiejar "^2.1.0"
+ debug "^3.1.0"
+ extend "^3.0.0"
+ form-data "^2.3.1"
+ formidable "^1.1.1"
+ methods "^1.1.1"
+ mime "^1.4.1"
+ qs "^6.5.1"
+ readable-stream "^2.0.5"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"