merge master

This commit is contained in:
riley
2017-05-18 12:18:00 -06:00
30 changed files with 841 additions and 76 deletions
+6 -2
View File
@@ -120,7 +120,7 @@ const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgn
const ignoredUsers = freshUser.ignoresUsers;
query.author_id = {$nin: ignoredUsers};
}
return CommentModel.where(query).count();
};
@@ -191,7 +191,7 @@ const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) =>
* @return {Promise} resolves to the counts of the comments from the
* query
*/
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id}) => {
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id}) => {
let query = CommentModel.find();
if (ids) {
@@ -210,6 +210,10 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id}) =
query = query.where({parent_id});
}
if (author_id) {
query = query.where({author_id});
}
return CommentModel
.find(query)
.count();
+9 -1
View File
@@ -15,7 +15,7 @@ const genUserByIDs = (context, ids) => UsersService
* @param {Object} context graph context
* @param {Object} query query terms to apply to the users query
*/
const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => {
let users = UserModel.find();
@@ -27,6 +27,14 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
});
}
if (statuses != null) {
users = users.where({
status: {
$in: statuses
}
});
}
if (cursor) {
if (sort === 'REVERSE_CHRONOLOGICAL') {
users = users.where({
+100 -1
View File
@@ -1,12 +1,91 @@
const debug = require('debug')('talk:graph:mutators:comment');
const errors = require('../../errors');
const ActionModel = require('../../models/action');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const CommentsService = require('../../services/comments');
const KarmaService = require('../../services/karma');
const linkify = require('linkify-it')();
const Wordlist = require('../../services/wordlist');
/**
* adjustKarma will adjust the affected user's karma depending on the moderators
* action.
*/
const adjustKarma = (Comments, id, status) => async () => {
try {
// Use the dataloader to get the comment that was just moderated and
// get the flag user's id's so we can adjust their karma too.
let [
comment,
flagUserIDs
] = await Promise.all([
// Load the comment that was just made/updated by the setCommentStatus
// operation.
Comments.get.load(id),
// Find all the flag actions that were referenced by this comment
// at this point in time.
ActionModel.find({
item_id: id,
item_type: 'COMMENTS',
action_type: 'FLAG'
}).then((actions) => {
// This is to ensure that this is always an array.
if (!actions) {
return [];
}
return actions.map(({user_id}) => user_id);
})
]);
debug(`Comment[${id}] by User[${comment.author_id}] was Status[${status}]`);
switch (status) {
case 'REJECTED':
// Reduce the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma reduced`);
// Decrease the flag user's karma, the moderator disagreed with this
// action.
debug(`FlaggingUser[${flagUserIDs.join(', ')}] had their karma increased`);
await Promise.all([
KarmaService.modifyUser(comment.author_id, -1, 'comment'),
KarmaService.modifyUser(flagUserIDs, 1, 'flag', true)
]);
break;
case 'ACCEPTED':
// Increase the user's karma.
debug(`CommentUser[${comment.author_id}] had their karma increased`);
// Increase the flag user's karma, the moderator agreed with this
// action.
debug(`FlaggingUser[${flagUserIDs.join(', ')}] had their karma reduced`);
await Promise.all([
KarmaService.modifyUser(comment.author_id, 1, 'comment'),
KarmaService.modifyUser(flagUserIDs, -1, 'flag', true)
]);
break;
}
return;
} catch (e) {
console.error(e);
}
};
/**
* Creates a new comment.
* @param {Object} user the user performing the request
@@ -86,6 +165,7 @@ const filterNewComment = (context, {body, asset_id}) => {
* @return {Promise} resolves to the comment's status
*/
const resolveNewCommentStatus = async (context, {asset_id, body}, wordlist = {}, settings = {}) => {
let {user} = context;
// Check to see if the body is too short, if it is, then complain about it!
if (body.length < 2) {
@@ -123,6 +203,22 @@ const resolveNewCommentStatus = async (context, {asset_id, body}, wordlist = {},
return 'REJECTED';
}
if (user && user.metadata) {
// If the user is not a reliable commenter (passed the unreliability
// threshold by having too many rejected comments) then we can change the
// status of the comment to `PREMOD`, therefore pushing the user's comments
// away from the public eye until a moderator can manage them. This of
// course can only be applied if the comment's current status is `NONE`,
// we don't want to interfere if the comment was rejected.
if (KarmaService.isReliable('comment', user.metadata.trust) === false) {
// Update the response from the comment creation to add the PREMOD so that
// that user's UI will reflect the fact that their comment is in pre-mod.
return 'PREMOD';
}
}
return moderation === 'PRE' ? 'PREMOD' : 'NONE';
};
@@ -179,7 +275,6 @@ const createPublicComment = async (context, commentInput) => {
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
*/
const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
let comment = await CommentsService.pushStatus(id, status, user ? user.id : null);
@@ -196,6 +291,10 @@ const setStatus = async ({user, loaders: {Comments}}, {id, status}) => {
Comments.countByAssetID.clear(comment.asset_id);
// postSetCommentStatus will use the arguments from the mutation and
// adjust the affected user's karma in the next tick.
process.nextTick(adjustKarma(Comments, id, status));
return comment;
};
+23 -19
View File
@@ -19,34 +19,32 @@ const RootQuery = {
// This endpoint is used for loading moderation queues, so hide it in the
// event that we aren't an admin.
async comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) {
let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored};
async comments(_, {query}, {user, loaders: {Comments, Actions}}) {
let {action_type} = query;
if (user != null && user.can('SEARCH_OTHERS_COMMENTS') && action_type) {
let ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
// Perform the query using the available resolver.
return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored});
query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
}
return Comments.getByQuery(query);
},
comment(_, {id}, {loaders: {Comments}}) {
return Comments.get.load(id);
},
async commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) {
async commentCount(_, {query}, {user, loaders: {Actions, Comments}}) {
if (user == null || !user.can('SEARCH_OTHERS_COMMENTS')) {
return null;
}
if (action_type) {
let ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
const {action_type} = query;
// Perform the query using the available resolver.
return Comments.getCountByQuery({ids, statuses, asset_id, parent_id});
if (action_type) {
query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
}
return Comments.getCountByQuery({statuses, asset_id, parent_id});
return Comments.getCountByQuery(query);
},
assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) {
@@ -79,21 +77,27 @@ const RootQuery = {
return user;
},
// this returns an arbitrary user
user(_, {id}, {user, loaders: {Users}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
return Users.getByID.load(id);
},
// This endpoint is used for loading the user moderation queues (users whose username has been flagged),
// so hide it in the event that we aren't an admin.
async users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) {
async users(_, {query}, {user, loaders: {Users, Actions}}) {
if (user == null || !user.can('SEARCH_OTHER_USERS')) {
return null;
}
const query = {limit, cursor, sort};
const {action_type} = query;
if (action_type) {
let ids = await Actions.getByTypes({action_type, item_type: 'USERS'});
// Perform the query using the available resolver.
return Users.getByQuery({ids, limit, cursor, sort}).find({status: 'PENDING'});
query.ids = await Actions.getByTypes({action_type, item_type: 'USERS'});
query.statuses = ['PENDING'];
}
return Users.getByQuery(query);
+25
View File
@@ -1,3 +1,5 @@
const KarmaService = require('../../services/karma');
const User = {
action_summaries({id}, _, {loaders: {Actions}}) {
return Actions.getSummariesByItemID.load(id);
@@ -10,6 +12,13 @@ const User = {
}
},
created_at({roles, created_at}, _, {user}) {
if (user && user.hasRoles('ADMIN')) {
return created_at;
}
return null;
},
comments({id}, _, {loaders: {Comments}, user}) {
// If the user is not an admin, only return comment list for the owner of
@@ -20,6 +29,15 @@ const User = {
return null;
},
profiles({profiles}, _, {user}) {
// if the user is not an admin, do not return the profiles
if (user && user.hasRoles('ADMIN')) {
return profiles;
}
return null;
},
ignoredUsers({id}, args, {user, loaders: {Users}}) {
// Only allow a logged in user that is either the current user or is a staff
@@ -43,6 +61,13 @@ const User = {
}
return null;
},
// Extract the reliability from the user metadata if they have permission.
reliable(user, _, {user: requestingUser}) {
if (requestingUser && requestingUser.hasRoles('ADMIN')) {
return KarmaService.model(user);
}
}
};
+10 -11
View File
@@ -1,6 +1,7 @@
const {SubscriptionManager} = require('graphql-subscriptions');
const {SubscriptionServer} = require('subscriptions-transport-ws');
const _ = require('lodash');
const debug = require('debug')('talk:graph:subscriptions');
const pubsub = require('./pubsub');
const schema = require('./schema');
@@ -9,24 +10,22 @@ const plugins = require('../services/plugins');
const {deserializeUser} = require('../services/subscriptions');
// Core setup functions
let setupFunctions = {
commentAdded: (options, args) => ({
commentAdded: {
filter: (comment) => comment.asset_id === args.asset_id
},
}),
};
/**
* Plugin support requires that we merge in existing setupFunctions with our new
* plugin based ones. This allows plugins to extend existing setupFunctions as well
* as provide new ones.
*/
setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {setupFunctions}) => {
const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => {
debug(`added plugin '${plugin.name}'`);
return _.merge(acc, setupFunctions);
}, setupFunctions);
}, {
commentAdded: (options, args) => ({
commentAdded: {
filter: (comment) => comment.asset_id === args.asset_id
},
}),
});
/**
* This creates a new subscription manager.
+43 -1
View File
@@ -5,6 +5,22 @@
# Date represented as an ISO8601 string.
scalar Date
################################################################################
## Reliability
################################################################################
# Reliability defines how a given user should be considered reliable for their
# comment or flag activity.
type Reliability {
# flagger will be `true` when the flagger is reliable, `false` if not, or
# `null` if the reliability cannot be determined.
flagger: Boolean
# commenter will be `true` when the commenter is reliable, `false` if not, or
# `null` if the reliability cannot be determined.
commenter: Boolean
}
################################################################################
## Users
@@ -20,6 +36,14 @@ enum USER_ROLES {
MODERATOR
}
type UserProfile {
# the id is an identifier for the user profile (email, facebook id, etc)
id: String!
# name of the provider attached to the authentication mode
provider: String!
}
# Any person who can author comments, create actions, and view comments on a
# stream.
type User {
@@ -30,6 +54,9 @@ type User {
# Username of a user.
username: String!
# creation date of user
created_at: String!
# Action summaries against the user.
action_summaries: [ActionSummary!]!
@@ -39,6 +66,9 @@ type User {
# the current roles of the user.
roles: [USER_ROLES!]
# the current profiles of the user.
profiles: [UserProfile]
# determines whether the user can edit their username
canEditName: Boolean
@@ -48,6 +78,11 @@ type User {
# returns all comments based on a query.
comments(query: CommentsQuery): [Comment!]
# reliable is the reference to a given user's Reliability. If the requesting
# user does not have permission to access the reliability, null will be
# returned.
reliable: Reliability
# returns user status
status: USER_STATUS
}
@@ -159,6 +194,10 @@ input CommentCountQuery {
# type.
action_type: ACTION_TYPE
# author_id allows the querying of comment counts based on the author of the
# comments.
author_id: ID
# Filter by a specific tag name.
tag: [String]
}
@@ -549,6 +588,9 @@ type RootQuery {
# Users returned based on a query.
users(query: UsersQuery): [User]
# a single User by id
user(id: ID!): User
# Asset metrics related to user actions are saturated into the assets
# returned. Parameters `from` and `to` are related to the action created_at field.
assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
@@ -741,7 +783,7 @@ type EditCommentResponse implements Response {
comment: Comment
# An array of errors relating to the mutation that occured.
errors: [UserError]
errors: [UserError]
}
# All mutations for the application are defined on this object.