diff --git a/test/api/unit/libs/slack.js b/test/api/unit/libs/slack.js
index 3252ed7e16..4b6acdf695 100644
--- a/test/api/unit/libs/slack.js
+++ b/test/api/unit/libs/slack.js
@@ -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',
diff --git a/test/api/v3/integration/chat/POST-chat.flag.test.js b/test/api/v3/integration/chat/POST-chat.flag.test.js
index 88b41744c8..723715c80b 100644
--- a/test/api/v3/integration/chat/POST-chat.flag.test.js
+++ b/test/api/v3/integration/chat/POST-chat.flag.test.js
@@ -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,
diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js
index bd0f158b7d..aa6b8c8806 100644
--- a/test/api/v3/integration/chat/POST-chat.test.js
+++ b/test/api/v3/integration/chat/POST-chat.test.js
@@ -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,
diff --git a/test/api/v4/members/POST-flag_private_message.test.js b/test/api/v4/members/POST-flag_private_message.test.js
new file mode 100644
index 0000000000..dbeb9b5e18
--- /dev/null
+++ b/test/api/v4/members/POST-flag_private_message.test.js
@@ -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'),
+ });
+ });
+});
diff --git a/website/client/components/appFooter.vue b/website/client/components/appFooter.vue
index 0859dc691c..89d12e0ecf 100644
--- a/website/client/components/appFooter.vue
+++ b/website/client/components/appFooter.vue
@@ -43,6 +43,8 @@
li(v-html='$t("communityForum")')
li
a(href='https://www.facebook.com/Habitica', target='_blank') {{ $t('communityFacebook') }}
+ li
+ a(href='https://www.instagram.com/habitica', target='_blank') {{ $t('communityInstagram') }}
li
a(href='https://www.reddit.com/r/habitrpg/', target='_blank') {{ $t('communityReddit') }}
.col-12.col-md-6
@@ -56,23 +58,22 @@
a(:href="getDataDisplayToolUrl", target='_blank') {{ $t('dataDisplayTool') }}
li
a(href='http://habitica.fandom.com/wiki/Guidance_for_Blacksmiths', target='_blank') {{ $t('guidanceForBlacksmiths') }}
- li
- a(href='http://devs.habitica.com/', target='_blank') {{ $t('devBlog') }}
.col-6.social
h3 {{ $t('footerSocial') }}
- a.social-circle(href='https://twitter.com/habitica', target='_blank')
- .social-icon.svg-icon(v-html='icons.twitter')
- // TODO: Not ready yet. a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
- .social-icon.svg-icon.instagram(v-html='icons.instagram')
- a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
- .social-icon.facebook.svg-icon(v-html='icons.facebook')
+ .icons
+ a.social-circle(href='https://twitter.com/habitica', target='_blank')
+ .social-icon.svg-icon(v-html='icons.twitter')
+ a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
+ .social-icon.svg-icon.instagram(v-html='icons.instagram')
+ a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
+ .social-icon.facebook.svg-icon(v-html='icons.facebook')
.row
.col-12.col-md-8 {{ $t('donateText3') }}
.col-12.col-md-4
- button.btn.btn-contribute(@click="donate()", v-if="user")
+ button.btn.btn-contribute.btn-flat(@click="donate()", v-if="user")
.svg-icon.heart(v-html="icons.heart")
.text {{ $t('companyDonate') }}
- .btn.btn-contribute(v-else)
+ .btn.btn-contribute.btn-flat(v-else)
a(href='http://habitica.fandom.com/wiki/Contributing_to_Habitica', target='_blank')
.svg-icon.heart(v-html="icons.heart")
.text {{ $t('companyContribute') }}
@@ -145,6 +146,22 @@
}
}
+ .icons {
+ display: flex;
+ justify-content: flex-end;
+ flex-shrink: 1;
+ }
+
+ // smaller than desktop
+ @media only screen and (max-width: 992px) {
+ .social-circle {
+ height: 32px !important;
+ width: 32px !important;
+
+ margin-left: 0.75em !important;
+ }
+ }
+
.social-circle {
width: 40px;
height: 40px;
@@ -152,17 +169,20 @@
background-color: #c3c0c7;
display: flex;
margin-left: 1em;
- float: right;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:hover {
+ background-color: #a5a1ac;
+ }
.social-icon {
color: #e1e0e3;
width: 16px;
margin: auto;
}
-
- .instagram {
- margin-top: .85em;
- }
}
.logo {
@@ -185,6 +205,14 @@
box-shadow: none;
border-radius: 4px;
+ &:hover {
+ background: #a5a1ac;
+
+ .text {
+ color: white;
+ }
+ }
+
a {
display: flex;
}
diff --git a/website/client/components/chat/chatCard.vue b/website/client/components/chat/chatCard.vue
index 362a839f75..3a35fa1ace 100644
--- a/website/client/components/chat/chatCard.vue
+++ b/website/client/components/chat/chatCard.vue
@@ -1,8 +1,8 @@
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 }}
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') }}
diff --git a/website/client/components/tasks/task.vue b/website/client/components/tasks/task.vue
index 16a03ed681..4d7f383f47 100644
--- a/website/client/components/tasks/task.vue
+++ b/website/client/components/tasks/task.vue
@@ -20,7 +20,7 @@
v-if="isUser && !isRunningYesterdailies",
:right="task.type === 'reward'",
ref="taskDropdown",
- v-b-tooltip.hover.top="$t('showMore')"
+ v-b-tooltip.hover.top="$t('options')"
)
div(slot="dropdown-toggle", draggable=false)
.svg-icon.dropdown-icon(v-html="icons.menu")
@@ -141,6 +141,11 @@
min-width: 0px;
overflow-wrap: break-word;
+ // markdown p-tag, can't find without /deep/
+ /deep/ p {
+ margin-bottom: 0;
+ }
+
&.has-notes {
padding-bottom: 4px;
}
@@ -229,7 +234,7 @@
overflow-wrap: break-word;
&.has-checklist {
- padding-bottom: 8px;
+ padding-bottom: 2px;
}
}
@@ -285,7 +290,7 @@
margin-bottom: -3px;
min-height: 0px;
width: 100%;
- margin-left: 8px;
+ margin-left: 0;
padding-right: 20px;
overflow-wrap: break-word;
@@ -427,7 +432,7 @@
border-left: none;
}
- .task-control, .reward-control {
+ .task-control:not(.task-disabled-habit-control-inner), .reward-control {
cursor: pointer;
}
diff --git a/website/client/components/tasks/user.vue b/website/client/components/tasks/user.vue
index e9652ccabc..4986ecec29 100644
--- a/website/client/components/tasks/user.vue
+++ b/website/client/components/tasks/user.vue
@@ -12,7 +12,7 @@
.col-12.col-md-4.offset-md-4
.d-flex
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
- button.btn.btn-secondary.dropdown-toggle.ml-2.d-flex.align-items-center(
+ button.btn.btn-secondary.dropdown-toggle.ml-2.d-flex.align-items-center.search-button(
type="button",
@click="toggleFilterPanel()",
:class="{active: selectedTags.length > 0}",
@@ -110,6 +110,10 @@
padding-top: 16px;
}
+ .input-search, .search-button {
+ height: 40px;
+ }
+
.tasks-navigation {
margin-bottom: 20px;
}
diff --git a/website/client/components/ui/statsbar.vue b/website/client/components/ui/statsbar.vue
new file mode 100644
index 0000000000..dc7ddb37c0
--- /dev/null
+++ b/website/client/components/ui/statsbar.vue
@@ -0,0 +1,126 @@
+
+ .progress-container(ref="container", :id="elementId", :class="{condensed}")
+ .svg-icon(v-html="icon")
+ .progress
+ .progress-bar(:class="progressClass", :style="{width: `${percent(value, maxValue)}%`}")
+ span.small-text {{value | statFloor}} / {{maxValue}}
+ b-tooltip.myClass(:target="() => $refs.container", :container="elementId", :title="tooltip", triggers="hover", placement="bottom")
+
+
+
+
+
diff --git a/website/client/components/userMenu/profile.vue b/website/client/components/userMenu/profile.vue
index cf544bfbda..e66208d0d8 100644
--- a/website/client/components/userMenu/profile.vue
+++ b/website/client/components/userMenu/profile.vue
@@ -324,6 +324,7 @@
}
.progress-container > .progress {
+ border-radius: 1px;
background-color: $gray-500;
}
}
@@ -371,8 +372,10 @@
.progress {
height: 8px;
+ border-radius: 1px;
.progress-bar {
+ border-radius: 1px;
background-color: $green-10 !important;
}
}
@@ -495,6 +498,9 @@ export default {
async userId () {
this.loadUser();
},
+ userLoggedIn () {
+ this.loadUser();
+ },
},
methods: {
async loadUser () {
diff --git a/website/client/store/actions/chat.js b/website/client/store/actions/chat.js
index 0bb568a863..584db3bf75 100644
--- a/website/client/store/actions/chat.js
+++ b/website/client/store/actions/chat.js
@@ -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;
}
diff --git a/website/client/store/actions/user.js b/website/client/store/actions/user.js
index d15bd3e805..fe6094fcc9 100644
--- a/website/client/store/actions/user.js
+++ b/website/client/store/actions/user.js
@@ -5,6 +5,7 @@ import axios from 'axios';
import { togglePinnedItem as togglePinnedItemOp } from 'common/script/ops/pinnedGearUtils';
import changeClassOp from 'common/script/ops/changeClass';
import disableClassesOp from 'common/script/ops/disableClasses';
+import openMysteryItemOp from 'common/script/ops/openMysteryItem';
export function fetch (store, options = {}) { // eslint-disable-line no-shadow
return loadAsyncResource({
@@ -127,7 +128,9 @@ export function castSpell (store, params) {
return axios.post(spellUrl, data);
}
-export function openMysteryItem () {
+export async function openMysteryItem (store) {
+ let user = store.state.user.data;
+ openMysteryItemOp(user);
return axios.post('/api/v4/user/open-mystery-item');
}
diff --git a/website/common/errors/apiErrorMessages.js b/website/common/errors/apiErrorMessages.js
index a1550fddad..f10747b12c 100644
--- a/website/common/errors/apiErrorMessages.js
+++ b/website/common/errors/apiErrorMessages.js
@@ -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.",',
};
diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json
index be9e61a57a..c249bc9f51 100644
--- a/website/common/locales/en/front.json
+++ b/website/common/locales/en/front.json
@@ -23,6 +23,7 @@
"communityBug": "Submit Bug",
"communityExtensions": "Add-ons & Extensions",
"communityFacebook": "Facebook",
+ "communityInstagram": "Instagram",
"communityFeature": "Request Feature",
"communityForum": "Forum",
"communityKickstarter": "Kickstarter",
diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json
index 57adad943b..f8fa686631 100644
--- a/website/common/locales/en/generic.json
+++ b/website/common/locales/en/generic.json
@@ -34,6 +34,7 @@
"saveEdits": "Save Edits",
"showMore": "Show More",
"showLess": "Show Less",
+ "options": "Options",
"expandToolbar": "Expand Toolbar",
"collapseToolbar": "Collapse Toolbar",
diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json
index 31b5c08887..496f1277f7 100644
--- a/website/common/locales/en/groups.json
+++ b/website/common/locales/en/groups.json
@@ -162,6 +162,7 @@
"abuseFlagModalBody": "Are you sure you want to report this post? You should only 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.",
diff --git a/website/common/locales/en/messages.json b/website/common/locales/en/messages.json
index 0a0e9b0c15..b1aa99d069 100644
--- a/website/common/locales/en/messages.json
+++ b/website/common/locales/en/messages.json
@@ -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."
}
diff --git a/website/server/controllers/api-v3/challenges.js b/website/server/controllers/api-v3/challenges.js
index 37f9cac929..82bb301169 100644
--- a/website/server/controllers/api-v3/challenges.js
+++ b/website/server/controllers/api-v3/challenges.js
@@ -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.
diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js
index 3c7f5468b0..84703ac0da 100644
--- a/website/server/controllers/api-v3/chat.js
+++ b/website/server/controllers/api-v3/chat.js
@@ -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;
diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js
index 166f82e430..9eef5108e9 100644
--- a/website/server/controllers/api-v3/groups.js
+++ b/website/server/controllers/api-v3/groups.js
@@ -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.
*
diff --git a/website/server/controllers/api-v3/hall.js b/website/server/controllers/api-v3/hall.js
index e42adb80c5..d3715bfda6 100644
--- a/website/server/controllers/api-v3/hall.js
+++ b/website/server/controllers/api-v3/hall.js
@@ -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
diff --git a/website/server/controllers/api-v4/members.js b/website/server/controllers/api-v4/members.js
new file mode 100644
index 0000000000..57a1b39ddd
--- /dev/null
+++ b/website/server/controllers/api-v4/members.js
@@ -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;
diff --git a/website/server/libs/chatReporting/chatReporter.js b/website/server/libs/chatReporting/chatReporter.js
index 148dbd4696..d55a95c353 100644
--- a/website/server/libs/chatReporting/chatReporter.js
+++ b/website/server/libs/chatReporting/chatReporter.js
@@ -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');
}
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 ff625bfb8a..b41f142579 100644
--- a/website/server/libs/chatReporting/groupChatReporter.js
+++ b/website/server/libs/chatReporting/groupChatReporter.js
@@ -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,
diff --git a/website/server/libs/chatReporting/inboxChatReporter.js b/website/server/libs/chatReporting/inboxChatReporter.js
new file mode 100644
index 0000000000..207b570fd3
--- /dev/null
+++ b/website/server/libs/chatReporting/inboxChatReporter.js
@@ -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;
+ }
+}
diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js
index cbdda9d8e2..26524ecac0 100644
--- a/website/server/libs/inbox/index.js
+++ b/website/server/libs/inbox/index.js
@@ -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;
diff --git a/website/server/libs/setupPassport.js b/website/server/libs/setupPassport.js
index 9f17200fd1..215c91daa5 100644
--- a/website/server/libs/setupPassport.js
+++ b/website/server/libs/setupPassport.js
@@ -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.
diff --git a/website/server/libs/slack.js b/website/server/libs/slack.js
index b0b5bb8898..0adaf24894 100644
--- a/website/server/libs/slack.js
+++ b/website/server/libs/slack.js
@@ -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,
};
diff --git a/website/server/models/group.js b/website/server/models/group.js
index cea35d21ad..e8bf77e39a 100644
--- a/website/server/models/group.js
+++ b/website/server/models/group.js
@@ -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