Merge branch 'negue/flagpm' into develop

This commit is contained in:
Matteo Pagliazzi
2019-03-03 17:12:19 +01:00
25 changed files with 513 additions and 87 deletions
+3 -2
View File
@@ -32,6 +32,7 @@ describe('slack', () => {
},
message: {
id: 'chat-id',
username: 'author',
user: 'Author',
uuid: 'author-id',
text: 'some text',
@@ -50,11 +51,11 @@ describe('slack', () => {
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: 'flagger (flagger-id; language: flagger-lang) flagged a message',
text: 'flagger (flagger-id; language: flagger-lang) flagged a group message',
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `Author - author@example.com - author-id\n${timestamp}`,
author_name: `@author Author (author@example.com; author-id)\n${timestamp}`,
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
@@ -63,11 +63,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}; language: en) flagged a message`,
text: `${user.profile.name} (${user.id}; language: en) flagged a group message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`,
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
@@ -98,11 +98,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`,
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a group message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`,
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
@@ -257,7 +257,7 @@ describe('POST /chat', () => {
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `${user.profile.name} - ${user.auth.local.email} - ${user._id}`,
author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
@@ -310,7 +310,7 @@ describe('POST /chat', () => {
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `${members[0].profile.name} - ${members[0].auth.local.email} - ${members[0]._id}`,
author_name: `@${members[0].auth.local.username} ${members[0].profile.name} (${members[0].auth.local.email}; ${members[0]._id})`,
title: 'Slur in Party - (private party)',
title_link: undefined,
text: testSlurMessage,
@@ -0,0 +1,74 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /members/flag-private-message/:messageId', () => {
let userToSendMessage;
let messageToSend = 'Test Private Message';
beforeEach(async () => {
userToSendMessage = await generateUser();
});
it('Allows players to flag their own private message', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let senderMessages = await userToSendMessage.get('/inbox/messages');
let sendersMessageInSendersInbox = _.find(senderMessages, (message) => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(sendersMessageInSendersInbox).to.exist;
await expect(userToSendMessage.post(`/members/flag-private-message/${sendersMessageInSendersInbox.id}`)).to.eventually.be.ok;
});
it('Flags a private message', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
});
it('Returns an error when user tries to flag a private message that is already flagged', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('messageGroupChatFlagAlreadyReported'),
});
});
});
+39 -12
View File
@@ -1,8 +1,8 @@
<template lang="pug">
div
.mentioned-icon(v-if='isUserMentioned')
.message-hidden(v-if='msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
.message-hidden(v-if='!inbox && msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
.message-hidden(v-if='!inbox && msg.flagCount > 1 && user.contributor.admin') Message hidden
.card-body
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
p.time
@@ -11,25 +11,28 @@ div
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}&nbsp;
span(v-if="msg.client && user.contributor.level >= 4") ({{ msg.client }})
.text(v-html='atHighlight(parseMarkdown(msg.text))')
.reported(v-if="isMessageReported && (inbox === true)")
span(v-once) {{ $t('reportedMessage')}}
br
span(v-once) {{ $t('canDeleteNow') }}
hr
.d-flex(v-if='msg.id')
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
div {{$t('copyAsTodo')}}
.action.d-flex.align-items-center(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
div {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
.action.d-flex.align-items-center(v-if='(inbox || (user.flags.communityGuidelinesAccepted && msg.uuid !== "system")) && !isMessageReported', @click='report(msg)')
.svg-icon(v-html="icons.report", v-once)
div(v-once) {{$t('report')}}
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
.svg-icon(v-html="icons.delete", v-once)
div(v-once) {{$t('delete')}}
.ml-auto.d-flex(v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}", v-if='!inbox')
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount > 0', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.liked", :title='$t("liked")')
| +{{ likeCount }}
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount === 0', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.like", :title='$t("like")')
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='!msg.likes[user._id] && !inbox') {{ $t('like') }}
</template>
<style lang="scss">
@@ -111,6 +114,11 @@ div
color: $purple-400;
}
}
.reported {
margin-top: 18px;
color: $red-50;
}
</style>
<script>
@@ -132,8 +140,15 @@ import reportIcon from 'assets/svg/report.svg';
import {highlightUsers} from '../../libs/highlightUsers';
export default {
props: ['msg', 'inbox', 'groupId'],
components: {userLink},
props: {
msg: {},
inbox: {
type: Boolean,
default: false,
},
groupId: {},
},
data () {
return {
icons: Object.freeze({
@@ -143,6 +158,7 @@ export default {
delete: deleteIcon,
liked: likedIcon,
}),
reported: false,
};
},
filters: {
@@ -191,6 +207,9 @@ export default {
}
return likeCount;
},
isMessageReported () {
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
},
},
methods: {
async like () {
@@ -216,10 +235,18 @@ export default {
copyAsTodo (message) {
this.$root.$emit('habitica::copy-as-todo', message);
},
async report () {
report () {
this.$root.$on('habitica:report-result', data => {
if (data.ok) {
this.reported = true;
}
this.$root.$off('habitica:report-result');
});
this.$root.$emit('habitica::report-chat', {
message: this.msg,
groupId: this.groupId,
groupId: this.groupId || 'privateMessage',
});
},
async remove () {
@@ -3,7 +3,6 @@
.row
.col-12
copy-as-todo-modal(:group-type='groupType', :group-name='groupName', :group-id='groupId')
report-flag-modal
div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)', :class='{row: inbox}')
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
avatar.avatar-left(
@@ -105,14 +104,21 @@ import findIndex from 'lodash/findIndex';
import Avatar from '../avatar';
import copyAsTodoModal from './copyAsTodoModal';
import reportFlagModal from './reportFlagModal';
import chatCard from './chatCard';
export default {
props: ['chat', 'groupType', 'groupId', 'groupName', 'inbox'],
props: {
chat: {},
inbox: {
type: Boolean,
default: false,
},
groupType: {},
groupId: {},
groupName: {},
},
components: {
copyAsTodoModal,
reportFlagModal,
chatCard,
Avatar,
},
@@ -106,14 +106,16 @@ export default {
this.$root.$emit('bv::hide::modal', 'report-flag');
},
async reportAbuse () {
this.notify('Thank you for reporting this violation. The moderators have been notified.');
this.text(this.$t(this.groupId === 'privateMessage' ? 'pmReported' : 'abuseReported'));
await this.$store.dispatch('chat:flag', {
let result = await this.$store.dispatch('chat:flag', {
groupId: this.groupId,
chatId: this.abuseObject.id,
comment: this.reportComment,
});
this.$root.$emit('habitica:report-result', result);
this.close();
},
async clearFlagCount () {
@@ -3,6 +3,7 @@ div
inbox-modal
creator-intro
profileModal
report-flag-modal
send-gems-modal
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
b-navbar-brand.brand
@@ -351,12 +352,15 @@ import profileModal from '../userMenu/profileModal';
import sendGemsModal from 'client/components/payments/sendGemsModal';
import userDropdown from './userDropdown';
import reportFlagModal from '../chat/reportFlagModal';
export default {
components: {
creatorIntro,
InboxModal,
notificationMenu,
profileModal,
reportFlagModal,
sendGemsModal,
userDropdown,
},
+9 -1
View File
@@ -48,10 +48,18 @@ export async function like (store, payload) {
}
export async function flag (store, payload) {
const url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatId}/flag`;
let url = '';
if (payload.groupId === 'privateMessage') {
url = `/api/v4/members/flag-private-message/${payload.chatId}`;
} else {
url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatId}/flag`;
}
const response = await axios.post(url, {
comment: payload.comment,
});
return response.data.data;
}
+4 -3
View File
@@ -8,11 +8,14 @@ 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.',
groupRemainOrLeaveChallenges: 'req.query.keep must be either "remain-in-challenges" or "leave-challenges"',
managerIdRequired: 'req.body.managerId must contain a user ID.',
managerIdRequired: 'req.body.managerId must contain a User ID.',
noSudoAccess: 'You don\'t have sudo access.',
eventRequired: '"req.params.event" is required.',
@@ -22,6 +25,4 @@ module.exports = {
missingCustomerId: 'Missing "req.query.customerId"',
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
missingSubKey: 'Missing "req.query.sub"',
messageIdRequired: '\"messageId\" must be a valid UUID.",',
};
+3 -2
View File
@@ -162,6 +162,7 @@
"abuseFlagModalBody": "Are you sure you want to report this post? You should <strong>only</strong> report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction.",
"abuseFlagModalButton": "Report Violation",
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
"pmReported": "Thank you for reporting this message.",
"abuseAlreadyReported": "You have already reported this message.",
"whyReportingPost": "Why are you reporting this post?",
"whyReportingPostPlaceholder": "Please help our moderators by letting us know why you are reporting this post for a violation, e.g., spam, swearing, religious oaths, bigotry, slurs, adult topics, violence.",
@@ -230,9 +231,9 @@
"memberCannotRemoveYourself": "You cannot remove yourself!",
"groupMemberNotFound": "User not found among group's members",
"mustBeGroupMember": "Must be member of the group.",
"canOnlyInviteEmailUuid": "Can only invite using user IDs, emails, or usernames.",
"canOnlyInviteEmailUuid": "Can only invite using User IDs, emails, or usernames.",
"inviteMissingEmail": "Missing email address in invite.",
"inviteMissingUuid": "Missing user id in invite",
"inviteMissingUuid": "Missing User ID in invite",
"inviteMustNotBeEmpty": "Invite must not be empty.",
"partyMustbePrivate": "Parties must be private",
"userAlreadyInGroup": "UserID: <%= userId %>, User \"<%= username %>\" already in that group.",
+4 -3
View File
@@ -45,7 +45,7 @@
"messageAuthEmailTaken": "Email already taken",
"messageAuthNoUserFound": "No user found.",
"messageAuthMustBeLoggedIn": "You must be logged in.",
"messageAuthMustIncludeTokens": "You must include a token and uid (user id) in your request",
"messageAuthMustIncludeTokens": "You must include a token and uid (User ID) in your request",
"messageGroupAlreadyInParty": "Already in a party, try refreshing.",
"messageGroupOnlyLeaderCanUpdate": "Only the group leader can update the group!",
@@ -71,6 +71,7 @@
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!",
"messageDeletedUser": "Sorry, this user has deleted their account.",
"messageMissingDisplayName": "Missing display name."
"messageMissingDisplayName": "Missing display name.",
"reportedMessage": "You have reported this message to moderators.",
"canDeleteNow": "You can now delete the message if you wish."
}
@@ -49,7 +49,7 @@ let api = {};
* @apiSuccess {String} challenge.name Full name of challenge.
* @apiSuccess {String} challenge.shortName A shortened name for the challenge, to be used as a tag.
* @apiSuccess {Object} challenge.leader User details of challenge leader.
* @apiSuccess {UUID} challenge.leader._id User id of challenge leader.
* @apiSuccess {UUID} challenge.leader._id User ID of challenge leader.
* @apiSuccess {Object} challenge.leader.profile Profile information of leader.
* @apiSuccess {Object} challenge.leader.profile.name Display Name of leader.
* @apiSuccess {String} challenge.updatedAt Timestamp of last update.
+11 -6
View File
@@ -30,12 +30,17 @@ const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email)
/**
* @apiDefine GroupIdRequired
* @apiError (404) {badRequest} groupIdRequired A group ID is required
* @apiError (400) {badRequest} groupIdRequired A group ID is required
*/
/**
* @apiDefine ChatIdRequired
* @apiError (404) {badRequest} chatIdRequired A chat ID is required
* @apiError (400) {badRequest} chatIdRequired A chat ID is required
*/
/**
* @apiDefine MessageIdRequired
* @apiError (400) {badRequest} messageIdRequired A message ID is required
*/
let api = {};
@@ -246,7 +251,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;
@@ -285,7 +290,7 @@ api.likeChat = {
* @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 {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 GroupNotFound
@@ -334,7 +339,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;
@@ -470,7 +475,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;
+7 -7
View File
@@ -941,11 +941,11 @@ api.removeGroupMember = {
* {"name": "User2", "email": "user-2@example.com"}
* ]
* }
* @apiParamExample {json} User Ids
* @apiParamExample {json} User IDs
* {
* "uuids": ["user-id-of-existing-user", "user-id-of-another-existing-user"]
* }
* @apiParamExample {json} User Ids and Emails
* @apiParamExample {json} User IDs and Emails
* {
* "emails": [
* {"email": "user-1@example.com"},
@@ -955,7 +955,7 @@ api.removeGroupMember = {
* }
*
* @apiSuccess {Array} data The invites
* @apiSuccess {Object} data[0] If the invitation was a user id, you'll receive back an object. You'll receive one Object for each succesful user id invite.
* @apiSuccess {Object} data[0] If the invitation was a User ID, you'll receive back an object. You'll receive one Object for each succesful User ID invite.
* @apiSuccess {String} data[1] If the invitation was an email, you'll receive back the email. You'll receive one String for each successful email invite.
*
* @apiSuccessExample {json} Successful Response with Emails
@@ -966,13 +966,13 @@ api.removeGroupMember = {
* ]
* }
*
* @apiSuccessExample {json} Successful Response with User Id
* @apiSuccessExample {json} Successful Response with User ID
* {
* "data": [
* { id: 'the-id-of-the-invited-user', name: 'The group name', inviter: 'your-user-id' }
* ]
* }
* @apiSuccessExample {json} Successful Response with User Ids and Emails
* @apiSuccessExample {json} Successful Response with User IDs and Emails
* {
* "data": [
* "user-1@example.com",
@@ -987,9 +987,9 @@ api.removeGroupMember = {
* param `Array`.
* @apiError (400) {BadRequest} UuidOrEmailOnly The `emails` and `uuids` params were both missing and/or a
* key other than `emails` or `uuids` was provided in the body param.
* @apiError (400) {BadRequest} CannotInviteSelf User id or email of invitee matches that of the inviter.
* @apiError (400) {BadRequest} CannotInviteSelf User ID or email of invitee matches that of the inviter.
* @apiError (400) {BadRequest} MustBeArray The `uuids` or `emails` body param was not an array.
* @apiError (400) {BadRequest} TooManyInvites A max of 100 invites (combined emails and user ids) can
* @apiError (400) {BadRequest} TooManyInvites A max of 100 invites (combined emails and User IDs) can
* be sent out at a time.
* @apiError (400) {BadRequest} ExceedsMembersLimit A max of 30 members can join a party.
*
+1 -1
View File
@@ -197,7 +197,7 @@ const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0};
/**
* @api {put} /api/v3/hall/heroes/:heroId Update any user ("hero")
* @apiParam (Path) {UUID} heroId user ID
* @apiParam (Path) {UUID} heroId User ID
* @apiName UpdateHero
* @apiGroup Hall
* @apiPermission Admin
@@ -0,0 +1,43 @@
import { authWithHeaders } from '../../middlewares/auth';
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
let api = {};
/**
* @api {post} /api/v4/members/flag-private-message/:messageId Flag a private message
* @apiDescription An email and slack message are 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 private 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 (always an empty object)
* @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, or of the recipient if `sent` is true
* @apiSuccess {String} data.user The Display Name of the author of the message, or of the recipient if `sent` is true
* @apiSuccess {String} data.username The Username of the author of the message, or of the recipient if `sent` is true
*
* @apiUse MessageNotFound
* @apiUse MessageIdRequired
* @apiError (400) {BadRequest} messageGroupChatFlagAlreadyReported You have already reported this message
*/
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, {
ok: true,
message,
});
},
};
module.exports = api;
@@ -1,6 +1,4 @@
import {
} from '../errors';
import { getUserInfo } from '../email';
import { getGroupUrl, getUserInfo } from '../email';
import { getAuthorEmailFromMessage } from '../chat';
export default class ChatReporter {
@@ -11,25 +9,51 @@ export default class ChatReporter {
async validate () {}
async notify (group, message) {
const reporterEmailContent = getUserInfo(this.user, ['email']).email;
this.authorEmail = await getAuthorEmailFromMessage(message);
this.emailVariables = [
async getMessageVariables (group, message) {
const reporterEmail = getUserInfo(this.user, ['email']).email;
const authorVariables = await this.getAuthorVariables(message);
const groupUrl = getGroupUrl(group);
return [
{name: 'MESSAGE_TIME', content: (new Date(message.timestamp)).toString()},
{name: 'MESSAGE_TEXT', content: message.text},
{name: 'REPORTER_USERNAME', content: this.user.profile.name},
{name: 'REPORTER_DISPLAY_NAME', content: this.user.profile.name},
{name: 'REPORTER_USERNAME', content: this.user.auth.local.username},
{name: 'REPORTER_UUID', content: this.user._id},
{name: 'REPORTER_EMAIL', content: reporterEmailContent},
{name: 'REPORTER_EMAIL', content: reporterEmail},
{name: 'REPORTER_MODAL_URL', content: `/static/front/#?memberId=${this.user._id}`},
{name: 'AUTHOR_USERNAME', content: message.user},
{name: 'AUTHOR_UUID', content: message.uuid},
{name: 'AUTHOR_EMAIL', content: this.authorEmail},
{name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId=${message.uuid}`},
...authorVariables,
{name: 'GROUP_NAME', content: group.name},
{name: 'GROUP_TYPE', content: group.type},
{name: 'GROUP_ID', content: group._id},
{name: 'GROUP_URL', content: groupUrl || 'N/A'},
];
}
createGenericAuthorVariables (prefix, {user, username, uuid, email}) {
return [
{name: `${prefix}_DISPLAY_NAME`, content: user},
{name: `${prefix}_USERNAME`, content: username},
{name: `${prefix}_UUID`, content: uuid},
{name: `${prefix}_EMAIL`, content: email},
{name: `${prefix}_MODAL_URL`, content: `/static/front/#?memberId=${uuid}`},
];
}
async getAuthorVariables (message) {
this.authorEmail = await getAuthorEmailFromMessage(message);
return this.createGenericAuthorVariables('AUTHOR', {
user: message.user,
username: message.username,
uuid: message.uuid,
email: this.authorEmail,
});
}
async flag () {
throw new Error('Flag must be implemented');
}
@@ -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);
// }
}
@@ -6,7 +6,7 @@ import {
BadRequest,
NotFound,
} from '../errors';
import { getGroupUrl, sendTxn } from '../email';
import { sendTxn } from '../email';
import slack from '../slack';
import { model as Group } from '../../models/group';
import { chatModel as Chat } from '../../models/message';
@@ -28,7 +28,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;
@@ -50,16 +50,12 @@ export default class GroupChatReporter extends ChatReporter {
}
async notify (group, message, userComment, automatedComment = '') {
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},
let emailVariables = await this.getMessageVariables(group, message);
emailVariables = emailVariables.concat([
{name: 'REPORTER_COMMENT', content: userComment || ''},
]));
]);
sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', emailVariables);
slack.sendFlagNotification({
authorEmail: this.authorEmail,
@@ -0,0 +1,129 @@
import nconf from 'nconf';
import { model as User } from '../../models/user';
import ChatReporter from './chatReporter';
import {
BadRequest,
} from '../errors';
import { getUserInfo, sendTxn} from '../email';
import slack from '../slack';
import apiError from '../apiError';
import * as inboxLib from '../inbox';
import {getAuthorEmailFromMessage} from '../chat';
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;
this.inboxUser = res.locals.user;
}
async validate () {
this.req.checkParams('messageId', apiError('messageIdRequired')).notEmpty();
let validationErrors = this.req.validationErrors();
if (validationErrors) throw validationErrors;
if (this.user.contributor.admin && this.req.query.userId) {
this.inboxUser = await User.findOne({_id: this.req.query.userId});
}
const message = await inboxLib.getUserInboxMessage(this.inboxUser, this.req.params.messageId);
if (!message) throw new BadRequest(this.res.t('messageGroupChatNotFound'));
const userComment = this.req.body.comment;
return {message, userComment};
}
async notify (message, userComment) {
const group = {
type: 'private messages',
name: 'N/A',
_id: 'N/A',
};
let emailVariables = await this.getMessageVariables(group, message);
emailVariables = emailVariables.concat([
{name: 'REPORTER_COMMENT', content: userComment || ''},
]);
sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', emailVariables);
slack.sendInboxFlagNotification({
authorEmail: this.authorEmail,
flagger: this.user,
message,
userComment,
});
}
async getAuthorVariables (message) {
const messageUser = {
user: message.user,
username: message.username,
uuid: message.uuid,
email: await getAuthorEmailFromMessage(message),
};
const reporter = {
user: this.user.profile.name,
username: this.user.auth.local.username,
uuid: this.user._id,
email: getUserInfo(this.user, ['email']).email,
};
// if message.sent, the reporter is the author of this message
const sendingUser = message.sent ? reporter : messageUser;
const recipient = message.sent ? messageUser : reporter;
this.authorEmail = sendingUser.email;
return [
...this.createGenericAuthorVariables('AUTHOR', sendingUser),
...this.createGenericAuthorVariables('RECIPIENT', recipient),
];
}
updateMessageAndSave (message, ...changedFields) {
for (const changedField of changedFields) {
message.markModified(changedField);
}
return message.save();
}
flagInboxMessage (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 BadRequest(this.res.t('messageGroupChatFlagAlreadyReported'));
}
message.flags[this.user._id] = true;
message.flagCount = 1;
return this.updateMessageAndSave(message, 'flags', 'flagCount');
}
async markMessageAsReported (message) {
message.reported = true;
return this.updateMessageAndSave(message, 'reported');
}
async flag () {
let {message, userComment} = await this.validate();
await this.flagInboxMessage(message);
await this.notify(message, userComment);
await this.markMessageAsReported(message);
return message;
}
}
+4
View File
@@ -16,6 +16,10 @@ export async function getUserInbox (user, asArray = true) {
}
}
export async function getUserInboxMessage (user, messageId) {
return Inbox.findOne({ownerId: user._id, _id: messageId}).exec();
}
export async function deleteMessage (user, messageId) {
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
if (!message) return false;
+1 -1
View File
@@ -6,7 +6,7 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// serialize users into and deserialize users out of the session. Typically,
// this will be as simple as storing the user ID when serializing, and finding
// this will be as simple as storing the User ID when serializing, and finding
// the user by ID when deserializing. However, since this example does not
// have a database of user records, the complete Facebook profile is serialized
// and deserialized.
+108 -7
View File
@@ -9,6 +9,10 @@ const SLACK_FLAGGING_URL = nconf.get('SLACK_FLAGGING_URL');
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK');
const SLACK_SUBSCRIPTIONS_URL = nconf.get('SLACK_SUBSCRIPTIONS_URL');
const BASE_URL = nconf.get('BASE_URL');
const IS_PRODUCTION = nconf.get('IS_PROD');
const SKIP_FLAG_METHODS = IS_PRODUCTION && !SLACK_FLAGGING_URL;
const SKIP_SUB_METHOD = IS_PRODUCTION && !SLACK_SUBSCRIPTIONS_URL;
let flagSlack;
let subscriptionSlack;
@@ -18,6 +22,26 @@ try {
subscriptionSlack = new IncomingWebhook(SLACK_SUBSCRIPTIONS_URL);
} catch (err) {
logger.error(err);
if (!IS_PRODUCTION) {
flagSlack = subscriptionSlack = {
send (data) {
logger.info('Data sent to slack', data);
},
};
}
}
/**
*
* @param formatObj.name userName
* @param formatObj.displayName displayName
* @param formatObj.email email
* @param formatObj.uuid uuid
* @returns {string}
*/
function formatUser (formatObj) {
return `@${formatObj.name} ${formatObj.displayName} (${formatObj.email}; ${formatObj.uuid})`;
}
function sendFlagNotification ({
@@ -28,13 +52,13 @@ function sendFlagNotification ({
userComment,
automatedComment,
}) {
if (!SLACK_FLAGGING_URL) {
if (SKIP_FLAG_METHODS) {
return;
}
let titleLink;
let authorName;
let title = `Flag in ${group.name}`;
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a message`;
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a group message`;
let footer = `<${SLACK_FLAGGING_FOOTER_LINK}?groupId=${group.id}&chatId=${message.id}|Flag this message.>`;
if (userComment) {
@@ -55,7 +79,12 @@ function sendFlagNotification ({
if (!message.user && message.uuid === 'system') {
authorName = 'System Message';
} else {
authorName = `${message.user} - ${authorEmail} - ${message.uuid}`;
authorName = formatUser({
name: message.username,
displayName: message.user,
email: authorEmail,
uuid: message.uuid,
});
}
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
@@ -77,6 +106,69 @@ function sendFlagNotification ({
});
}
function sendInboxFlagNotification ({
authorEmail,
flagger,
message,
userComment,
}) {
if (SKIP_FLAG_METHODS) {
return;
}
let titleLink = '';
let authorName;
let title = `Flag in ${flagger.profile.name}'s Inbox`;
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a PM`;
let footer = '';
if (userComment) {
text += ` and commented: ${userComment}`;
}
let messageText = message.text;
let sender = '';
let recipient = '';
const flaggerFormat = formatUser({
displayName: flagger.profile.name,
name: flagger.auth.local.username,
email: flagger.auth.local.email,
uuid: flagger._id,
});
const messageUserFormat = formatUser({
displayName: message.user,
name: message.username,
email: authorEmail,
uuid: message.uuid,
});
if (message.sent) {
sender = flaggerFormat;
recipient = messageUserFormat;
} else {
sender = messageUserFormat;
recipient = flaggerFormat;
}
authorName = `${sender} wrote this message to ${recipient}.`;
flagSlack.send({
text,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: authorName,
title,
title_link: titleLink,
text: messageText,
footer,
mrkdwn_in: [
'text',
],
}],
});
}
function sendSubscriptionNotification ({
buyer,
recipient,
@@ -84,7 +176,7 @@ function sendSubscriptionNotification ({
months,
groupId,
}) {
if (!SLACK_SUBSCRIPTIONS_URL) {
if (SKIP_SUB_METHOD) {
return;
}
let text;
@@ -108,7 +200,7 @@ function sendSlurNotification ({
group,
message,
}) {
if (!SLACK_FLAGGING_URL) {
if (SKIP_FLAG_METHODS) {
return;
}
let titleLink;
@@ -124,7 +216,12 @@ function sendSlurNotification ({
title += ` - (${group.privacy} ${group.type})`;
}
authorName = `${author.profile.name} - ${authorEmail} - ${author.id}`;
authorName = formatUser({
name: author.auth.local.username,
displayName: author.profile.name,
email: authorEmail,
uuid: author.id,
});
flagSlack.send({
text,
@@ -143,5 +240,9 @@ function sendSlurNotification ({
}
module.exports = {
sendFlagNotification, sendSubscriptionNotification, sendSlurNotification,
sendFlagNotification,
sendInboxFlagNotification,
sendSubscriptionNotification,
sendSlurNotification,
formatUser,
};
+1 -1
View File
@@ -408,7 +408,7 @@ function getInviteCount (uuids, emails) {
/**
* Checks invitation uuids and emails for possible errors.
*
* @param uuids An array of user ids
* @param uuids An array of User IDs
* @param emails An array of emails
* @param res Express res object for use with translations
* @throws BadRequest An error describing the issue with the invitations