Merge branch 'master' into remember-sort

Conflicts:
	.eslintignore
	.gitignore
This commit is contained in:
Chi Vinh Le
2017-09-08 17:56:18 +07:00
45 changed files with 592 additions and 240 deletions
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,38 @@
import React from 'react';
/**
* CheckToxicityHook adds hooks to the `commentBox`
* that handles checking a comment for toxicity.
*/
export default class CheckToxicityHook extends React.Component {
// checked signifies if we already sent a request with the `checkToxicity` set to true.
checked = false;
componentDidMount() {
this.toxicityPreHook = this.props.registerHook('preSubmit', (input) => {
// If we haven't check the toxicity yet, make sure to include `checkToxicity=true` in the mutation.
// Otherwise post comment without checking the toxicity.
if (!this.checked) {
input.checkToxicity = true;
this.checked = true;
}
});
this.toxicityPostHook = this.props.registerHook('postSubmit', () => {
// Reset `checked` after comment was successfully posted.
this.checked = false;
});
}
componentWillUnmount() {
this.props.unregisterHook(this.toxicityPreHook);
this.props.unregisterHook(this.toxicityPostHook);
}
render() {
return null;
}
}
@@ -0,0 +1,9 @@
import translations from './translations.yml';
import CheckToxicityHook from './components/CheckToxicityHook';
export default {
translations,
slots: {
commentInputDetailArea: [CheckToxicityHook],
},
};
@@ -0,0 +1,6 @@
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.
es:
@@ -0,0 +1,8 @@
const {readFileSync} = require('fs');
const path = require('path');
const hooks = require('./server/hooks');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
hooks,
};
@@ -0,0 +1,9 @@
{
"name": "@coralproject/talk-plugin-toxicity",
"pluginName": "talk-plugin-toxicity",
"version": "0.0.1",
"description": "Provides support for measuring the toxicity of user comments using the Perspectives API",
"main": "index.js",
"author": "The Coral Project Team <coral@mozillafoundation.org>",
"license": "Apache-2.0"
}
@@ -0,0 +1,12 @@
const config = {
API_ENDPOINT: process.env.TALK_PERSPECTIVE_API_ENDPOINT || 'https://commentanalyzer.googleapis.com/v1alpha1',
API_KEY: process.env.TALK_PERSPECTIVE_API_KEY,
THRESHOLD: process.env.TALK_TOXICITY_THRESHOLD || 0.8,
API_TIMEOUT: process.env.TALK_PERSPECTIVE_TIMEOUT || 300,
};
if (process.env.NODE_ENV !== 'test' && !config.API_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.');
}
module.exports = config;
@@ -0,0 +1,13 @@
const {APIError} = require('errors');
// ErrToxic is sent during a `CreateComment` mutation where
// `input.checkToxicity` is set to true and the comment contains
// toxic language as determined by the perspective service.
const ErrToxic = new APIError('Comment is toxic', {
status: 400,
translation_key: 'COMMENT_IS_TOXIC',
});
module.exports = {
ErrToxic,
};
@@ -0,0 +1,67 @@
const {getScores, isToxic} = require('./perspective');
const {ErrToxic} = require('./errors');
const ActionsService = require('../../../services/actions');
// We don't add the hooks during _test_ as the perspective API is not available.
if (process.env.NODE_ENV === 'test') {
return null;
}
module.exports = {
RootMutation: {
createComment: {
async pre(_, {input}, _context, _info) {
let scores;
// Try getting scores.
try {
scores = await getScores(input.body);
}
catch(err) {
// Warn and let mutation pass.
console.trace(err);
return;
}
const commentIsToxic = isToxic(scores);
if (input.checkToxicity && commentIsToxic) {
throw ErrToxic;
}
// attach scores to metadata.
input.metadata = Object.assign({}, input.metadata, {
perspective: scores,
});
if (commentIsToxic) {
// TODO: this should have a different status than Premod.
input.status = 'PREMOD';
}
},
async post(_, _input, _context, _info, result) {
const metadata = result.comment.metadata;
if (metadata.perspective && isToxic(metadata.perspective)) {
// TODO: this is kind of fragile, we should refactor this to resolve
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
// defined in a checkable schema.
// Add a flag to the comment.
await ActionsService.create({
item_id: result.comment.id,
item_type: 'COMMENTS',
action_type: 'FLAG',
user_id: null,
group_id: 'Comment contains toxic language',
metadata: {}
});
}
return result;
},
},
},
};
@@ -0,0 +1,85 @@
const fetch = require('node-fetch');
const {API_ENDPOINT, API_KEY, THRESHOLD, API_TIMEOUT} = require('./config');
/**
* Get scores from the perspective api
* @param {string} text text to be anaylized
* @return {object} object containing toxicity scores
*/
async function getScores(text) {
const response = await fetch(`${API_ENDPOINT}/comments:analyze?key=${API_KEY}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
timeout: API_TIMEOUT,
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
},
};
}
/**
* Get toxicity probability
* @param {object} scores scores as returned by `getScores`
* @return {number} toxicity probability from 0 - 1.0
*/
function getProbability(scores) {
return scores.SEVERE_TOXICITY.summaryScore;
}
/**
* isToxic determines if given probabilty or scores meets the toxicity threshold.
* @param {object|number} scoresOrProbability scores or probability
* @return {boolean}
*/
function isToxic(scoresOrProbability) {
const probability = typeof scoresOrProbability === 'object'
? getProbability(scoresOrProbability)
: scoresOrProbability;
return probability > THRESHOLD;
}
/**
* maskKeyInError is a decorator that calls fn and masks the
* API_KEY in errors before throwing.
* @param {function} fn Function that returns a Promise
* @return {function} decorated function
*/
function maskKeyInError(fn) {
return async (...args) => {
try {
return await fn(...args);
}
catch(err) {
if (err.message) {
err.message = err.message.replace(API_KEY, '***');
}
throw err;
}
};
}
module.exports = {
getScores: maskKeyInError(getScores),
getProbability,
isToxic,
};
@@ -0,0 +1,7 @@
input CreateCommentInput {
# If true, the mutation will fail when the
# body contains toxic language.
checkToxicity: Boolean
}
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1