diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index 11469a56847..93e2173bca0 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -27,6 +27,7 @@ import { } from '../../models/message'; import highlightMentions from '../../libs/highlightMentions'; import { handleGetMembersForChallenge } from '../../libs/challenges/handleGetMembersForChallenge'; +import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory'; const { achievements } = common; @@ -777,4 +778,39 @@ api.transferGems = { }, }; +/** + * @api {post} /api/v3/members/:memberId/flag Flag (report) a user + * @apiDescription Sends an email to staff about another user or their profile + * @apiName FlagUser + * @apiGroup Members + * + * @apiParam (Path) {UUID} memberId The unique ID of the user being flagged + * @apiParam (Body) {String} [comment] explain why the user was flagged + * + * @apiSuccess {Object} data The flagged user + * @apiSuccess {UUID} data.id The id of the flagged user + * @apiSuccess {String} data.username The username of the flagged user + * @apiSuccess {Object} data.profile The flagged user's profile information + * @apiSuccess {String} data.profile.blurb Text of the flagged user's profile bio + * @apiSuccess {String} data.profile.imageUrl URL of the flagged user's profile image + * @apiSuccess {String} data.profile.name The flagged user's display name + * + * @apiError (400) {BadRequest} AlreadyFlagged A profile cannot be flagged + * more than once by the same user. + * @apiError (400) {BadRequest} MemberIdRequired The `memberId` param is required + * and must be a valid `UUID`. + * @apiError (404) {NotFound} UserWithIdNotFound The `memberId` param did not + * belong to an existing user. + */ +api.flagUser = { + method: 'POST', + url: '/members/:memberId/flag', + middlewares: [authWithHeaders()], + async handler (req, res) { + const chatReporter = chatReporterFactory('User', req, res); + const flaggedUser = await chatReporter.flag(); + res.respond(200, flaggedUser); + }, +}; + export default api; diff --git a/website/server/libs/chatReporting/chatReporterFactory.js b/website/server/libs/chatReporting/chatReporterFactory.js index 39f8810ba54..e0b9e7b0196 100644 --- a/website/server/libs/chatReporting/chatReporterFactory.js +++ b/website/server/libs/chatReporting/chatReporterFactory.js @@ -1,11 +1,14 @@ import GroupChatReporter from './groupChatReporter'; import InboxChatReporter from './inboxChatReporter'; +import ProfileReporter from './profileReporter'; export function chatReporterFactory (type, req, res) { // eslint-disable-line import/prefer-default-export, max-len if (type === 'Group') { return new GroupChatReporter(req, res); } if (type === 'Inbox') { return new InboxChatReporter(req, res); + } if (type === 'Profile' || type === 'User') { + return new ProfileReporter(req, res); } throw new Error('Invalid chat reporter type.'); diff --git a/website/server/libs/chatReporting/profileReporter.js b/website/server/libs/chatReporting/profileReporter.js index 60a32899226..7374e249132 100644 --- a/website/server/libs/chatReporting/profileReporter.js +++ b/website/server/libs/chatReporting/profileReporter.js @@ -34,7 +34,8 @@ export default class ProfileReporter extends ChatReporter { throw new NotFound(this.res.t('userWithIDNotFound')); } - if (flaggedUser.profile.flags.indexOf(this.user._id) !== -1 && !this.user.hasPermission('moderator')) { + if (flaggedUser.profile.flags && flaggedUser.profile.flags.indexOf(this.user._id) !== -1 + && !this.user.hasPermission('moderator')) { throw new BadRequest('A profile can not be flagged more than once by the same user.'); } @@ -64,6 +65,17 @@ export default class ProfileReporter extends ChatReporter { ]; } + flagProfile (flaggedUser) { + // Log user ids that have flagged the account + if (!flaggedUser.profile.flags) { + flaggedUser.profile.flags = []; + } + + flaggedUser.profile.flags.push(this.user._id); + + return flaggedUser.save(); + } + async notify (flaggedUser, userComment) { let emailVariables = await this.getEmailVariables(flaggedUser); emailVariables = emailVariables.concat([ @@ -78,4 +90,14 @@ export default class ProfileReporter extends ChatReporter { userComment, }); } + + async flag () { + const { flaggedUser, userComment } = await this.validate(); + await this.flagProfile(flaggedUser); + await this.notify(flaggedUser, userComment); + if (!this.user.hasPermission('moderator')) { + flaggedUser.profile.flags = [this.user._id]; + } + return flaggedUser; + } } \ No newline at end of file diff --git a/website/server/libs/slack.js b/website/server/libs/slack.js index 615c7d4284d..71dd0192177 100644 --- a/website/server/libs/slack.js +++ b/website/server/libs/slack.js @@ -176,6 +176,39 @@ function sendInboxFlagNotification ({ .catch(err => logger.error(err, 'Error while sending flag data to Slack.')); } +function sendProfileFlagNotification ({ + reporter, + flaggedUser, + userComment, +}) { + const title = 'User Profile Report'; + const titleLink = `${BASE_URL}/static/front/#?memberId=${flaggedUser._id}`; + const text = `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${flaggedUser.auth.local.username}'s profile`; + if (userComment) { + text += ` and commented: ${userComment}`; + } + const profileData = `Bio: ${flaggedUser.profile.blurb}`; + if (flaggedUser.profile.imageUrl) { + profileData += `\n\nImage URL: ${flaggedUser.profile.imageUrl}`; + } + + flagSlack + .send({ + text, + attachments: [{ + fallback: 'Flag Profile', + color: 'danger', + title, + title_link: titleLink, + text: profileData, + mrkdwn_in: [ + 'text', + ], + }], + }) + .catch(err => logger.error(err, 'Error while sending flag data to Slack.')); +} + function sendSubscriptionNotification ({ buyer, recipient, @@ -302,6 +335,7 @@ function sendSlurNotification ({ export { sendFlagNotification, sendInboxFlagNotification, + sendProfileFlagNotification, sendSubscriptionNotification, sendShadowMutedPostNotification, sendSlurNotification, diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 8a98ee17ce2..ad5b7a11659 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -621,6 +621,9 @@ export default new Schema({ required: true, trim: true, }, + flags: [ + { $type: String, validate: [v => validator.isUUID(v), 'Invalid user UUID.'], ref: 'User' }, + ], }, stats: { hp: { $type: Number, default: shared.maxHealth },