diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 48966c5e8..04235c217 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -13,11 +13,16 @@ const suspendUser = ({user}, {id, message}) => { }); }; +const ignoreUser = async ({user}, userToIgnore) => { + return await UsersService.ignoreUsers(user.id, [userToIgnore.id]); +}; + module.exports = (context) => { let mutators = { User: { setUserStatus: () => Promise.reject(errors.ErrNotAuthorized), - suspendUser: () => Promise.reject(errors.ErrNotAuthorized) + suspendUser: () => Promise.reject(errors.ErrNotAuthorized), + ignoreUser: (action) => ignoreUser(context, action), } }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 3d5ea9ad9..0e763889f 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -23,6 +23,9 @@ const RootMutation = { suspendUser(_, {id, message}, {mutators: {User}}) { return wrapResponse(null)(User.suspendUser({id, message})); }, + ignoreUser(_, {id}, {mutators: {User}}) { + return wrapResponse(null)(User.ignoreUser({id})); + }, setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 32e2e4263..42f5a021d 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -83,6 +83,16 @@ const RootQuery = { return user; }, + myIgnoredUsers: async (_, args, {user, loaders: {Users}}) => { + + // get currentUser again since context.user was out of date when running test/graph/mutations/ignoreUser + const currentUser = (await Users.getByQuery({ids: [user.id], limit: 1}))[0]; + if ( ! (currentUser && Array.isArray(currentUser.ignoresUsers) && currentUser.ignoresUsers.length)) { + return []; + } + return await Users.getByQuery({ids: currentUser.ignoresUsers}); + }, + // 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. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 8b07c2ab2..86aafbb0e 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -537,6 +537,9 @@ type RootQuery { # role. me: User + # Users that the currently logged in user ignores + myIgnoredUsers: [User] + # Users returned based on a query. users(query: UsersQuery): [User] @@ -703,6 +706,13 @@ type RemoveCommentTagResponse implements Response { errors: [UserError] } +# Response to ignoreUser mutation +type IgnoreUserResponse implements Response { + # An array of errors relating to the mutation that occured. + errors: [UserError] +} + + # All mutations for the application are defined on this object. type RootMutation { @@ -735,6 +745,9 @@ type RootMutation { # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse + + # Ignore comments by another user + ignoreUser(id: ID!): IgnoreUserResponse } ################################################################################ diff --git a/models/user.js b/models/user.js index e1cbb466b..2663410a7 100644 --- a/models/user.js +++ b/models/user.js @@ -117,7 +117,13 @@ const UserSchema = new mongoose.Schema({ type: String, default: '' } - } + }, + + ignoresUsers: [{ + + // user id of another user + type: String, + }] }, { // This will ensure that we have proper timestamps available on this model. diff --git a/services/users.js b/services/users.js index a86a6686d..45b113f35 100644 --- a/services/users.js +++ b/services/users.js @@ -1,3 +1,4 @@ +const assert = require('assert'); const bcrypt = require('bcrypt'); const url = require('url'); const jwt = require('jsonwebtoken'); @@ -834,4 +835,23 @@ module.exports = class UsersService { throw err; }); } + + /** + * Ignore another user + * @param {String} userId the id of the user that is ignoring another users + * @param {String[]} usersToIgnore Array of user IDs to ignore + */ + static ignoreUsers(userId, usersToIgnore) { + assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array'); + assert(usersToIgnore.every(u => typeof u === 'string'), 'usersToIgnore is an array of string user IDs'); + + // TODO: For each usersToIgnore, make sure they exist? + return UserModel.update({id: userId}, { + $addToSet: { + ignoresUsers: { + $each: usersToIgnore + } + } + }); + } }; diff --git a/test/graph/mutations/ignoreUser.js b/test/graph/mutations/ignoreUser.js new file mode 100644 index 000000000..7dd103e5b --- /dev/null +++ b/test/graph/mutations/ignoreUser.js @@ -0,0 +1,55 @@ +const expect = require('chai').expect; +const {graphql} = require('graphql'); + +const schema = require('../../../graph/schema'); +const Context = require('../../../graph/context'); +const UsersService = require('../../../services/users'); +const SettingsService = require('../../../services/settings'); + +describe('graph.mutations.ignoreUser', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const ignoreUserMutation = ` + mutation ignoreUser ($id: ID!) { + ignoreUser(id:$id) { + errors { + translation_key + } + } + } + `; + + const getMyIgnoredUsersQuery = ` + query myIgnoredUsers { + myIgnoredUsers { + id, + username + } + } + `; + + it('users can ignoreUser', async () => { + const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); + const context = new Context({user}); + const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); + if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { + console.error(ignoreUserResponse.errors); + } + expect(ignoreUserResponse.errors).to.be.empty; + + // now check my ignored users + const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); + if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.error(myIgnoredUsersResponse.errors); + } + expect(myIgnoredUsersResponse.errors).to.be.empty; + const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; + expect(myIgnoredUsers.length).to.equal(1); + expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); + expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); + }); + +}); diff --git a/test/server/services/users.js b/test/server/services/users.js index 22fe7ddd5..04c5c457e 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -169,6 +169,24 @@ describe('services.UsersService', () => { }); }); + describe('#ignoreUser', () => { + + // @TODO: assert cannot ignore yourself + + it('should add user id to ignoredUsers set', async () => { + const user = mockUsers[0]; + const usersToIgnore = [mockUsers[1], mockUsers[2]]; + await UsersService.ignoreUsers(user.id, usersToIgnore.map(u => u.id)); + const userAfterIgnoring = await UsersService.findById(user.id); + expect(userAfterIgnoring.ignoresUsers.length).to.equal(2); + + // ignore same user another time, make sure it's not added to the list. + await UsersService.ignoreUsers(user.id, usersToIgnore.slice(0, 1).map(u => u.id)); + const userAfterIgnoring2 = await UsersService.findById(user.id); + expect(userAfterIgnoring2.ignoresUsers.length).to.equal(2); + }); + }); + describe('#ban', () => { it('should set the status to banned', () => { return UsersService