Compare commits

...
14 Commits
Author SHA1 Message Date
Wyatt Johnson 620273769f Update package.json (#1818) 2018-08-24 12:22:23 -06:00
Wyatt Johnson dff3d79569 Subscription Optimizations (#1817)
* fix: optimized user extraction during subscription operations

* fix: fixed query

* feat: added user refeshing batching
2018-08-24 16:14:18 +00:00
Suriyaa ✌️️ 0904453e41 Remove Node Security Platform Badge (#1813) 2018-08-22 16:04:58 +00:00
Helmut Januschka 90eafb557c fix language detection in IE11 and Edge (#1810) 2018-08-21 17:52:02 +00:00
Kim Gardner 4dbd1e0bb3 Add version tip to docs (#1790) 2018-08-08 17:03:23 +00:00
Kim Gardner cbda970437 Merge pull request #1789 from coralproject/release-4-6-1
Bump version 4.6.1
2018-08-08 17:42:19 +01:00
Wyatt Johnson eecd814e53 Merge branch 'master' into release-4-6-1 2018-08-08 16:31:42 +00:00
Kim Gardner fb4b6330da 4.6.1 Release 2018-08-08 17:30:01 +01:00
Kim Gardner eb24b5c027 Merge pull request #1785 from coralproject/comment-edit-moderation
Comment Editing Passthrough
2018-08-08 16:44:17 +01:00
Kim Gardner 3346f5de77 Merge branch 'master' into comment-edit-moderation 2018-08-08 14:39:45 +01:00
Kim Gardner 1a69cea9f4 Merge pull request #1786 from coralproject/lowercasedSearch
Case sensitive displayName Issue
2018-08-08 14:38:43 +01:00
Wyatt Johnson d763f0b8f6 fix: added lowercased/case searches for displayName 2018-08-07 16:31:51 -06:00
Wyatt Johnson 6bb98a00dc fix: added support for comment edit mod passthrough 2018-08-07 13:56:23 -06:00
Leandro 2edb8e8d0a use config variable scheduledDeletionDelayHours to schedule deletion time on hoc (#1775) 2018-08-01 21:48:11 +00:00
10 changed files with 257 additions and 117 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946/badge)](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests)
# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests)
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk).
+9 -3
View File
@@ -75,10 +75,15 @@ let TIMEAGO_INSTANCE;
// detectLanguage will try to get the locale from storage if available,
// otherwise will try to get it from the navigator, otherwise, it will fallback
// to the default language.
const detectLanguage = () =>
first(
const detectLanguage = () => {
var browserLanguages = navigator.languages;
//IE11 and MS-EDGE do not provide navigator.languages
if (!browserLanguages) {
browserLanguages = [navigator.language];
}
return first(
negotiateLanguages(
navigator.languages,
browserLanguages,
whitelistedLanguages || supportedLocales,
{
defaultLocale,
@@ -86,6 +91,7 @@ const detectLanguage = () =>
}
)
);
};
export function setupTranslations() {
// locale
@@ -3,6 +3,10 @@ title: Troubleshooting Tips
permalink: /troubleshooting-tips/
---
## How do I find out what version I'm running?
If you visit https://<YOUR TALK INSTANCE>/api/v1, it will return the version you're running and the hash for the latest commit.
## I've installed Talk but I can't see the comment stream appear on my articles
* Make sure you've adding the correct domains to your Permitted Domains in Configure > Tech Settings
+6 -6
View File
@@ -84,10 +84,10 @@ const getUsersByQuery = async (
if (value.length > 0) {
// Lowercase the search term and escape any regex characters.
value = escapeRegExp(value).toLowerCase();
value = escapeRegExp(value);
// Compile the prefix search regex.
const $regex = new RegExp(`^${value}`);
const lowercasedRegex = new RegExp(`^${value.toLowerCase()}`);
const notLowercasedRegex = new RegExp(`^${value}`);
// Merge in the regex params.
query.merge({
@@ -95,7 +95,7 @@ const getUsersByQuery = async (
// Search by a prefix match on the username.
{
lowercaseUsername: {
$regex,
$regex: lowercasedRegex,
},
},
@@ -104,7 +104,7 @@ const getUsersByQuery = async (
profiles: {
$elemMatch: {
id: {
$regex,
$regex: lowercasedRegex,
},
provider: 'local',
},
@@ -114,7 +114,7 @@ const getUsersByQuery = async (
// Search by the displayName metadata field.
{
'metadata.displayName': {
$regex,
$regex: notLowercasedRegex,
},
},
],
+17 -2
View File
@@ -293,7 +293,16 @@ const setStatus = async (ctx, { id, status }) => {
*/
const editComment = async (
ctx,
{ id, asset_id, edit: { body, metadata = {} } }
{
id,
asset_id,
edit: {
body,
metadata = {},
status: commentStatus,
actions: commentActions = [],
},
}
) => {
const {
connectors: {
@@ -303,7 +312,13 @@ const editComment = async (
// Build up the new comment we're setting. We need to check this with
// moderation now.
let comment = { id, asset_id, body };
let comment = {
id,
asset_id,
body,
status: commentStatus,
actions: commentActions,
};
// Determine the new status of the comment.
const { actions, status } = await Moderation.process(ctx, comment);
+87 -12
View File
@@ -1,11 +1,14 @@
const { SubscriptionManager } = require('graphql-subscriptions');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const debug = require('debug')('talk:graph:subscriptions');
const DataLoader = require('dataloader');
const { getPubsub } = require('./pubsub');
const schema = require('../schema');
const Context = require('../context');
const plugins = require('../../services/plugins');
const User = require('../../models/user');
const { singleJoinBy } = require('../loaders/util');
const { deserializeUser } = require('../../services/subscriptions');
const setupFunctions = require('./setupFunctions');
@@ -59,31 +62,103 @@ const onConnect = async (connectionParams, connection) => {
}`;
}
try {
// Pull the user off of the upgrade request.
const hydratedRequest = await deserializeUser(connection.upgradeReq);
// Update the connections upgrade request, as we'll use that to verify that
// the user is allowed each operation.
connection.upgradeReq = hydratedRequest;
} catch (err) {
console.error(err);
}
// Call all the hooks.
await Promise.all(
hooks.onConnect.map(hook => hook(connectionParams, connection))
);
};
const onOperation = (parsedMessage, baseParams, connection) => {
// Cache the upgrade request.
let upgradeReq = connection.upgradeReq;
/**
* batchedUserRefresher will get users based on ID for websocket user refresh
* operations to reduce load related to user refreshing.
*/
const batchedUserRefresher = new DataLoader(
userIDs => {
console.log(`OPERATION: refreshing ${userIDs.length} users.`);
return User.find({ id: { $in: userIDs } }).then(
singleJoinBy(userIDs, 'id')
);
},
{
// Disable the cache, as this dataloader is long lived, and the point of
// using this dataloader is to batch refetch operations rather than caching
// then as we normally would.
cache: false,
}
);
// Attach the context per request.
baseParams.context = async () => {
let req;
const contextGenerator = req => {
// Pull the user(?) off the request.
const { user, jwt } = req;
try {
req = await deserializeUser(upgradeReq);
debug(`user ${req.user ? 'was' : 'was not'} on websocket request`);
} catch (e) {
console.error(e);
if (!user || !jwt) {
// There is no valid user on the request, let it continue as is then.
return async () => new Context(req);
}
return new Context({});
// Provide a flag that can be used to short circuit invalid requests.
let expiredLogin = false;
async function refreshUser() {
// Check to see if this request has been short circuited.
if (expiredLogin) {
// It has, let's exit here.
return null;
}
// Validate that the JWT for this user has not expired.
const { exp = false } = jwt;
if (exp && exp < Date.now() / 1000) {
// Mark that this token has expired, don't bother performing this syscall
// again to check the time.
expiredLogin = true;
return null;
}
try {
// Let's refresh the user from the database, as they may have changed.
const refreshedUser = await batchedUserRefresher.load(user.id);
if (!refreshedUser) {
return null;
}
return refreshedUser;
} catch (err) {
return null;
}
}
// Return the context builder function that'll use the passed context to
// generate future contexts.
return async () => {
// Refresh the user (potentially null).
const refreshedUser = await refreshUser();
// Attach the refreshedUser to the request.
req.user = refreshedUser;
// Return the new context.
return new Context(req);
};
};
const onOperation = async (parsedMessage, baseParams, connection) => {
// Pull the upgrade request off of the connection.
const upgradeReq = connection.upgradeReq;
// Attach the context handler to the request.
baseParams.context = contextGenerator(upgradeReq);
return baseParams;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.6.0",
"version": "4.6.2",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
+87 -68
View File
@@ -25,85 +25,104 @@ let enabled = true;
// }
// });
async function checkForSpam(ctx, { asset_id, body }) {
const req = ctx.parent.parent;
const loaders = ctx.loaders;
//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(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: body,
is_test: false,
});
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
return spam;
} catch (err) {
console.trace(err);
return;
}
}
function handlePositiveSpam(input) {
// 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: {},
});
}
module.exports = {
RootMutation: {
createComment: {
async pre(_, { input }, ctx) {
const req = ctx.parent.parent;
const loaders = ctx.loaders;
//If the key validation failed, then we can't run with the client.
if (!enabled) {
debug('not enabled, passing');
return;
editComment: {
pre: async (_, { asset_id, edit: { body }, edit }, ctx) => {
const spam = await checkForSpam(ctx, { asset_id, body });
if (spam) {
// Mark the comment as positive spam.
handlePositiveSpam(edit);
}
let spam = false;
try {
const user_ip = get(req, 'ip', false);
if (!user_ip) {
debug('no ip on request');
return;
},
},
createComment: {
pre: async (_, { input }, ctx) => {
const spam = await checkForSpam(ctx, input);
if (spam) {
if (input.checkSpam) {
throw new ErrSpam();
}
// 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: false,
});
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
} catch (err) {
console.trace(err);
return;
// Mark the comment as positive spam.
handlePositiveSpam(input);
}
// Attach scores to metadata.
input.metadata = merge({}, input.metadata || {}, {
akismet: spam,
});
if (spam) {
if (input.checkSpam) {
throw new 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: {},
});
}
},
},
},
@@ -3,6 +3,8 @@ import { gql } from 'react-apollo';
import moment from 'moment';
import update from 'immutability-helper';
import { scheduledDeletionDelayHours } from '../../config';
export const withRequestDownloadLink = withMutation(
gql`
mutation DownloadCommentHistory {
@@ -48,7 +50,7 @@ export const withRequestAccountDeletion = withMutation(
});
const scheduledDeletionDate = moment()
.add(24, 'hours')
.add(scheduledDeletionDelayHours, 'hours')
.toDate();
const data = update(prev, {
@@ -1,40 +1,59 @@
const { getScores, isToxic } = require('./perspective');
const { ErrToxic } = require('./errors');
function handlePositiveToxic(input) {
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: 'TOXIC_COMMENT',
metadata: {},
});
}
async function getScore(body) {
// Try getting scores.
let scores;
try {
scores = await getScores(body);
} catch (err) {
// Warn and let mutation pass.
console.trace(err); // TODO: log/handle this differently?
return;
}
return scores;
}
module.exports = {
RootMutation: {
editComment: {
pre: async (_, { edit: { body }, edit }) => {
const scores = await getScore(body);
if (isToxic(scores)) {
handlePositiveToxic(edit);
}
},
},
createComment: {
async pre(_, { input }, _context, _info) {
// Try getting scores.
let scores;
try {
scores = await getScores(input.body);
} catch (err) {
// Warn and let mutation pass.
console.trace(err); // TODO: log/handle this differently?
return;
}
// Attach scores to metadata.
input.metadata = Object.assign({}, input.metadata, {
perspective: scores,
});
const scores = await getScore(input.body);
if (isToxic(scores)) {
if (input.checkToxicity) {
throw new ErrToxic();
}
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: 'TOXIC_COMMENT',
metadata: {},
});
// Mark the comment as positive toxic.
handlePositiveToxic(input);
}
// Attach scores to metadata.
input.metadata = Object.assign({}, input.metadata, {
perspective: scores,
});
},
},
},