From 484ae2539b4497b14cd398d677c93afec77501ba Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 2 Feb 2018 15:00:47 -0700 Subject: [PATCH] added akismet support --- .eslintignore | 46 +++--- .gitignore | 1 + graph/index.js | 2 +- plugins/talk-plugin-akismet/.eslintrc.json | 3 + .../talk-plugin-akismet/client/.eslintrc.json | 3 + .../client/components/CheckSpamHook.js | 54 +++++++ .../client/components/SpamDetail.js | 20 +++ .../client/components/SpamLabel.js | 9 ++ .../client/containers/CheckSpamHook.js | 8 + .../client/containers/SpamCommentDetail.js | 12 ++ .../containers/SpamCommentFlagDetail.js | 12 ++ .../client/containers/SpamDetail.js | 21 +++ .../client/containers/SpamLabel.js | 22 +++ plugins/talk-plugin-akismet/client/index.js | 15 ++ .../client/translations.yml | 15 ++ plugins/talk-plugin-akismet/client/utils.js | 6 + plugins/talk-plugin-akismet/config.js | 12 ++ plugins/talk-plugin-akismet/errors.js | 13 ++ plugins/talk-plugin-akismet/index.js | 123 +++++++++++++++ plugins/talk-plugin-akismet/package.json | 12 ++ plugins/talk-plugin-akismet/yarn.lock | 141 ++++++++++++++++++ 21 files changed, 527 insertions(+), 23 deletions(-) create mode 100644 plugins/talk-plugin-akismet/.eslintrc.json create mode 100644 plugins/talk-plugin-akismet/client/.eslintrc.json create mode 100644 plugins/talk-plugin-akismet/client/components/CheckSpamHook.js create mode 100644 plugins/talk-plugin-akismet/client/components/SpamDetail.js create mode 100644 plugins/talk-plugin-akismet/client/components/SpamLabel.js create mode 100644 plugins/talk-plugin-akismet/client/containers/CheckSpamHook.js create mode 100644 plugins/talk-plugin-akismet/client/containers/SpamCommentDetail.js create mode 100644 plugins/talk-plugin-akismet/client/containers/SpamCommentFlagDetail.js create mode 100644 plugins/talk-plugin-akismet/client/containers/SpamDetail.js create mode 100644 plugins/talk-plugin-akismet/client/containers/SpamLabel.js create mode 100644 plugins/talk-plugin-akismet/client/index.js create mode 100644 plugins/talk-plugin-akismet/client/translations.yml create mode 100644 plugins/talk-plugin-akismet/client/utils.js create mode 100644 plugins/talk-plugin-akismet/config.js create mode 100644 plugins/talk-plugin-akismet/errors.js create mode 100644 plugins/talk-plugin-akismet/index.js create mode 100644 plugins/talk-plugin-akismet/package.json create mode 100644 plugins/talk-plugin-akismet/yarn.lock diff --git a/.eslintignore b/.eslintignore index 7ee579147..50d3f7109 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,32 +1,34 @@ +**/*.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-author-menu !plugins/talk-plugin-comment-content -!plugins/talk-plugin-permalink +!plugins/talk-plugin-deep-reply-count +!plugins/talk-plugin-facebook-auth !plugins/talk-plugin-featured-comments -!plugins/talk-plugin-sort-newest -!plugins/talk-plugin-sort-oldest -!plugins/talk-plugin-sort-most-replied +!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-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-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-deep-reply-count +!plugins/talk-plugin-sort-newest +!plugins/talk-plugin-sort-oldest !plugins/talk-plugin-subscriber -!plugins/talk-plugin-flag-details -public -node_modules +!plugins/talk-plugin-toxic-comments +!plugins/talk-plugin-viewing-options \ No newline at end of file diff --git a/.gitignore b/.gitignore index 70b6d482e..a27a08f15 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/graph/index.js b/graph/index.js index 2ccaaea11..c05d878e6 100644 --- a/graph/index.js +++ b/graph/index.js @@ -8,7 +8,7 @@ module.exports = { // Schema is created already, so just include it. schema, - // Load in the new context here, this'll create the loaders + mutators for + // Load in the new context here, this will create the loaders + mutators for // the lifespan of this request. context: new Context(req), diff --git a/plugins/talk-plugin-akismet/.eslintrc.json b/plugins/talk-plugin-akismet/.eslintrc.json new file mode 100644 index 000000000..64e91163f --- /dev/null +++ b/plugins/talk-plugin-akismet/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk" +} \ No newline at end of file diff --git a/plugins/talk-plugin-akismet/client/.eslintrc.json b/plugins/talk-plugin-akismet/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-akismet/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-akismet/client/components/CheckSpamHook.js b/plugins/talk-plugin-akismet/client/components/CheckSpamHook.js new file mode 100644 index 000000000..58eeac11f --- /dev/null +++ b/plugins/talk-plugin-akismet/client/components/CheckSpamHook.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { t } from 'plugin-api/beta/client/services'; + +/** + * CheckSpamHook adds hooks to the `commentBox` + * that handles checking a comment for spam. + */ +export default class CheckSpamHook extends React.Component { + // checked signifies if we already sent a request with the `checkSpam` set to true. + checked = false; + + componentDidMount() { + this.spamPreHook = this.props.registerHook('preSubmit', input => { + // 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..554ffb849 --- /dev/null +++ b/plugins/talk-plugin-akismet/client/components/SpamDetail.js @@ -0,0 +1,20 @@ +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..a9a209139 --- /dev/null +++ b/plugins/talk-plugin-akismet/client/translations.yml @@ -0,0 +1,15 @@ +en: + error: + COMMENT_IS_SPAM: | + Are you sure? The language in this comment matches looks like spam. + You can edit the comment or submit it for moderator review. + talk-plugin-akismet: + spam: "Spam" + spam_comment: "Detected Spam" + still_spam: | + This edited comment might still violate our community guidelines. + 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..a368da505 --- /dev/null +++ b/plugins/talk-plugin-akismet/index.js @@ -0,0 +1,123 @@ +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; +// 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"