diff --git a/test/api/v3/integration/chat/POST-chat.flag.test.js b/test/api/v3/integration/chat/POST-chat.flag.test.js new file mode 100644 index 0000000000..5f4954d28a --- /dev/null +++ b/test/api/v3/integration/chat/POST-chat.flag.test.js @@ -0,0 +1,126 @@ +import { + generateUser, + requester, + translate as t, +} from '../../../../helpers/api-integration.helper'; +import _ from 'lodash'; + +describe('POST /chat/:chatId/flag', () => { + let user; + let api; + let group; + let testMessage = 'Test Message'; + + before(() => { + let groupName = 'Test Guild'; + let groupType = 'guild'; + let groupPrivacy = 'public'; + + return generateUser({balance: 1}).then((generatedUser) => { + user = generatedUser; + api = requester(user); + }) + .then(() => { + return api.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + }) + .then((generatedGroup) => { + group = generatedGroup; + }); + }); + + it('Returns an error when chat message is not found', () => { + return expect(api.post(`/groups/${group._id}/chat/incorrectMessage/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageGroupChatNotFound'), + }); + }); + + it('Returns an error when user tries to flag their own message', () => { + return api.post(`/groups/${group._id}/chat`, { message: testMessage}) + .then((result) => { + return expect(api.post(`/groups/${group._id}/chat/${result.message.id}/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageGroupChatFlagOwnMessage'), + }); + }); + }); + + it('Flags a chat', () => { + let api2; + let message; + + return generateUser().then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + message = result.message; + return api.post(`/groups/${group._id}/chat/${message.id}/flag`); + }) + .then((result) => { + expect(result.flags[user._id]).to.equal(true); + expect(result.flagCount).to.equal(1); + return api.get(`/groups/${group._id}`); + }) + .then((updatedGroup) => { + let messageToCheck = _.find(updatedGroup.chat, {id: message.id}); + expect(messageToCheck.flags[user._id]).to.equal(true); + }); + }); + + it('Flags a chat with a higher flag acount when an admin flags the message', () => { + let api2; + let secondUser; + let message; + + return generateUser({'contributor.admin': true}).then((generatedUser) => { + secondUser = generatedUser; + api2 = requester(generatedUser); + return api.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + message = result.message; + return api2.post(`/groups/${group._id}/chat/${message.id}/flag`); + }) + .then((result) => { + expect(result.flags[secondUser._id]).to.equal(true); + expect(result.flagCount).to.equal(5); + return api.get(`/groups/${group._id}`); + }) + .then((updatedGroup) => { + let messageToCheck = _.find(updatedGroup.chat, {id: message.id}); + expect(messageToCheck.flags[secondUser._id]).to.equal(true); + expect(messageToCheck.flagCount).to.equal(5); + }); + }); + + it('Returns an error when user tries to flag a message that is already flagged', () => { + let api2; + let message; + + return generateUser().then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + message = result.message; + return api.post(`/groups/${group._id}/chat/${message.id}/flag`); + }) + .then(() => { + return expect(api.post(`/groups/${group._id}/chat/${message.id}/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageGroupChatFlagAlreadyReported'), + }); + }); + }); +}); diff --git a/test/api/v3/integration/chat/POST-chat.like.test.js b/test/api/v3/integration/chat/POST-chat.like.test.js new file mode 100644 index 0000000000..cda9df4e1c --- /dev/null +++ b/test/api/v3/integration/chat/POST-chat.like.test.js @@ -0,0 +1,103 @@ +import { + generateUser, + requester, + translate as t, +} from '../../../../helpers/api-integration.helper'; +import _ from 'lodash'; + +describe('POST /chat/:chatId/like', () => { + let user; + let api; + let group; + let testMessage = 'Test Message'; + + before(() => { + let groupName = 'Test Guild'; + let groupType = 'guild'; + let groupPrivacy = 'public'; + + return generateUser({balance: 1}).then((generatedUser) => { + user = generatedUser; + api = requester(user); + }) + .then(() => { + return api.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + }) + .then((generatedGroup) => { + group = generatedGroup; + }); + }); + + it('Returns an error when chat message is not found', () => { + return expect(api.post(`/groups/${group._id}/chat/incorrectMessage/like`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageGroupChatNotFound'), + }); + }); + + it('Returns an error when user tries to like their own message', () => { + return api.post(`/groups/${group._id}/chat`, { message: testMessage}) + .then((result) => { + return expect(api.post(`/groups/${group._id}/chat/${result.message.id}/like`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageGroupChatLikeOwnMessage'), + }); + }); + }); + + it('Likes a chat', () => { + let api2; + let message; + + return generateUser().then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + message = result.message; + return api.post(`/groups/${group._id}/chat/${message.id}/like`); + }) + .then((result) => { + expect(result.likes[user._id]).to.equal(true); + return api.get(`/groups/${group._id}`); + }) + .then((updatedGroup) => { + let messageToCheck = _.find(updatedGroup.chat, {id: message.id}); + expect(messageToCheck.likes[user._id]).to.equal(true); + }); + }); + + it('Unlikes a chat', () => { + let api2; + let message; + + return generateUser().then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + message = result.message; + return api.post(`/groups/${group._id}/chat/${message.id}/like`); + }) + .then((result) => { + expect(result.likes[user._id]).to.equal(true); + return api.post(`/groups/${group._id}/chat/${message.id}/like`); + }) + .then((result) => { + expect(result.likes[user._id]).to.equal(false); + return api.get(`/groups/${group._id}`); + }) + .then((updatedGroup) => { + let messageToCheck = _.find(updatedGroup.chat, {id: message.id}); + expect(messageToCheck.likes[user._id]).to.equal(false); + }); + }); +}); diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js new file mode 100644 index 0000000000..f427ac460d --- /dev/null +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -0,0 +1,98 @@ +import { + generateUser, + requester, + translate as t, +} from '../../../../helpers/api-integration.helper'; + +describe('POST /chat', () => { + let user; + let api; + + before(() => { + return generateUser().then((generatedUser) => { + user = generatedUser; + api = requester(user); + }); + }); + + it('Returns an error when no message is provided', () => { + let groupName = 'Test Guild'; + let groupType = 'guild'; + let groupPrivacy = 'public'; + let testMessage = ''; + let api2; + + return generateUser({balance: 1}).then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + }) + .then((group) => { + return expect(api.post(`/groups/${group._id}/chat`, { message: testMessage})) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + }); + + it('Returns an error when group is not found', () => { + let testMessage = 'Test Message'; + return expect(api.post('/groups/nvalidID/chat', { message: testMessage})).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('groupNotFound'), + }); + }); + + it('Returns an error when chat privileges are revoked', () => { + let groupName = 'Test Guild'; + let groupType = 'guild'; + let groupPrivacy = 'public'; + let testMessage = 'Test Message'; + let api2; + + return generateUser({balance: 1, 'flags.chatRevoked': true}).then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + }) + .then((group) => { + return expect(api2.post(`/groups/${group._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: 'Your chat privileges have been revoked.', + }); + }); + }); + + it('creates a chat', () => { + let groupName = 'Test Guild'; + let groupType = 'guild'; + let groupPrivacy = 'public'; + let testMessage = 'Test Message'; + let api2; + + return generateUser({balance: 1}).then((generatedUser) => { + api2 = requester(generatedUser); + return api2.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + }) + .then((group) => { + return api2.post(`/groups/${group._id}/chat`, { message: testMessage}); + }) + .then((result) => { + expect(result.message.id).to.exist; + }); + }); +}); diff --git a/website/src/controllers/api-v3/chat.js b/website/src/controllers/api-v3/chat.js index f0025d6e53..2bfb4c6847 100644 --- a/website/src/controllers/api-v3/chat.js +++ b/website/src/controllers/api-v3/chat.js @@ -1,9 +1,13 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth'; import cron from '../../middlewares/api-v3/cron'; import { model as Group } from '../../models/group'; +import { model as User } from '../../models/user'; import { NotFound, } from '../../libs/api-v3/errors'; +import _ from 'lodash'; +import { sendTxn } from '../../libs/api-v3/email'; +import nconf from 'nconf'; let api = {}; @@ -39,4 +43,242 @@ api.getChat = { }, }; +/** + * @api {post} /groups/:groupId/chat Post chat message to a group + * @apiVersion 3.0.0 + * @apiName PostCat + * @apiGroup Chat + * + * @apiParam {UUID} groupId The group _id + * @apiParam {message} message The chat's message + * @apiParam {previousMsg} previousMsg The previous chat message which will force a return of the full group chat + * + * @apiSuccess {Array} chat An array of chat messages + */ +api.postChat = { + method: 'POST', + url: '/groups/:groupId/chat', + middlewares: [authWithHeaders(), cron], + handler (req, res, next) { + let user = res.locals.user; + let groupId = req.params.groupId; + let chatUpdated; + + req.checkParams('groupId', res.t('groupIdRequired')).notEmpty(); + req.checkBody('message', res.t('messageGroupChatBlankMessage')).notEmpty(); + + let validationErrors = req.validationErrors(); + if (validationErrors) return next(validationErrors); + + Group.getGroup(user, groupId) + .then((group) => { + if (!group) throw new NotFound(res.t('groupNotFound')); + if (group.type !== 'party' && user.flags.chatRevoked) { + throw new NotFound('Your chat privileges have been revoked.'); + } + + let lastClientMsg = req.query.previousMsg; + chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false; + + group.sendChat(req.body.message, user); + + if (group.type === 'party') { + user.party.lastMessageSeen = group.chat[0].id; + user.save(); + } + return group.save(); + }) + .then((group) => { + if (chatUpdated) { + res.respond(200, {chat: group.chat}); + } else { + res.respond(200, {message: group.chat[0]}); + } + }) + .catch(next); + }, +}; + +/** + * @api {post} /groups/:groupId/chat/:chatId/like Like a group chat message + * @apiVersion 3.0.0 + * @apiName LikeChat + * @apiGroup Chat + * + * @apiParam {groupId} groupId The group _id + * @apiParam {chatId} chatId The chat message _id + * + * @apiSuccess {Array} chat An array of chat messages + */ +api.likeChat = { + method: 'Post', + url: '/groups/:groupId/chat/:chatId/like', + middlewares: [authWithHeaders(), cron], + handler (req, res, next) { + let user = res.locals.user; + let groupId = req.params.groupId; + let message; + + req.checkParams('groupId', res.t('groupIdRequired')).notEmpty(); + req.checkParams('chatId', res.t('chatIdRequired')).notEmpty(); + + let validationErrors = req.validationErrors(); + if (validationErrors) return next(validationErrors); + + Group.getGroup(user, groupId) + .then((group) => { + if (!group) throw new NotFound(res.t('groupNotFound')); + message = _.find(group.chat, {id: req.params.chatId}); + if (!message) throw new NotFound(res.t('messageGroupChatNotFound')); + + if (message.uuid === user._id) throw new NotFound(res.t('messageGroupChatLikeOwnMessage')); + + let update = {$set: {}}; + + if (!message.likes) message.likes = {}; + + message.likes[user._id] = !message.likes[user._id]; + update.$set[`chat.$.likes.${user._id}`] = message.likes[user._id]; + + return Group.update( + {_id: group._id, 'chat.id': message.id}, + update + ); + }) + .then((groupSaved) => { + if (!groupSaved) throw new NotFound(res.t('groupNotFound')); + res.respond(200, message); + }) + .catch(next); + }, +}; + +/** + * @api {post} /groups/:groupId/chat/:chatId/like Like a group chat message + * @apiVersion 3.0.0 + * @apiName LikeChat + * @apiGroup Chat + * + * @apiParam {groupId} groupId The group _id + * @apiParam {chatId} chatId The chat message _id + * + * @apiSuccess {Array} chat An array of chat messages + */ +api.flagChat = { + method: 'Post', + url: '/groups/:groupId/chat/:chatId/flag', + middlewares: [authWithHeaders(), cron], + handler (req, res, next) { + let user = res.locals.user; + let groupId = req.params.groupId; + let message; + let group; + let author; + + req.checkParams('groupId', res.t('groupIdRequired')).notEmpty(); + req.checkParams('chatId', res.t('chatIdRequired')).notEmpty(); + + let validationErrors = req.validationErrors(); + if (validationErrors) return next(validationErrors); + + Group.getGroup(user, groupId) + .then((groupFound) => { + if (!groupFound) throw new NotFound(res.t('groupNotFound')); + group = groupFound; + message = _.find(group.chat, {id: req.params.chatId}); + + if (!message) throw new NotFound(res.t('messageGroupChatNotFound')); + + if (message.uuid === user._id) throw new NotFound(res.t('messageGroupChatFlagOwnMessage')); + + return User.findOne({_id: message.uuid}, {auth: 1}); + }) + .then((foundAuthor) => { + author = foundAuthor; + + let update = {$set: {}}; + + // Log user ids that have flagged the message + if (!message.flags) message.flags = {}; + if (message.flags[user._id] && !user.contributor.admin) throw new NotFound(res.t('messageGroupChatFlagAlreadyReported')); + message.flags[user._id] = true; + update.$set[`chat.$.flags.${user._id}`] = true; + + // Log total number of flags (publicly viewable) + if (!message.flagCount) message.flagCount = 0; + if (user.contributor.admin) { + // Arbitraty amount, higher than 2 + message.flagCount = 5; + } else { + message.flagCount++; + } + update.$set['chat.$.flagCount'] = message.flagCount; + + return Group.update( + {_id: group._id, 'chat.id': message.id}, + update + ); + }) + .then((group2) => { + if (!group2) throw new NotFound(res.t('groupNotFound')); + + let addressesToSendTo = nconf.get('FLAG_REPORT_EMAIL'); + addressesToSendTo = typeof addressesToSendTo === 'string' ? JSON.parse(addressesToSendTo) : addressesToSendTo; + + if (Array.isArray(addressesToSendTo)) { + addressesToSendTo = addressesToSendTo.map((email) => { + return {email, canSend: true}; + }); + } else { + addressesToSendTo = {email: addressesToSendTo}; + } + + let reporterEmailContent; + if (user.auth.local) { + reporterEmailContent = user.auth.local.email; + } else if (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0]) { + reporterEmailContent = user.auth.facebook.emails[0].value; + } + + let authorEmailContent; + if (author.auth.local) { + authorEmailContent = author.auth.local.email; + } else if (author.auth.facebook && author.auth.facebook.emails && author.auth.facebook.emails[0]) { + authorEmailContent = author.auth.facebook.emails[0].value; + } + + let groupUrl; + if (group._id === 'habitrpg') { + groupUrl = '/#/options/groups/tavern'; + } else if (group.type === 'guild') { + groupUrl = `/#/options/groups/guilds/{$group._id}`; + } else { + groupUrl = 'party'; + } + + sendTxn(addressesToSendTo, 'flag-report-to-mods', [ + {name: 'MESSAGE_TIME', content: (new Date(message.timestamp)).toString()}, + {name: 'MESSAGE_TEXT', content: message.text}, + + {name: 'REPORTER_USERNAME', content: user.profile.name}, + {name: 'REPORTER_UUID', content: user._id}, + {name: 'REPORTER_EMAIL', content: reporterEmailContent}, + {name: 'REPORTER_MODAL_URL', content: `/static/front/#?memberId={$user._id}`}, + + {name: 'AUTHOR_USERNAME', content: message.user}, + {name: 'AUTHOR_UUID', content: message.uuid}, + {name: 'AUTHOR_EMAIL', content: authorEmailContent}, + {name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId={$message.uuid}`}, + + {name: 'GROUP_NAME', content: group.name}, + {name: 'GROUP_TYPE', content: group.type}, + {name: 'GROUP_ID', content: group._id}, + {name: 'GROUP_URL', content: groupUrl}, + ]); + res.respond(200, message); + }) + .catch(next); + }, +}; + export default api;