diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index e9dcd2d86d..2a1cf8319c 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -117,5 +117,9 @@ "missingPetFoodFeed": "\"pet\" and \"food\" are required parameters.", "invalidPetName": "Invalid pet name supplied.", "missingEggHatchingPotionHatch": "\"egg\" and \"hatchingPotion\" are required parameters.", - "invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'." + "invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'.", + "cannoyBuyItem": "You can't buy this item", + "messageRequired": "A message is required.", + "toUserIDRequired": "A toUserId is required", + "notAuthorizedToSendMessageToThisUser": "Can't send message to this user." } diff --git a/test/api/v3/integration/members/POST-send_private_message.test.js b/test/api/v3/integration/members/POST-send_private_message.test.js new file mode 100644 index 0000000000..013f741708 --- /dev/null +++ b/test/api/v3/integration/members/POST-send_private_message.test.js @@ -0,0 +1,171 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +describe('POST /send-private-message', () => { + let userToSendMessage; + let messageToSend = { message: 'Test Private Message' }; + + beforeEach(async () => { + userToSendMessage = await generateUser(); + }); + + it('returns error when message is not provided', async () => { + await expect(userToSendMessage.post('/send-private-message')) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Invalid request parameters.', + }); + }); + + it('returns error when toUserId is not provided', async () => { + await expect(userToSendMessage.post('/send-private-message', { + message: messageToSend, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Invalid request parameters.', + }); + }); + + it('returns error when to user is not found', async () => { + await expect(userToSendMessage.post('/send-private-message', { + message: messageToSend, + toUserId: generateUUID(), + })).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userNotFound'), + }); + }); + + it('returns error when to user has blocked the sender', async () => { + let receiver = await generateUser({'inbox.blocks': [userToSendMessage._id]}); + + await expect(userToSendMessage.post('/send-private-message', { + message: messageToSend, + toUserId: receiver._id, + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notAuthorizedToSendMessageToThisUser'), + }); + }); + + it('returns error when sender has blocked to user', async () => { + let receiver = await generateUser(); + let sender = await generateUser({'inbox.blocks': [receiver._id]}); + + await expect(sender.post('/send-private-message', { + message: messageToSend, + toUserId: receiver._id, + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notAuthorizedToSendMessageToThisUser'), + }); + }); + + it('returns error when to user has opted out of messaging', async () => { + let receiver = await generateUser({'inbox.optOut': true}); + + await expect(userToSendMessage.post('/send-private-message', { + message: messageToSend, + toUserId: receiver._id, + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notAuthorizedToSendMessageToThisUser'), + }); + }); + + it('sends a private message to a user', async () => { + let receiver = await generateUser(); + + await userToSendMessage.post('/send-private-message', { + message: messageToSend, + toUserId: receiver._id, + }); + + let updatedReceiver = await receiver.get('/user'); + let updatedSender = await userToSendMessage.get('/user'); + + let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { + return message.uuid === userToSendMessage._id && message.text === messageToSend.message; + }); + + let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => { + return message.uuid === receiver._id && message.text === messageToSend.message; + }); + + expect(sendersMessageInReceiversInbox).to.exist; + expect(sendersMessageInSendersInbox).to.exist; + }); + + it('sends a private message about gems to a user', async () => { + let receiver = await generateUser(); + let messageAboutGemsToSend = { + type: 'gems', + gems: { + amount: 2, + }, + message: 'Test Message About Gems', + }; + + await userToSendMessage.post('/send-private-message', { + message: messageAboutGemsToSend, + toUserId: receiver._id, + }); + + let updatedReceiver = await receiver.get('/user'); + let updatedSender = await userToSendMessage.get('/user'); + + let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { + return message.uuid === userToSendMessage._id; + }); + + let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => { + return message.uuid === receiver._id; + }); + + expect(sendersMessageInReceiversInbox).to.exist; + expect(sendersMessageInReceiversInbox.text).to.equal(`Hello ${receiver.profile.name}, ${userToSendMessage.profile.name} has sent you ${messageAboutGemsToSend.gems.amount} gems! ${messageAboutGemsToSend.message}`); + expect(sendersMessageInSendersInbox).to.exist; + expect(sendersMessageInSendersInbox.text).to.equal(`Hello ${receiver.profile.name}, ${userToSendMessage.profile.name} has sent you ${messageAboutGemsToSend.gems.amount} gems! ${messageAboutGemsToSend.message}`); + }); + + it('sends a private message about subscriptions to a user', async () => { + let receiver = await generateUser(); + let messageAboutSubscriptionToSend = { + type: 'subscription', + subscription: { + key: 'basic_12mo', + }, + message: 'Test Message About Subscription', + }; + + await userToSendMessage.post('/send-private-message', { + message: messageAboutSubscriptionToSend, + toUserId: receiver._id, + }); + + let updatedReceiver = await receiver.get('/user'); + let updatedSender = await userToSendMessage.get('/user'); + + let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { + return message.uuid === userToSendMessage._id; + }); + + let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => { + return message.uuid === receiver._id; + }); + + expect(sendersMessageInReceiversInbox).to.exist; + expect(sendersMessageInReceiversInbox.text).to.equal(`Hello ${receiver.profile.name}, ${userToSendMessage.profile.name} has sent you 12 months of subscription! ${messageAboutSubscriptionToSend.message}`); + expect(sendersMessageInSendersInbox).to.exist; + expect(sendersMessageInSendersInbox.text).to.equal(`Hello ${receiver.profile.name}, ${userToSendMessage.profile.name} has sent you 12 months of subscription! ${messageAboutSubscriptionToSend.message}`); + }); +}); diff --git a/website/src/controllers/api-v3/members.js b/website/src/controllers/api-v3/members.js index ad42fe5e8d..36533968d5 100644 --- a/website/src/controllers/api-v3/members.js +++ b/website/src/controllers/api-v3/members.js @@ -9,8 +9,13 @@ import { model as Group } from '../../models/group'; import { model as Challenge } from '../../models/challenge'; import { NotFound, + NotAuthorized, } from '../../libs/api-v3/errors'; import * as Tasks from '../../models/task'; +import { + getUserInfo, + sendTxn as sendTxnEmail, +} from '../../libs/api-v3/email'; let api = {}; @@ -231,4 +236,53 @@ api.getChallengeMemberProgress = { }, }; +/** + * @api {posts} /send-private-message Get a challenge member progress + * @apiVersion 3.0.0 + * @apiName SendPrivateMessage + * @apiGroup Members + * + * @apiParam {String} message The message + * @apiParam {UUID} toUserId The toUser _id + * + * @apiSuccess {} Object Returns an empty object + */ +api.sendPrivateMessage = { + method: 'POST', + url: '/send-private-message', + middlewares: [authWithHeaders(), cron], + async handler (req, res) { + req.checkBody('message', res.t('messageRequired')).notEmpty(); + req.checkBody('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID(); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let sender = res.locals.user; + let message = req.body.message; + + let userToReceiveMessage = await User.findById(req.body.toUserId).exec(); + if (!userToReceiveMessage) throw new NotFound(res.t('userNotFound')); + + let userBlockedSender = userToReceiveMessage.inbox.blocks.indexOf(sender._id) !== -1; + let userIsBlockBySender = sender.inbox.blocks.indexOf(userToReceiveMessage._id) !== -1; + let userOptedOutOfMessaging = userToReceiveMessage.inbox.optOut; + + if (userBlockedSender || userIsBlockBySender || userOptedOutOfMessaging) { + throw new NotAuthorized(res.t('notAuthorizedToSendMessageToThisUser')); + } + + await sender.sendMessage(userToReceiveMessage, message); + + if (userToReceiveMessage.preferences.emailNotifications.newPM !== false) { + sendTxnEmail(userToReceiveMessage, 'new-pm', [ + {name: 'SENDER', content: getUserInfo(sender, ['name']).name}, + {name: 'PMS_INBOX_URL', content: '/#/options/groups/inbox'}, + ]); + } + + res.respond(200, {}); + }, +}; + module.exports = api; diff --git a/website/src/models/user.js b/website/src/models/user.js index bd45c2a7c2..9ab410e6d3 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -7,6 +7,8 @@ import * as Tasks from './task'; import Q from 'q'; import { schema as TagSchema } from './tag'; import baseModel from '../libs/api-v3/baseModel'; +import { chatDefaults } from './group'; +import { defaults } from 'lodash'; // import {model as Challenge} from './challenge'; let Schema = mongoose.Schema; @@ -706,6 +708,30 @@ schema.methods.getGroups = function getUserGroups () { return userGroups; }; +schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, messageData) { + let msg; + let sender = this; + + if (!messageData.type) { + msg = messageData.message; + } else { + msg = `Hello ${userToReceiveMessage.profile.name }, ${sender.profile.name} has sent you `; + msg += messageData.type === 'gems' ? `${messageData.gems.amount} gems! ` : `${shared.content.subscriptionBlocks[messageData.subscription.key].months} months of subscription! `; + msg += messageData.message; + } + + shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(msg, sender)); + userToReceiveMessage.inbox.newMessages++; + userToReceiveMessage._v++; + userToReceiveMessage.markModified('inbox.messages'); + + shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(msg, userToReceiveMessage))); + sender.markModified('inbox.messages'); + + let promises = [userToReceiveMessage.save(), sender.save()]; + await Q.all(promises); +}; + export let model = mongoose.model('User', schema); // Initially export an empty object so external requires will get