mirror of
https://github.com/wassname/talk.git
synced 2026-08-05 13:30:26 +08:00
merge master
This commit is contained in:
@@ -4,6 +4,10 @@ const {
|
||||
arrayJoinBy
|
||||
} = require('./util');
|
||||
const DataLoader = require('dataloader');
|
||||
const {
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
|
||||
SEARCH_OTHERS_COMMENTS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const CommentModel = require('../../models/comment');
|
||||
const UsersService = require('../../services/users');
|
||||
@@ -120,7 +124,7 @@ const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgn
|
||||
const ignoredUsers = freshUser.ignoresUsers;
|
||||
query.author_id = {$nin: ignoredUsers};
|
||||
}
|
||||
|
||||
|
||||
return CommentModel.where(query).count();
|
||||
};
|
||||
|
||||
@@ -191,7 +195,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 +214,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();
|
||||
@@ -226,7 +234,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
|
||||
|
||||
// Only administrators can search for comments with statuses that are not
|
||||
// `null`, or `'ACCEPTED'`.
|
||||
if (user != null && user.hasRoles('ADMIN') && statuses) {
|
||||
if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses) {
|
||||
comments = comments.where({
|
||||
status: {
|
||||
$in: statuses
|
||||
@@ -249,7 +257,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
|
||||
}
|
||||
|
||||
// Only let an admin request any user or the current user request themself.
|
||||
if (user && (user.hasRoles('ADMIN') || user.id === author_id) && author_id != null) {
|
||||
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === author_id) && author_id != null) {
|
||||
comments = comments.where({author_id});
|
||||
}
|
||||
|
||||
@@ -399,7 +407,7 @@ const genRecentComments = (_, ids) => {
|
||||
*/
|
||||
const genComments = ({user}, ids) => {
|
||||
let comments;
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
if (user && user.can(SEARCH_OTHERS_COMMENTS)) {
|
||||
comments = CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,6 +2,7 @@ const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
@@ -45,7 +46,7 @@ const deleteAction = ({user}, {id}) => {
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
if (context.user && context.user.can('mutation:createAction', 'mutation:deleteAction')) {
|
||||
if (context.user && context.user.can(CREATE_ACTION, DELETE_ACTION)) {
|
||||
return {
|
||||
Action: {
|
||||
create: (action) => createAction(context, action),
|
||||
|
||||
+109
-5
@@ -1,11 +1,19 @@
|
||||
const errors = require('../../errors');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
const CommentsService = require('../../services/comments');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const linkify = require('linkify-it')();
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
const {
|
||||
CREATE_COMMENT,
|
||||
SET_COMMENT_STATUS,
|
||||
ADD_COMMENT_TAG,
|
||||
EDIT_COMMENT
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const debug = require('debug')('talk:graph:mutators:tags');
|
||||
const plugins = require('../../services/plugins');
|
||||
@@ -47,7 +55,7 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
|
||||
}
|
||||
|
||||
// Add the staff tag for comments created as a staff member.
|
||||
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
|
||||
if (user.can(ADD_COMMENT_TAG)) {
|
||||
tags.push(TagsService.newTagLink(user, {
|
||||
name: 'STAFF',
|
||||
item_type
|
||||
@@ -57,6 +65,82 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
|
||||
return tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -132,6 +216,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) {
|
||||
@@ -169,6 +254,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';
|
||||
};
|
||||
|
||||
@@ -225,7 +326,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);
|
||||
|
||||
@@ -242,6 +342,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;
|
||||
};
|
||||
|
||||
@@ -274,15 +378,15 @@ module.exports = (context) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can('mutation:createComment')) {
|
||||
if (context.user && context.user.can(CREATE_COMMENT)) {
|
||||
mutators.Comment.create = (comment) => createPublicComment(context, comment);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can('mutation:setCommentStatus')) {
|
||||
if (context.user && context.user.can(SET_COMMENT_STATUS)) {
|
||||
mutators.Comment.setStatus = (action) => setStatus(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can('mutation:editComment')) {
|
||||
if (context.user && context.user.can(EDIT_COMMENT)) {
|
||||
mutators.Comment.edit = (action) => edit(context, action);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const TagsService = require('../../services/tags');
|
||||
const errors = require('../../errors');
|
||||
const {ADD_COMMENT_TAG, REMOVE_COMMENT_TAG} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* Modifies the targeted model with the specified operation to add/remove a tag.
|
||||
@@ -26,11 +27,11 @@ module.exports = (context) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can('mutation:addTag')) {
|
||||
if (context.user && context.user.can(ADD_COMMENT_TAG)) {
|
||||
mutators.Tag.add = (tag) => modify(context, TagsService.add, tag);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can('mutation:removeTag')) {
|
||||
if (context.user && context.user.can(REMOVE_COMMENT_TAG)) {
|
||||
mutators.Tag.remove = (tag) => modify(context, TagsService.remove, tag);
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -1,12 +1,17 @@
|
||||
const errors = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants');
|
||||
|
||||
const setUserStatus = ({user}, {id, status}) => {
|
||||
return UsersService.setStatus(id, status);
|
||||
};
|
||||
|
||||
const suspendUser = ({user}, {id, message}) => {
|
||||
return UsersService.suspendUser(id, message);
|
||||
const suspendUser = ({user}, {id, message, until}) => {
|
||||
return UsersService.suspendUser(id, message, until);
|
||||
};
|
||||
|
||||
const rejectUsername = ({user}, {id, message}) => {
|
||||
return UsersService.rejectUsername(id, message);
|
||||
};
|
||||
|
||||
const ignoreUser = ({user}, userToIgnore) => {
|
||||
@@ -22,18 +27,23 @@ module.exports = (context) => {
|
||||
User: {
|
||||
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
suspendUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
rejectUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
ignoreUser: (action) => ignoreUser(context, action),
|
||||
stopIgnoringUser: (action) => stopIgnoringUser(context, action),
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can('mutation:setUserStatus')) {
|
||||
if (context.user && context.user.can(SET_USER_STATUS)) {
|
||||
mutators.User.setUserStatus = (action) => setUserStatus(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can('mutation:suspendUser')) {
|
||||
if (context.user && context.user.can(SUSPEND_USER)) {
|
||||
mutators.User.suspendUser = (action) => suspendUser(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REJECT_USERNAME)) {
|
||||
mutators.User.rejectUsername = (action) => rejectUsername(context, action);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
|
||||
|
||||
const Action = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
@@ -11,7 +13,7 @@ const Action = {
|
||||
// This will load the user for the specific action. We'll limit this to the
|
||||
// admin users only or the current logged in user.
|
||||
user({user_id}, _, {loaders: {Users}, user}) {
|
||||
if (user && (user.hasRole('ADMIN') || user_id === user.id)) {
|
||||
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
|
||||
return Users.getByID.load(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ const Comment = {
|
||||
},
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
if (user && user.can('SEARCH_ACTIONS')) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
@@ -50,10 +49,12 @@ const Comment = {
|
||||
asset({asset_id}, _, {loaders: {Assets}}) {
|
||||
return Assets.getByID.load(asset_id);
|
||||
},
|
||||
editing(comment) {
|
||||
async editing(comment, _, {loaders: {Settings}}) {
|
||||
const settings = await Settings.load();
|
||||
const editableUntil = new Date(Number(comment.created_at) + settings.editCommentWindowLength);
|
||||
return {
|
||||
edited: comment.edited,
|
||||
editableUntil: comment.editableUntil
|
||||
editableUntil: editableUntil
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,8 +19,11 @@ const RootMutation = {
|
||||
setUserStatus(_, {id, status}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.setUserStatus({id, status}));
|
||||
},
|
||||
suspendUser(_, {id, message}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.suspendUser({id, message}));
|
||||
suspendUser(_, {input: {id, message, until}}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.suspendUser({id, message, until}));
|
||||
},
|
||||
rejectUsername(_, {input: {id, message}}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.rejectUsername({id, message}));
|
||||
},
|
||||
ignoreUser(_, {id}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.ignoreUser({id}));
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const {
|
||||
SEARCH_ASSETS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
SEARCH_OTHER_USERS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const RootQuery = {
|
||||
assets(_, args, {loaders: {Assets}, user}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -19,38 +26,36 @@ 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.hasRoles('ADMIN') && 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});
|
||||
if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
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}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
|
||||
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}}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,7 +67,7 @@ const RootQuery = {
|
||||
},
|
||||
|
||||
commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
if (user == null || !user.can(SEARCH_COMMENT_METRICS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -79,21 +84,27 @@ const RootQuery = {
|
||||
return user;
|
||||
},
|
||||
|
||||
// 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}}) {
|
||||
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
// this returns an arbitrary user
|
||||
user(_, {id}, {user, loaders: {Users}}) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = {limit, cursor, sort};
|
||||
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}, {user, loaders: {Users, Actions}}) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const {SEARCH_OTHER_USERS} = require('../../perms/constants');
|
||||
|
||||
const TagLink = {
|
||||
assigned_by({assigned_by}, _, {user, loaders: {Users}}) {
|
||||
if (user && user.hasRoles('ADMIN') && assigned_by != null) {
|
||||
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
|
||||
return Users.getByID.load(assigned_by);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-4
@@ -1,4 +1,12 @@
|
||||
const {decorateWithTags} = require('./util');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const {
|
||||
SEARCH_ACTIONS,
|
||||
SEARCH_OTHER_USERS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const User = {
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
@@ -7,26 +15,42 @@ const User = {
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
if (user && user.can(SEARCH_ACTIONS)) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
},
|
||||
created_at({roles, created_at}, _, {user}) {
|
||||
if (user && user.can(SEARCH_OTHER_USERS)) {
|
||||
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
|
||||
// the comments.
|
||||
if (user && (user.hasRoles('ADMIN') || user.id === id)) {
|
||||
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === id)) {
|
||||
return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
profiles({profiles}, _, {user}) {
|
||||
|
||||
// if the user is not an admin, do not return the profiles
|
||||
if (user && user.can(SEARCH_OTHER_USERS)) {
|
||||
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
|
||||
// member to access the ignoredUsers of a given user.
|
||||
if (!user || ((user.id !== id) && !(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')))) {
|
||||
if (!user || ((user.id !== id) && !user.can(SEARCH_OTHER_USERS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,11 +64,18 @@ const User = {
|
||||
roles({id, roles}, _, {user}) {
|
||||
|
||||
// If the user is not an admin, only return the current user's roles.
|
||||
if (user && (user.hasRoles('ADMIN') || user.id === id)) {
|
||||
if (user && (user.can(UPDATE_USER_ROLES) || user.id === id)) {
|
||||
return roles;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
// Extract the reliability from the user metadata if they have permission.
|
||||
reliable(user, _, {user: requestingUser}) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
|
||||
return KarmaService.model(user);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const {ADD_COMMENT_TAG} = require('../../perms/constants');
|
||||
|
||||
/**
|
||||
* Decorates the typeResolver with the tags field.
|
||||
*/
|
||||
const decorateWithTags = (typeResolver) => {
|
||||
typeResolver.tags = ({tags = []}, _, {user}) => {
|
||||
if (user && (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) {
|
||||
if (user && user.can(ADD_COMMENT_TAG)) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
+10
-11
@@ -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.
|
||||
|
||||
+79
-2
@@ -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]
|
||||
|
||||
# the tags on the user
|
||||
tags: [TagLink!]
|
||||
|
||||
@@ -51,6 +81,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
|
||||
}
|
||||
@@ -196,6 +231,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]
|
||||
}
|
||||
@@ -441,6 +480,7 @@ type Settings {
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
|
||||
organizationName: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -589,6 +629,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!]
|
||||
@@ -714,6 +757,29 @@ input CreateDontAgreeInput {
|
||||
message: String
|
||||
}
|
||||
|
||||
# Input for suspendUser mutation.
|
||||
input SuspendUserInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
|
||||
# target user will be suspended until this date.
|
||||
until: Date!
|
||||
}
|
||||
|
||||
# Input for rejectUsername mutation.
|
||||
input RejectUsernameInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
}
|
||||
|
||||
# DeleteActionResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type DeleteActionResponse implements Response {
|
||||
@@ -738,6 +804,14 @@ type SuspendUserResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# RejectUsernameResponse is the response returned with possibly some errors
|
||||
# relating to the reject username action attempt.
|
||||
type RejectUsernameResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetCommentStatusResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type SetCommentStatusResponse implements Response {
|
||||
@@ -820,8 +894,11 @@ type RootMutation {
|
||||
# Sets User status. Requires the `ADMIN` role.
|
||||
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
|
||||
|
||||
# Sets User status to BANNED and canEditName to true. It sends a message to the banned User. Requires the `ADMIN` role.
|
||||
suspendUser(id: ID!, message: String): SuspendUserResponse
|
||||
# Suspends a user. Requires the `ADMIN` role.
|
||||
suspendUser(input: SuspendUserInput!): SuspendUserResponse
|
||||
|
||||
# Suspends a user. Requires the `ADMIN` role.
|
||||
rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse
|
||||
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
|
||||
|
||||
Reference in New Issue
Block a user