added akismet support

This commit is contained in:
Wyatt Johnson
2018-02-02 15:00:47 -07:00
parent 9b7f1dc43e
commit 484ae2539b
21 changed files with 527 additions and 23 deletions
+24 -22
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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),
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk"
}
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -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,
};
@@ -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 = () => (
<CommentDetail
icon={'add_box'}
header={t('talk-plugin-akismet.spam_comment')}
/>
);
SpamLabel.propTypes = {
comment: PropTypes.shape({
actions: PropTypes.array,
spam: PropTypes.spam,
}),
};
export default SpamLabel;
@@ -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 = () => (
<FlagLabel iconName="add_box">{t('talk-plugin-akismet.spam')}</FlagLabel>
);
export default SpamLabel;
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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],
},
};
@@ -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"
@@ -0,0 +1,6 @@
export function isSpam(actions) {
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SPAM_COMMENT'
);
}
+12
View File
@@ -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;
+13
View File
@@ -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,
};
+123
View File
@@ -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),
},
},
};
+12
View File
@@ -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 <coral@mozillafoundation.org>",
"license": "Apache-2.0",
"dependencies": {
"akismet-api": "^4.0.1"
}
}
+141
View File
@@ -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"