mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
620273769f | ||
|
|
dff3d79569 | ||
|
|
0904453e41 | ||
|
|
90eafb557c | ||
|
|
4dbd1e0bb3 | ||
|
|
cbda970437 | ||
|
|
eecd814e53 | ||
|
|
fb4b6330da | ||
|
|
eb24b5c027 | ||
|
|
3346f5de77 | ||
|
|
1a69cea9f4 | ||
|
|
d763f0b8f6 | ||
|
|
6bb98a00dc | ||
|
|
2edb8e8d0a |
@@ -1,4 +1,4 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user