diff --git a/website/common/errors/apiErrorMessages.js b/website/common/errors/apiErrorMessages.js index 4857be3d58..5690cdfe9d 100644 --- a/website/common/errors/apiErrorMessages.js +++ b/website/common/errors/apiErrorMessages.js @@ -8,6 +8,9 @@ module.exports = { missingTypeKeyEquip: '"key" and "type" are required parameters.', + chatIdRequired: 'req.params.chatId must contain a chatId.', + messageIdRequired: 'req.params.messageId must contain a message ID.', + guildsOnlyPaginate: 'Only public guilds support pagination.', guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.', groupIdRequired: 'req.params.groupId must contain a groupId.', diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index 34a3868efe..a8f5241971 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -234,7 +234,7 @@ api.likeChat = { let groupId = req.params.groupId; req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); - req.checkParams('chatId', res.t('chatIdRequired')).notEmpty(); + req.checkParams('chatId', apiError('chatIdRequired')).notEmpty(); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; @@ -325,7 +325,7 @@ api.clearChatFlags = { let chatId = req.params.chatId; req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); - req.checkParams('chatId', res.t('chatIdRequired')).notEmpty(); + req.checkParams('chatId', apiError('chatIdRequired')).notEmpty(); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; @@ -465,7 +465,7 @@ api.deleteChat = { let chatId = req.params.chatId; req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); - req.checkParams('chatId', res.t('chatIdRequired')).notEmpty(); + req.checkParams('chatId', apiError('chatIdRequired')).notEmpty(); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index 421e2cf4bb..151e64bc2d 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -20,6 +20,7 @@ import { } from '../../libs/email'; import { sendNotification as sendPushNotification } from '../../libs/pushNotifications'; import { achievements } from '../../../../website/common/'; +import {chatReporterFactory} from '../../libs/chatReporting/chatReporterFactory'; let api = {}; @@ -514,6 +515,40 @@ api.sendPrivateMessage = { }, }; + +/** + * @api {post} /api/v3//members/flag-private-message/:messageId Flag a private message + * @apiDescription A message will be hidden immediately if a moderator flags the message. An email is sent to the moderators about every flagged message. + * @apiName FlagPrivateMessage + * @apiGroup Member + * + * @apiParam (Path) {UUID} messageId The private message id + * + * @apiSuccess {Object} data The flagged chat message + * @apiSuccess {UUID} data.id The id of the message + * @apiSuccess {String} data.text The text of the message + * @apiSuccess {Number} data.timestamp The timestamp of the message in milliseconds + * @apiSuccess {Object} data.likes The likes of the message + * @apiSuccess {Object} data.flags The flags of the message + * @apiSuccess {Number} data.flagCount The number of flags the message has + * @apiSuccess {UUID} data.uuid The user id of the author of the message + * @apiSuccess {String} data.user The username of the author of the message + * + * @apiUse MessageNotFound + * @apiUse MessageIdRequired + * @apiError (404) {NotFound} messageGroupChatFlagAlreadyReported The message has already been flagged + */ +api.flagPrivateMessage = { + method: 'POST', + url: '/members/flag-private-message/:messageId', + middlewares: [authWithHeaders()], + async handler (req, res) { + const chatReporter = chatReporterFactory('Inbox', req, res); + const message = await chatReporter.flag(); + res.respond(200, message); + }, +}; + /** * @api {post} /api/v3/members/transfer-gems Send a gem gift to a member * @apiName TransferGems diff --git a/website/server/libs/chatReporting/chatReporterFactory.js b/website/server/libs/chatReporting/chatReporterFactory.js index cd97da6793..50ac7e9e89 100644 --- a/website/server/libs/chatReporting/chatReporterFactory.js +++ b/website/server/libs/chatReporting/chatReporterFactory.js @@ -1,11 +1,10 @@ import GroupChatReporter from './groupChatReporter'; -// import InboxChatReporter from './inboxChatReporter'; +import InboxChatReporter from './inboxChatReporter'; export function chatReporterFactory (type, req, res) { if (type === 'Group') { return new GroupChatReporter(req, res); + } else if (type === 'Inbox') { + return new InboxChatReporter(req, res); } - // else if (type === 'Inbox') { - // return new InboxChatReporter(req, res); - // } } diff --git a/website/server/libs/chatReporting/groupChatReporter.js b/website/server/libs/chatReporting/groupChatReporter.js index c344607418..658e51b335 100644 --- a/website/server/libs/chatReporting/groupChatReporter.js +++ b/website/server/libs/chatReporting/groupChatReporter.js @@ -26,7 +26,7 @@ export default class GroupChatReporter extends ChatReporter { async validate () { this.req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); - this.req.checkParams('chatId', this.res.t('chatIdRequired')).notEmpty(); + this.req.checkParams('chatId', apiError('chatIdRequired')).notEmpty(); let validationErrors = this.req.validationErrors(); if (validationErrors) throw validationErrors; diff --git a/website/server/libs/chatReporting/inboxChatReporter.js b/website/server/libs/chatReporting/inboxChatReporter.js new file mode 100644 index 0000000000..841daa25d7 --- /dev/null +++ b/website/server/libs/chatReporting/inboxChatReporter.js @@ -0,0 +1,95 @@ +import nconf from 'nconf'; + +import ChatReporter from './chatReporter'; +import { + BadRequest, + NotFound, +} from '../errors'; +import { getGroupUrl, sendTxn } from '../email'; +import slack from '../slack'; +import { model as Group } from '../../models/group'; +import { model as Chat } from '../../models/chat'; +import apiError from '../apiError'; + +const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'); +const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => { + return { email, canSend: true }; +}); + +export default class InboxChatReporter extends ChatReporter { + constructor (req, res) { + super(req, res); + + this.user = res.locals.user; + } + + async validate () { + this.req.checkParams('messageId', apiError('messageIdRequired')).notEmpty(); + + let validationErrors = this.req.validationErrors(); + if (validationErrors) throw validationErrors; + + let group = await Group.getGroup({ + user: this.user, + groupId: this.groupId, + optionalMembership: this.user.contributor.admin, + }); + if (!group) throw new NotFound(this.res.t('groupNotFound')); + + const message = await Chat.findOne({_id: this.req.params.chatId}).exec(); + if (!message) throw new NotFound(this.res.t('messageGroupChatNotFound')); + if (message.uuid === 'system') throw new BadRequest(this.res.t('messageCannotFlagSystemMessages', {communityManagerEmail: COMMUNITY_MANAGER_EMAIL})); + + const userComment = this.req.body.comment; + + return {message, group, userComment}; + } + + async notify (group, message, userComment) { + await super.notify(group, message); + + const groupUrl = getGroupUrl(group); + sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', this.emailVariables.concat([ + {name: 'GROUP_NAME', content: group.name}, + {name: 'GROUP_TYPE', content: group.type}, + {name: 'GROUP_ID', content: group._id}, + {name: 'GROUP_URL', content: groupUrl}, + {name: 'REPORTER_COMMENT', content: userComment || ''}, + ])); + + slack.sendFlagNotification({ + authorEmail: this.authorEmail, + flagger: this.user, + group, + message, + userComment, + }); + } + + async flagGroupMessage (group, message) { + // Log user ids that have flagged the message + if (!message.flags) message.flags = {}; + // TODO fix error type + if (message.flags[this.user._id] && !this.user.contributor.admin) throw new NotFound(this.res.t('messageGroupChatFlagAlreadyReported')); + message.flags[this.user._id] = true; + message.markModified('flags'); + + // Log total number of flags (publicly viewable) + if (!message.flagCount) message.flagCount = 0; + if (this.user.contributor.admin) { + // Arbitrary amount, higher than 2 + message.flagCount = 5; + } else { + message.flagCount++; + } + + await message.save(); + } + + async flag () { + let {message, group, userComment} = await this.validate(); + await this.flagGroupMessage(group, message); + await this.notify(group, message, userComment); + return message; + } +}