From 6f82fd76f5acace588f4d892ce16f7c701747c0e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 6 Sep 2017 22:11:48 +0700 Subject: [PATCH] First draft of toxic-comments --- .eslintignore | 1 + .gitignore | 1 + .../coral-embed-stream/src/graphql/index.js | 2 +- client/coral-framework/graphql/fragments.js | 1 + client/coral-framework/graphql/mutations.js | 8 +- graph/errorHandler.js | 70 +++++++++ graph/mutators/comment.js | 5 +- graph/resolvers/root_mutation.js | 69 ++++---- graph/schema.js | 10 +- graph/typeDefs.graphql | 8 +- plugins.default.json | 6 +- .../client/actions.js | 5 - .../client/components/CheckToxicityHook.js | 27 ++++ .../client/components/ToxicCommentAlert.js | 62 -------- .../client/components/styles.css | 5 - .../client/constants.js | 1 - .../client/index.js | 6 +- .../client/reducer.js | 18 --- .../client/translations.yml | 4 + plugins/talk-plugin-toxic-comments/index.js | 7 +- .../package-lock.json | 144 ----------------- .../talk-plugin-toxic-comments/package.json | 7 +- .../server/apiKey.js | 6 + .../server/constants.js | 3 + .../server/errors.js | 15 ++ .../server/hooks.js | 37 +++++ .../server/perspective.js | 37 +++++ .../server/router.js | 65 +++----- .../server/typeDefs.graphql | 4 + plugins/talk-plugin-toxic-comments/yarn.lock | 147 ------------------ test/server/graph/mutations/createComment.js | 6 +- test/server/graph/mutations/removeTag.js | 2 +- 32 files changed, 298 insertions(+), 491 deletions(-) create mode 100644 graph/errorHandler.js delete mode 100644 plugins/talk-plugin-toxic-comments/client/actions.js create mode 100644 plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js delete mode 100644 plugins/talk-plugin-toxic-comments/client/components/ToxicCommentAlert.js delete mode 100644 plugins/talk-plugin-toxic-comments/client/components/styles.css delete mode 100644 plugins/talk-plugin-toxic-comments/client/constants.js delete mode 100644 plugins/talk-plugin-toxic-comments/client/reducer.js delete mode 100644 plugins/talk-plugin-toxic-comments/package-lock.json create mode 100644 plugins/talk-plugin-toxic-comments/server/apiKey.js create mode 100644 plugins/talk-plugin-toxic-comments/server/constants.js create mode 100644 plugins/talk-plugin-toxic-comments/server/errors.js create mode 100644 plugins/talk-plugin-toxic-comments/server/hooks.js create mode 100644 plugins/talk-plugin-toxic-comments/server/perspective.js create mode 100644 plugins/talk-plugin-toxic-comments/server/typeDefs.graphql diff --git a/.eslintignore b/.eslintignore index 4805e34a4..d23f5dfb4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -22,5 +22,6 @@ plugins/* !plugins/talk-plugin-author-menu !plugins/talk-plugin-member-since !plugins/talk-plugin-ignore-user +!plugins/talk-plugin-toxic-comments node_modules diff --git a/.gitignore b/.gitignore index 98abecb32..c3c17fac3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ plugins/* !plugins/talk-plugin-author-menu !plugins/talk-plugin-member-since !plugins/talk-plugin-ignore-user +!plugins/talk-plugin-toxic-comments **/node_modules/* story.html diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 09470de14..ad281ce0c 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -109,7 +109,7 @@ export default { }, mutations: { PostComment: ({ - variables: {comment: {asset_id, body, parent_id, tags = []}}, + variables: {input: {asset_id, body, parent_id, tags = []}}, state: {auth}, }) => ({ optimisticResponse: { diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index 110f0796b..5b3fe5498 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -6,6 +6,7 @@ export default { 'SetCommentStatusResponse', 'SuspendUserResponse', 'RejectUsernameResponse', + 'CreateCommentResponse', 'SetUserStatusResponse', 'CreateFlagResponse', 'EditCommentResponse', diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 4254400bb..c09ce8751 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -192,17 +192,17 @@ export const withSetUserStatus = withMutation( export const withPostComment = withMutation( gql` - mutation PostComment($comment: CreateCommentInput!) { - createComment(comment: $comment) { + mutation PostComment($input: CreateCommentInput!) { + createComment(input: $input) { ...CreateCommentResponse } } `, { props: ({mutate}) => ({ - postComment: (comment) => { + postComment: (input) => { return mutate({ variables: { - comment + input }, }); } diff --git a/graph/errorHandler.js b/graph/errorHandler.js new file mode 100644 index 000000000..066ae3319 --- /dev/null +++ b/graph/errorHandler.js @@ -0,0 +1,70 @@ +const { + GraphQLObjectType, + GraphQLInterfaceType +} = require('graphql'); +const {maskErrors} = require('graphql-errors'); +const errors = require('../errors'); +const {Error: {ValidationError}} = require('mongoose'); + +// This function is pretty much copied verbatim from the graphql-tools repo: +// https://github.com/apollographql/graphql-tools/blob/b12973c86e00be209d04af0184780998056051c4/src/schemaGenerator.ts#L180-L194 +const forEachField = (schema, fn) => { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +}; + +// If an APIError happens in a mutation, then respond with `{errors: Array}` +// according to the schema. +const decorateWithMutationErrorHandler = (field) => { + const fieldResolver = field.resolve; + field.resolve = async (obj, args, ctx, info) => { + try { + return await fieldResolver(obj, args, ctx, info); + } + catch(err) { + if (err instanceof errors.APIError) { + return { + errors: [err] + }; + } else if (err instanceof ValidationError) { + + // TODO: wrap this with one of our internal errors. + throw err; + } + + throw err; + } + }; +}; + +// Masks errors during production and handle mutation errors inside the schema. +const decorateWithErrorHandler = (schema) => { + forEachField(schema, (field, typeName) => { + + // Handle mutation errors. + if (typeName === 'RootMutation') { + decorateWithMutationErrorHandler(field); + } + + // If we are in production mode, don't show server errors to the front end. + if (process.env.NODE_ENV === 'production') { + + // Mask errors that are thrown if we are in a production environment. + maskErrors(field); + } + }); +}; + +module.exports = { + decorateWithErrorHandler, +}; diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 4ca044fcf..71e341547 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -154,7 +154,7 @@ const adjustKarma = (Comments, id, status) => async () => { * @param {String} [status='NONE'] the status of the new comment * @return {Promise} resolves to the created comment */ -const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => { +const createComment = async (context, {tags = [], body, asset_id, parent_id = null, metadata = {}}, status = 'NONE') => { const {user, loaders: {Comments}, pubsub} = context; // Resolve the tags for the comment. @@ -166,7 +166,8 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu parent_id, status, tags, - author_id: user.id + author_id: user.id, + metadata, }); // If the loaders are present, clear the caches for these values because we diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 685c26a17..437d2ab5b 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -1,35 +1,41 @@ -const wrapResponse = require('../helpers/response'); - const RootMutation = { - createComment(_, {comment}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.create(comment)); + async createComment(_, {input}, {mutators: {Comment}}) { + return { + comment: await Comment.create(input), + }; }, - editComment(_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) { - return wrapResponse('comment')(Comment.edit({id, asset_id, edit: {body}})); + async editComment(_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) { + return { + comment: await Comment.edit({id, asset_id, edit: {body}}), + }; }, - createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) { - return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}})); + async createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) { + return { + flag: Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}), + }; }, - createDontAgree(_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) { - return wrapResponse('dontagree')(Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}})); + async createDontAgree(_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) { + return { + dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}}), + }; }, - deleteAction(_, {id}, {mutators: {Action}}) { - return wrapResponse(null)(Action.delete({id})); + async deleteAction(_, {id}, {mutators: {Action}}) { + await Action.delete({id}); }, - setUserStatus(_, {id, status}, {mutators: {User}}) { - return wrapResponse(null)(User.setUserStatus({id, status})); + async setUserStatus(_, {id, status}, {mutators: {User}}) { + await User.setUserStatus({id, status}); }, - suspendUser(_, {input: {id, message, until}}, {mutators: {User}}) { - return wrapResponse(null)(User.suspendUser({id, message, until})); + async suspendUser(_, {input: {id, message, until}}, {mutators: {User}}) { + await User.suspendUser({id, message, until}); }, - rejectUsername(_, {input: {id, message}}, {mutators: {User}}) { - return wrapResponse(null)(User.rejectUsername({id, message})); + async rejectUsername(_, {input: {id, message}}, {mutators: {User}}) { + await User.rejectUsername({id, message}); }, - ignoreUser(_, {id}, {mutators: {User}}) { - return wrapResponse(null)(User.ignoreUser({id})); + async ignoreUser(_, {id}, {mutators: {User}}) { + await User.ignoreUser({id}); }, - stopIgnoringUser(_, {id}, {mutators: {User}}) { - return wrapResponse(null)(User.stopIgnoringUser({id})); + async stopIgnoringUser(_, {id}, {mutators: {User}}) { + await User.stopIgnoringUser({id}); }, async setCommentStatus(_, {id, status}, {mutators: {Comment}, pubsub}) { const comment = await Comment.setStatus({id, status}); @@ -42,19 +48,20 @@ const RootMutation = { // Publish the comment status change via the subscription. pubsub.publish('commentRejected', comment); } - return wrapResponse(null)(comment); }, - addTag(_, {tag}, {mutators: {Tag}}) { - return wrapResponse(null)(Tag.add(tag)); + async addTag(_, {tag}, {mutators: {Tag}}) { + await Tag.add(tag); }, - removeTag(_, {tag}, {mutators: {Tag}}) { - return wrapResponse(null)(Tag.remove(tag)); + async removeTag(_, {tag}, {mutators: {Tag}}) { + await Tag.remove(tag); }, - createToken(_, {input}, {mutators: {Token}}) { - return wrapResponse('token')(Token.create(input)); + async createToken(_, {input}, {mutators: {Token}}) { + return { + token: await Token.create(input), + }; }, - revokeToken(_, {input}, {mutators: {Token}}) { - return wrapResponse(null)(Token.revoke(input)); + async revokeToken(_, {input}, {mutators: {Token}}) { + await Token.revoke(input); } }; diff --git a/graph/schema.js b/graph/schema.js index 53360e701..d4cd9f890 100644 --- a/graph/schema.js +++ b/graph/schema.js @@ -1,6 +1,6 @@ const {makeExecutableSchema} = require('graphql-tools'); -const {maskErrors} = require('graphql-errors'); const {decorateWithHooks} = require('./hooks'); +const {decorateWithErrorHandler} = require('./errorHandler'); const plugins = require('../services/plugins'); const resolvers = require('./resolvers'); @@ -11,11 +11,7 @@ const schema = makeExecutableSchema({typeDefs, resolvers}); // Plugin to the schema level resolvers to provide an before/after hook. decorateWithHooks(schema, plugins.get('server', 'hooks')); -// If we are in production mode, don't show server errors to the front end. -if (process.env.NODE_ENV === 'production') { - - // Mask errors that are thrown if we are in a production environment. - maskErrors(schema); -} +// Handle errors like masking in production and mutation errors. +decorateWithErrorHandler(schema); module.exports = schema; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 9d8e25f12..c3cbc2e57 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1033,7 +1033,7 @@ type RevokeTokenResponse implements Response { type RootMutation { # Creates a comment on the asset. - createComment(comment: CreateCommentInput!): CreateCommentResponse + createComment(input: CreateCommentInput!): CreateCommentResponse # Creates a flag on an entity. createFlag(flag: CreateFlagInput!): CreateFlagResponse @@ -1060,10 +1060,10 @@ type RootMutation { setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse # Add a tag. - addTag(tag: ModifyTagInput!): ModifyTagResponse! + addTag(tag: ModifyTagInput!): ModifyTagResponse # Removes a tag. - removeTag(tag: ModifyTagInput!): ModifyTagResponse! + removeTag(tag: ModifyTagInput!): ModifyTagResponse # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse @@ -1072,7 +1072,7 @@ type RootMutation { createToken(input: CreateTokenInput!): CreateTokenResponse! # RevokeToken will revoke an existing token. - revokeToken(input: RevokeTokenInput!): RevokeTokenResponse! + revokeToken(input: RevokeTokenInput!): RevokeTokenResponse # Stop Ignoring comments by another user stopIgnoringUser(id: ID!): StopIgnoringUserResponse diff --git a/plugins.default.json b/plugins.default.json index 950cec229..46762865f 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -4,7 +4,8 @@ "talk-plugin-respect", "talk-plugin-offtopic", "talk-plugin-facebook-auth", - "talk-plugin-featured-comments" + "talk-plugin-featured-comments", + "talk-plugin-toxic-comments" ], "client": [ "talk-plugin-respect", @@ -20,6 +21,7 @@ "talk-plugin-sort-most-replied", "talk-plugin-author-menu", "talk-plugin-member-since", - "talk-plugin-ignore-user" + "talk-plugin-ignore-user", + "talk-plugin-toxic-comments" ] } diff --git a/plugins/talk-plugin-toxic-comments/client/actions.js b/plugins/talk-plugin-toxic-comments/client/actions.js deleted file mode 100644 index 9cb3947f8..000000000 --- a/plugins/talk-plugin-toxic-comments/client/actions.js +++ /dev/null @@ -1,5 +0,0 @@ -import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants'; - -export const toggleCheckbox = () => ({ - type: OFFTOPIC_TOGGLE_CHECKBOX -}); diff --git a/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js new file mode 100644 index 000000000..2e4c66d3e --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js @@ -0,0 +1,27 @@ +import React from 'react'; + +export default class CheckToxicityHook extends React.Component { + checked = false; + + componentDidMount() { + this.toxicityPreHook = this.props.registerHook('preSubmit', (input) => { + if (!this.checked) { + input.checkToxicity = true; + this.checked = true; + } + }); + + this.toxicityPostHook = this.props.registerHook('postSubmit', () => { + this.checked = false; + }); + } + + componentWillUnmount() { + this.props.unregisterHook(this.toxicityPreHook); + this.props.unregisterHook(this.toxicityPostHook); + } + + render() { + return null; + } +} diff --git a/plugins/talk-plugin-toxic-comments/client/components/ToxicCommentAlert.js b/plugins/talk-plugin-toxic-comments/client/components/ToxicCommentAlert.js deleted file mode 100644 index 51ad5ef6d..000000000 --- a/plugins/talk-plugin-toxic-comments/client/components/ToxicCommentAlert.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import styles from './styles.css'; -import {t} from 'plugin-api/beta/client/services'; -import {isTagged} from 'plugin-api/beta/client/utils'; - -export default class ToxicCommentAlert extends React.Component { - - constructor(props) { - super(props); - this.state = { - toxic: false - }; - } - - componentDidMount() { - this.toxicityHook = this.props.registerHook('preSubmit', (data) => { - const comment = data.body; - (async() => { - var toxicity = await fetch('/api/v1/toxicity/score', { - method: 'POST', - body: comment - }) - .then(response => response.json()) - .then(function(json) { - return json.score; - }) - .catch(function(err) { - console.log(err); - return 0; - }); - console.log(toxicity); - if(toxicity > 0.3){ - this.setState({ - toxic: true - }); - } - else { - this.setState({ - toxic: false - }); - } - })(); - }); - } - - componentWillUnmount() { - this.props.unregisterHook(this.toxicityHook); - } - - render() { - return( -
- { - this.state.toxic ? ( - Are you sure you want to post this? Other members of the community my view your comment as toxic, so please take a moment to reconsider. - ) : null - } -
- ); - } - -} diff --git a/plugins/talk-plugin-toxic-comments/client/components/styles.css b/plugins/talk-plugin-toxic-comments/client/components/styles.css deleted file mode 100644 index d898dd111..000000000 --- a/plugins/talk-plugin-toxic-comments/client/components/styles.css +++ /dev/null @@ -1,5 +0,0 @@ -.toxicComment { - color: red; - font-weight: bold; - padding: 12px 15px 10px 15px -} diff --git a/plugins/talk-plugin-toxic-comments/client/constants.js b/plugins/talk-plugin-toxic-comments/client/constants.js deleted file mode 100644 index 122d7de17..000000000 --- a/plugins/talk-plugin-toxic-comments/client/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const OFFTOPIC_TOGGLE_CHECKBOX = 'OFFTOPIC_TOGGLE_CHECKBOX'; diff --git a/plugins/talk-plugin-toxic-comments/client/index.js b/plugins/talk-plugin-toxic-comments/client/index.js index dc9aa5348..46f8c0fc0 100644 --- a/plugins/talk-plugin-toxic-comments/client/index.js +++ b/plugins/talk-plugin-toxic-comments/client/index.js @@ -1,5 +1,5 @@ import translations from './translations.yml'; -import ToxicCommentAlert from './components/ToxicCommentAlert'; +import CheckToxicityHook from './components/CheckToxicityHook'; /** * coral-plugin-offtopic depends on coral-plugin-viewing-options @@ -9,6 +9,6 @@ import ToxicCommentAlert from './components/ToxicCommentAlert'; export default { translations, slots: { - commentInputDetailArea: [ToxicCommentAlert], - } + commentInputDetailArea: [CheckToxicityHook], + }, }; diff --git a/plugins/talk-plugin-toxic-comments/client/reducer.js b/plugins/talk-plugin-toxic-comments/client/reducer.js deleted file mode 100644 index adab987a0..000000000 --- a/plugins/talk-plugin-toxic-comments/client/reducer.js +++ /dev/null @@ -1,18 +0,0 @@ -import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants'; - -const initialState = { - checked: false -}; - -export default function offTopic (state = initialState, action) { - switch (action.type) { - case OFFTOPIC_TOGGLE_CHECKBOX: { - return { - ...state, - checked: !state.checked - }; - } - default : - return state; - } -} diff --git a/plugins/talk-plugin-toxic-comments/client/translations.yml b/plugins/talk-plugin-toxic-comments/client/translations.yml index 7f6db9370..b1553a3d9 100644 --- a/plugins/talk-plugin-toxic-comments/client/translations.yml +++ b/plugins/talk-plugin-toxic-comments/client/translations.yml @@ -1,4 +1,8 @@ en: + error: + COMMENT_IS_TOXIC: | + Are you sure? The language in this comment might violate our community guidelines. + You can edit the comment or submit it for moderator review. talk-plugin-featured-comments: featured: Featured go_to_conversation: Go to conversation diff --git a/plugins/talk-plugin-toxic-comments/index.js b/plugins/talk-plugin-toxic-comments/index.js index 10651ec9d..232f45052 100644 --- a/plugins/talk-plugin-toxic-comments/index.js +++ b/plugins/talk-plugin-toxic-comments/index.js @@ -1,5 +1,10 @@ +const {readFileSync} = require('fs'); +const path = require('path'); const router = require('./server/router'); +const hooks = require('./server/hooks'); module.exports = { - router + typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'), + router, + hooks, }; diff --git a/plugins/talk-plugin-toxic-comments/package-lock.json b/plugins/talk-plugin-toxic-comments/package-lock.json deleted file mode 100644 index 690852f34..000000000 --- a/plugins/talk-plugin-toxic-comments/package-lock.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "name": "@coralproject/talk-plugin-toxicity", - "version": "0.0.1", - "lockfileVersion": 1, - "dependencies": { - "axios": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", - "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=" - }, - "body-parser": { - "version": "1.17.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", - "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", - "dependencies": { - "debug": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", - "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" - } - } - }, - "boom": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/boom/-/boom-3.2.2.tgz", - "integrity": "sha1-DwzF0ErcUAO4x9cfQsynJx/vDng=" - }, - "bytes": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" - }, - "content-type": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", - "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" - }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" - }, - "depd": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "express-boom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/express-boom/-/express-boom-2.0.0.tgz", - "integrity": "sha1-1AC5QOlhqKou2OP3fFlfoeQbZLM=" - }, - "follow-redirects": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", - "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==" - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "http-errors": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", - "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" - }, - "iconv-lite": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "mime-db": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" - }, - "mime-types": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "raw-body": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", - "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=" - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - } - } -} diff --git a/plugins/talk-plugin-toxic-comments/package.json b/plugins/talk-plugin-toxic-comments/package.json index b288f95c4..848e6d948 100644 --- a/plugins/talk-plugin-toxic-comments/package.json +++ b/plugins/talk-plugin-toxic-comments/package.json @@ -5,10 +5,5 @@ "description": "Provides support for measuring the toxicity of user comments using the Perspectives API", "main": "index.js", "author": "The Coral Project Team ", - "license": "Apache-2.0", - "dependencies": { - "axios": "^0.16.2", - "body-parser": "^1.17.2", - "express-boom": "^2.0.0" - } + "license": "Apache-2.0" } diff --git a/plugins/talk-plugin-toxic-comments/server/apiKey.js b/plugins/talk-plugin-toxic-comments/server/apiKey.js new file mode 100644 index 000000000..43e02d154 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/apiKey.js @@ -0,0 +1,6 @@ +const apiKey = process.env.TALK_PERSPECTIVE_API_KEY; +if(!apiKey) { + throw new Error('Please set the TALK_PERSPECTIVE_API_KEY environment variable to use the toxic-comments plugin. Visit https://www.perspectiveapi.com/ to request API access.'); +} + +module.exports = apiKey; diff --git a/plugins/talk-plugin-toxic-comments/server/constants.js b/plugins/talk-plugin-toxic-comments/server/constants.js new file mode 100644 index 000000000..a335c519c --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/constants.js @@ -0,0 +1,3 @@ +module.exports = { + TOXICITY_THRESHOLD: 0.8, +}; diff --git a/plugins/talk-plugin-toxic-comments/server/errors.js b/plugins/talk-plugin-toxic-comments/server/errors.js new file mode 100644 index 000000000..57f6ef959 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/errors.js @@ -0,0 +1,15 @@ +const {APIError} = require('../../../errors'); + +const ErrNoComment = new APIError('Comment must be provided', { + status: 400, +}); + +const ErrToxic = new APIError('Comment is toxic', { + status: 400, + translation_key: 'COMMENT_IS_TOXIC', +}); + +module.exports = { + ErrNoComment, + ErrToxic, +}; diff --git a/plugins/talk-plugin-toxic-comments/server/hooks.js b/plugins/talk-plugin-toxic-comments/server/hooks.js new file mode 100644 index 000000000..07166c73a --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/hooks.js @@ -0,0 +1,37 @@ +const perspective = require('./perspective'); +const {ADD_COMMENT_TAG} = require('../../../perms/constants'); +const {ErrToxic} = require('./errors'); +const {TOXICITY_THRESHOLD} = require('./constants'); + +module.exports = { + Comment: { + tags: { + post(comment, input, {user}, _info, result) { + if (comment.metadata.perspective && user && user.can(ADD_COMMENT_TAG)) { + return result.concat({tag: {name: 'TOXIC', created_at: new Date()}}); + } + return result; + } + }, + }, + RootMutation: { + createComment: { + async pre(_, {input}, _context, _info) { + + // Don't call out to perspective when running tests. + if (process.env.NODE_ENV === 'test') { + return; + } + + const apiKey = require('./apiKey'); + const scores = await perspective.getScores(apiKey, input.body); + if (input.checkToxicity && scores.SEVERE_TOXICITY.summaryScore > TOXICITY_THRESHOLD) { + throw ErrToxic; + } + input.metadata = Object.assign({}, input.metadata, { + perspective: scores, + }); + }, + }, + }, +}; diff --git a/plugins/talk-plugin-toxic-comments/server/perspective.js b/plugins/talk-plugin-toxic-comments/server/perspective.js new file mode 100644 index 000000000..e863b4777 --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/perspective.js @@ -0,0 +1,37 @@ +const fetch = require('node-fetch'); + +const API_ENPOINT = 'https://commentanalyzer.googleapis.com/v1alpha1'; + +async function getScores(apiKey, text) { + const response = await fetch(`${API_ENPOINT}/comments:analyze?key=${apiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + comment: { + text, + }, + + // TODO: support other languages. + languages: ['en'], + requestedAttributes: { + TOXICITY: {}, + SEVERE_TOXICITY: {}, + } + }), + }); + const data = await response.json(); + return { + TOXICITY: { + summaryScore: data.attributeScores.TOXICITY.summaryScore.value + }, + SEVERE_TOXICITY: { + summaryScore: data.attributeScores.SEVERE_TOXICITY.summaryScore.value + }, + }; +} + +module.exports = { + getScores, +}; diff --git a/plugins/talk-plugin-toxic-comments/server/router.js b/plugins/talk-plugin-toxic-comments/server/router.js index 4f3011efc..42f189d26 100644 --- a/plugins/talk-plugin-toxic-comments/server/router.js +++ b/plugins/talk-plugin-toxic-comments/server/router.js @@ -1,56 +1,33 @@ -const http = require('axios'); -const boom = require('express-boom'); -const bodyParser = require('body-parser'); +const perspective = require('./perspective'); +const {ErrNoComment} = require('./errors'); module.exports = (router) => { - const key = process.env.TALK_PERSPECTIVE_API_KEY; - if(!key) { - throw new Error('Please set the TALK_PERSPECTIVE_API_KEY environment variable to use the toxic-comments plugin. Visit https://www.perspectiveapi.com/ to request API access.'); - } - - - router.use(boom()); - router.use(bodyParser.text()); - /** * POST /api/v1/toxicity/score * args: * - provide the comment in the request body */ - router.post('/api/v1/toxicity/score', (req, res) => { - var comment = req.body; - if(comment) { - var body = { - comment: { - text: comment, - }, - languages: ["en"], - requestedAttributes: { - TOXICITY: {} - } - }; - var headers = { - 'Content-Type': 'application/json', - }; - http.post( - 'https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze?key='+key, - body) - .then(function(response) { - var data = response.data; - var score = { - comment: comment, - score: data.attributeScores.TOXICITY.summaryScore.value - } - return res.json(score); - }) - .catch(function(err) { - console.log(err); - res.boom.badRequest('The Perspective API returned an error. Please check the server logs for details.'); - }) + router.post('/api/v1/toxicity/score', async (req, res, next) => { + const apiKey = process.env.TALK_PERSPECTIVE_API_KEY; + if(!apiKey) { + throw new Error('Please set the TALK_PERSPECTIVE_API_KEY environment variable to use the toxic-comments plugin. Visit https://www.perspectiveapi.com/ to request API access.'); } - else { - res.boom.badRequest('No comment provided'); + + const {comment} = req.body; + + if(!comment) { + return next(ErrNoComment); + } + + try { + const scores = await perspective.getScores(apiKey, comment); + return res.json({ + comment, + score: scores.SEVERE_TOXICITY.summaryScore, + }); + } catch(err) { + return next(err); } }); diff --git a/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql new file mode 100644 index 000000000..7352db00f --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql @@ -0,0 +1,4 @@ +input CreateCommentInput { + checkToxicity: Boolean +} + diff --git a/plugins/talk-plugin-toxic-comments/yarn.lock b/plugins/talk-plugin-toxic-comments/yarn.lock index 8097789e1..fb57ccd13 100644 --- a/plugins/talk-plugin-toxic-comments/yarn.lock +++ b/plugins/talk-plugin-toxic-comments/yarn.lock @@ -2,150 +2,3 @@ # yarn lockfile v1 -axios@^0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" - dependencies: - follow-redirects "^1.2.3" - is-buffer "^1.1.5" - -body-parser@^1.17.2: - version "1.17.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "2.6.7" - depd "~1.1.0" - http-errors "~1.6.1" - iconv-lite "0.4.15" - on-finished "~2.3.0" - qs "6.4.0" - raw-body "~2.2.0" - type-is "~1.6.15" - -boom@3.2.x: - version "3.2.2" - resolved "https://registry.yarnpkg.com/boom/-/boom-3.2.2.tgz#0f0cc5d04adc5003b8c7d71f42cca7271fef0e78" - dependencies: - hoek "4.x.x" - -bytes@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" - -content-type@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" - -debug@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" - dependencies: - ms "2.0.0" - -debug@^2.4.5: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - dependencies: - ms "2.0.0" - -depd@1.1.0, depd@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -express-boom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/express-boom/-/express-boom-2.0.0.tgz#d400b940e961a8aa2ed8e3f77c595fa1e41b64b3" - dependencies: - boom "3.2.x" - -follow-redirects@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea" - dependencies: - debug "^2.4.5" - -hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" - -http-errors@~1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" - dependencies: - depd "1.1.0" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -iconv-lite@0.4.15: - version "0.4.15" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" - -mime-types@~2.1.15: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" - dependencies: - mime-db "~1.27.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -qs@6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -raw-body@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" - dependencies: - bytes "2.4.0" - iconv-lite "0.4.15" - unpipe "1.0.0" - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - -"statuses@>= 1.3.1 < 2": - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - -type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 8b99dd557..72c163d35 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -16,8 +16,8 @@ describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); const query = ` - mutation CreateComment($comment: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) { - createComment(comment: $comment) { + mutation CreateComment($input: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) { + createComment(input: $input) { comment { id status @@ -176,7 +176,7 @@ describe('graph.mutations.createComment', () => { const context = new Context({user: new UserModel({status: 'ACTIVE'})}); return graphql(schema, query, {}, context, { - comment: { + input: { asset_id: '123', body } diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index 4cb4f1e6a..761cd44c9 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -45,7 +45,7 @@ describe('graph.mutations.removeTag', () => { console.error(response.errors); } expect(response.errors).to.be.empty; - expect(response.data.removeTag.errors).to.be.null; + expect(response.data.removeTag).to.be.null; let retrievedComment = await CommentsService.findById(comment.id);