mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
add shadow-muting chat hiding feature (automatic flagging of public posts from shadow-muted players) - fixes 10851 (#11239)
* move existing tests for chatRevoked users to 'mute user' describe block * give consistent names to chatRevoked tests and use const not let * improve methods for restoring chat permissions to test users * add tests for shadow-muting and define constants for flag-related numbers * update user profile URLs and reverse private/public 'if' statements * implement shadow muting in the API and schemas * add interface for mods to turn shadow muting on/off for a user - checkbox in the Hall - icon in the user's profile * mark chat posts as being shadow muted (marking is visible to mods only) * convert Admin Tools in profile from icons to text; make crown icon a toggle * move logic for displaying flag count to a computed property * prevent chat notifications for shadow-muted posts
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||||
TAVERN_ID,
|
TAVERN_ID,
|
||||||
} from '../../../../../website/server/models/group';
|
} from '../../../../../website/server/models/group';
|
||||||
|
import { CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
|
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
|
||||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||||
@@ -81,6 +82,10 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('mute user', () => {
|
describe('mute user', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
member.update({'flags.chatRevoked': false});
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||||
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||||
@@ -89,6 +94,129 @@ describe('POST /chat', () => {
|
|||||||
message: t('chatPrivilegesRevoked'),
|
message: t('chatPrivilegesRevoked'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
|
||||||
|
const { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'Private Guild',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const privateGuildMemberWithChatsRevoked = members[0];
|
||||||
|
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||||
|
|
||||||
|
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when chat privileges are revoked when sending a message to a party', async () => {
|
||||||
|
const { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'Party',
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const privatePartyMemberWithChatsRevoked = members[0];
|
||||||
|
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||||
|
|
||||||
|
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shadow-mute user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.spy(email, 'sendTxn');
|
||||||
|
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
member.update({'flags.chatShadowMuted': false});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => {
|
||||||
|
const userWithChatShadowMuted = await member.update({'flags.chatShadowMuted': true});
|
||||||
|
const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
|
||||||
|
|
||||||
|
// Email sent to mods
|
||||||
|
await sleep(0.5);
|
||||||
|
expect(email.sendTxn).to.be.calledOnce;
|
||||||
|
expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods');
|
||||||
|
|
||||||
|
// Slack message to mods
|
||||||
|
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||||
|
text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`,
|
||||||
|
attachments: [{
|
||||||
|
fallback: 'Shadow-Muted Message',
|
||||||
|
color: 'danger',
|
||||||
|
author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`,
|
||||||
|
title: 'Shadow-Muted Post in Test Guild',
|
||||||
|
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
|
||||||
|
text: testMessage,
|
||||||
|
mrkdwn_in: [
|
||||||
|
'text',
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
|
||||||
|
const { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'Private Guild',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userWithChatShadowMuted = members[0];
|
||||||
|
await userWithChatShadowMuted.update({'flags.chatShadowMuted': true});
|
||||||
|
|
||||||
|
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
expect(message.message.flagCount).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a chat with zero flagCount when sending a message to a party', async () => {
|
||||||
|
const { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'Party',
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userWithChatShadowMuted = members[0];
|
||||||
|
await userWithChatShadowMuted.update({'flags.chatShadowMuted': true});
|
||||||
|
|
||||||
|
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
expect(message.message.flagCount).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
|
||||||
|
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
expect(message.message.flagCount).to.eql(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('banned word', () => {
|
context('banned word', () => {
|
||||||
@@ -235,6 +363,7 @@ describe('POST /chat', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
|
user.update({'flags.chatRevoked': false});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors and revokes privileges when chat message contains a banned slur', async () => {
|
it('errors and revokes privileges when chat message contains a banned slur', async () => {
|
||||||
@@ -274,11 +403,6 @@ describe('POST /chat', () => {
|
|||||||
error: 'NotAuthorized',
|
error: 'NotAuthorized',
|
||||||
message: t('chatPrivilegesRevoked'),
|
message: t('chatPrivilegesRevoked'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @TODO: The next test should not depend on this. We should reset the user test in a beforeEach
|
|
||||||
// Restore chat privileges to continue testing
|
|
||||||
user.flags.chatRevoked = false;
|
|
||||||
await user.update({'flags.chatRevoked': false});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not allow slurs in private groups', async () => {
|
it('does not allow slurs in private groups', async () => {
|
||||||
@@ -327,10 +451,6 @@ describe('POST /chat', () => {
|
|||||||
error: 'NotAuthorized',
|
error: 'NotAuthorized',
|
||||||
message: t('chatPrivilegesRevoked'),
|
message: t('chatPrivilegesRevoked'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore chat privileges to continue testing
|
|
||||||
members[0].flags.chatRevoked = false;
|
|
||||||
await members[0].update({'flags.chatRevoked': false});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when slur is typed in mixed case', async () => {
|
it('errors when slur is typed in mixed case', async () => {
|
||||||
@@ -345,42 +465,6 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
|
|
||||||
let { group, members } = await createAndPopulateGroup({
|
|
||||||
groupDetails: {
|
|
||||||
name: 'Private Guild',
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'private',
|
|
||||||
},
|
|
||||||
members: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let privateGuildMemberWithChatsRevoked = members[0];
|
|
||||||
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
|
||||||
|
|
||||||
let message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
|
||||||
|
|
||||||
expect(message.message.id).to.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not error when sending a message to a party with a user with revoked chat', async () => {
|
|
||||||
let { group, members } = await createAndPopulateGroup({
|
|
||||||
groupDetails: {
|
|
||||||
name: 'Party',
|
|
||||||
type: 'party',
|
|
||||||
privacy: 'private',
|
|
||||||
},
|
|
||||||
members: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let privatePartyMemberWithChatsRevoked = members[0];
|
|
||||||
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
|
||||||
|
|
||||||
let message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
|
||||||
|
|
||||||
expect(message.message.id).to.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates a chat', async () => {
|
it('creates a chat', async () => {
|
||||||
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||||
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||||
@@ -486,8 +570,13 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('chat notifications', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
member.update({newMessages: {}, notifications: []});
|
||||||
|
});
|
||||||
|
|
||||||
it('notifies other users of new messages for a guild', async () => {
|
it('notifies other users of new messages for a guild', async () => {
|
||||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||||
let memberWithNotification = await member.get('/user');
|
let memberWithNotification = await member.get('/user');
|
||||||
|
|
||||||
expect(message.message.id).to.exist;
|
expect(message.message.id).to.exist;
|
||||||
@@ -507,7 +596,7 @@ describe('POST /chat', () => {
|
|||||||
members: 1,
|
members: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage});
|
let message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
let memberWithNotification = await members[0].get('/user');
|
let memberWithNotification = await members[0].get('/user');
|
||||||
|
|
||||||
expect(message.message.id).to.exist;
|
expect(message.message.id).to.exist;
|
||||||
@@ -517,6 +606,21 @@ describe('POST /chat', () => {
|
|||||||
})).to.exist;
|
})).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not notify other users of a new message that is already hidden from shadow-muting', async () => {
|
||||||
|
await user.update({'flags.chatShadowMuted': true});
|
||||||
|
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||||
|
let memberWithNotification = await member.get('/user');
|
||||||
|
|
||||||
|
await user.update({'flags.chatShadowMuted': false});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist;
|
||||||
|
expect(memberWithNotification.notifications.find(n => {
|
||||||
|
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id;
|
||||||
|
})).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
context('Spam prevention', () => {
|
context('Spam prevention', () => {
|
||||||
it('Returns an error when the user has been posting too many messages', async () => {
|
it('Returns an error when the user has been posting too many messages', async () => {
|
||||||
// Post as many messages are needed to reach the spam limit
|
// Post as many messages are needed to reach the spam limit
|
||||||
@@ -533,7 +637,7 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('contributor should not receive spam alert', async () => {
|
it('contributor should not receive spam alert', async () => {
|
||||||
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
|
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL});
|
||||||
|
|
||||||
// Post 1 more message than the spam limit to ensure they do not reach the limit
|
// Post 1 more message than the spam limit to ensure they do not reach the limit
|
||||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
|
||||||
|
|||||||
@@ -105,16 +105,22 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
|
|
||||||
it('updates chatRevoked flag', async () => {
|
it('updates chatRevoked flag', async () => {
|
||||||
let hero = await generateUser();
|
let hero = await generateUser();
|
||||||
|
|
||||||
await user.put(`/hall/heroes/${hero._id}`, {
|
await user.put(`/hall/heroes/${hero._id}`, {
|
||||||
flags: {chatRevoked: true},
|
flags: {chatRevoked: true},
|
||||||
});
|
});
|
||||||
|
|
||||||
await hero.sync();
|
await hero.sync();
|
||||||
|
|
||||||
expect(hero.flags.chatRevoked).to.eql(true);
|
expect(hero.flags.chatRevoked).to.eql(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updates chatShadowMuted flag', async () => {
|
||||||
|
let hero = await generateUser();
|
||||||
|
await user.put(`/hall/heroes/${hero._id}`, {
|
||||||
|
flags: {chatShadowMuted: true},
|
||||||
|
});
|
||||||
|
await hero.sync();
|
||||||
|
expect(hero.flags.chatShadowMuted).to.eql(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('updates contributor level', async () => {
|
it('updates contributor level', async () => {
|
||||||
let hero = await generateUser({
|
let hero = await generateUser({
|
||||||
contributor: {level: 5},
|
contributor: {level: 5},
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
div
|
div
|
||||||
.mentioned-icon(v-if='isUserMentioned')
|
.mentioned-icon(v-if='isUserMentioned')
|
||||||
.message-hidden(v-if='!inbox && msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
|
.message-hidden(v-if='!inbox && user.contributor.admin && msg.flagCount') {{flagCountDescription}}
|
||||||
.message-hidden(v-if='!inbox && msg.flagCount > 1 && user.contributor.admin') Message hidden
|
|
||||||
.card-body
|
.card-body
|
||||||
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
|
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
|
||||||
p.time
|
p.time
|
||||||
@@ -137,7 +136,8 @@ import copyIcon from 'assets/svg/copy.svg';
|
|||||||
import likeIcon from 'assets/svg/like.svg';
|
import likeIcon from 'assets/svg/like.svg';
|
||||||
import likedIcon from 'assets/svg/liked.svg';
|
import likedIcon from 'assets/svg/liked.svg';
|
||||||
import reportIcon from 'assets/svg/report.svg';
|
import reportIcon from 'assets/svg/report.svg';
|
||||||
import {highlightUsers} from '../../libs/highlightUsers';
|
import { highlightUsers } from '../../libs/highlightUsers';
|
||||||
|
import { CHAT_FLAG_LIMIT_FOR_HIDING, CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../common/script/constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {userLink},
|
components: {userLink},
|
||||||
@@ -210,6 +210,12 @@ export default {
|
|||||||
isMessageReported () {
|
isMessageReported () {
|
||||||
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
|
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
|
||||||
},
|
},
|
||||||
|
flagCountDescription () {
|
||||||
|
if (!this.msg.flagCount) return '';
|
||||||
|
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) return 'Message flagged once, not hidden';
|
||||||
|
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) return 'Message hidden';
|
||||||
|
return 'Message hidden (shadow-muted)';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async like () {
|
async like () {
|
||||||
@@ -274,6 +280,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||||
|
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||||
this.$emit('chat-card-mounted', this.msg.id);
|
this.$emit('chat-card-mounted', this.msg.id);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,11 @@
|
|||||||
h4.expand-toggle(:class="{'open': expandAuth}", @click="expandAuth = !expandAuth") Auth
|
h4.expand-toggle(:class="{'open': expandAuth}", @click="expandAuth = !expandAuth") Auth
|
||||||
div(v-if="expandAuth")
|
div(v-if="expandAuth")
|
||||||
pre {{hero.auth}}
|
pre {{hero.auth}}
|
||||||
|
.form-group
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatShadowMuted')
|
||||||
|
strong Chat Shadow Muting On
|
||||||
.form-group
|
.form-group
|
||||||
.checkbox
|
.checkbox
|
||||||
label
|
label
|
||||||
@@ -180,6 +185,7 @@ export default {
|
|||||||
if (!this.hero.flags) {
|
if (!this.hero.flags) {
|
||||||
this.hero.flags = {
|
this.hero.flags = {
|
||||||
chatRevoked: false,
|
chatRevoked: false,
|
||||||
|
chatShadowMuted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.expandItems = false;
|
this.expandItems = false;
|
||||||
|
|||||||
@@ -12,22 +12,29 @@
|
|||||||
button.btn.btn-secondary.positive-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) !== -1',
|
button.btn.btn-secondary.positive-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) !== -1',
|
||||||
@click="unblockUser()", v-b-tooltip.hover.right="$t('unblock')")
|
@click="unblockUser()", v-b-tooltip.hover.right="$t('unblock')")
|
||||||
.svg-icon.positive-icon(v-html="icons.positive")
|
.svg-icon.positive-icon(v-html="icons.positive")
|
||||||
button.btn.btn-secondary.positive-icon(v-if='this.userLoggedIn.contributor.admin && !adminToolsLoaded',
|
button.btn.btn-secondary.positive-icon(v-if='this.userLoggedIn.contributor.admin',
|
||||||
@click="loadAdminTools()", v-b-tooltip.hover.right="'Admin - Load Tools'")
|
@click="toggleAdminTools()", v-b-tooltip.hover.right="'Admin - Toggle Tools'")
|
||||||
.svg-icon.positive-icon(v-html="icons.staff")
|
.svg-icon.positive-icon(v-html="icons.staff")
|
||||||
span(v-if='this.userLoggedIn.contributor.admin && adminToolsLoaded')
|
.row.admin-profile-actions(v-if='this.userLoggedIn.contributor.admin && adminToolsLoaded')
|
||||||
button.btn.btn-secondary.positive-icon(v-if='!hero.flags || (hero.flags && !hero.flags.chatRevoked)',
|
.col-12.text-right
|
||||||
@click="adminRevokeChat()", v-b-tooltip.hover.bottom="'Admin - Revoke Chat Privileges'")
|
span.admin-action(v-if='!hero.flags || (hero.flags && !hero.flags.chatShadowMuted)',
|
||||||
.svg-icon.positive-icon(v-html="icons.megaphone")
|
@click="adminTurnOnShadowMuting()", v-b-tooltip.hover.bottom="'Turn on Shadow Muting'")
|
||||||
button.btn.btn-secondary.positive-icon(v-if='hero.flags && hero.flags.chatRevoked',
|
| shadow-mute
|
||||||
@click="adminReinstateChat()", v-b-tooltip.hover.bottom="'Admin - Reinstate Chat Privileges'")
|
span.admin-action(v-if='hero.flags && hero.flags.chatShadowMuted',
|
||||||
.svg-icon.positive-icon(v-html="icons.challenge")
|
@click="adminTurnOffShadowMuting()", v-b-tooltip.hover.bottom="'Turn off Shadow Muting'")
|
||||||
button.btn.btn-secondary.positive-icon(v-if='!hero.auth.blocked',
|
| un-shadow-mute
|
||||||
@click="adminBlockUser()", v-b-tooltip.hover.right="'Admin - Ban User'")
|
span.admin-action(v-if='!hero.flags || (hero.flags && !hero.flags.chatRevoked)',
|
||||||
.svg-icon.positive-icon(v-html="icons.lock")
|
@click="adminRevokeChat()", v-b-tooltip.hover.bottom="'Revoke Chat Privileges'")
|
||||||
button.btn.btn-secondary.positive-icon(v-if='hero.auth.blocked',
|
| mute
|
||||||
@click="adminUnblockUser()", v-b-tooltip.hover.right="'Admin - Unblock User'")
|
span.admin-action(v-if='hero.flags && hero.flags.chatRevoked',
|
||||||
.svg-icon.positive-icon(v-html="icons.member")
|
@click="adminReinstateChat()", v-b-tooltip.hover.bottom="'Reinstate Chat Privileges'")
|
||||||
|
| un-mute
|
||||||
|
span.admin-action(v-if='!hero.auth.blocked',
|
||||||
|
@click="adminBlockUser()", v-b-tooltip.hover.bottom="'Ban User'")
|
||||||
|
| ban
|
||||||
|
span.admin-action(v-if='hero.auth.blocked',
|
||||||
|
@click="adminUnblockUser()", v-b-tooltip.hover.bottom="'Un-Ban User'")
|
||||||
|
| un-ban
|
||||||
.row
|
.row
|
||||||
.col-12
|
.col-12
|
||||||
member-details(:member="user")
|
member-details(:member="user")
|
||||||
@@ -184,6 +191,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-profile-actions {
|
||||||
|
margin-bottom: 3em;
|
||||||
|
|
||||||
|
.admin-action {
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.profile-actions {
|
.profile-actions {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
@@ -586,6 +603,22 @@ export default {
|
|||||||
openSendGemsModal () {
|
openSendGemsModal () {
|
||||||
this.$root.$emit('habitica::send-gems', this.user);
|
this.$root.$emit('habitica::send-gems', this.user);
|
||||||
},
|
},
|
||||||
|
adminTurnOnShadowMuting () {
|
||||||
|
if (!this.hero.flags) {
|
||||||
|
this.hero.flags = {};
|
||||||
|
}
|
||||||
|
this.hero.flags.chatShadowMuted = true;
|
||||||
|
|
||||||
|
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
||||||
|
},
|
||||||
|
adminTurnOffShadowMuting () {
|
||||||
|
if (!this.hero.flags) {
|
||||||
|
this.hero.flags = {};
|
||||||
|
}
|
||||||
|
this.hero.flags.chatShadowMuted = false;
|
||||||
|
|
||||||
|
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
||||||
|
},
|
||||||
adminRevokeChat () {
|
adminRevokeChat () {
|
||||||
if (!this.hero.flags) {
|
if (!this.hero.flags) {
|
||||||
this.hero.flags = {};
|
this.hero.flags = {};
|
||||||
@@ -612,9 +645,13 @@ export default {
|
|||||||
|
|
||||||
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
||||||
},
|
},
|
||||||
async loadAdminTools () {
|
async toggleAdminTools () {
|
||||||
|
if (this.adminToolsLoaded) {
|
||||||
|
this.adminToolsLoaded = false;
|
||||||
|
} else {
|
||||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: this.user._id });
|
this.hero = await this.$store.dispatch('hall:getHero', { uuid: this.user._id });
|
||||||
this.adminToolsLoaded = true;
|
this.adminToolsLoaded = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
showAllocation () {
|
showAllocation () {
|
||||||
return this.user._id === this.userLoggedIn._id && this.hasClass;
|
return this.user._id === this.userLoggedIn._id && this.hasClass;
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export const MAX_SUMMARY_SIZE_FOR_GUILDS = 250;
|
|||||||
export const MAX_SUMMARY_SIZE_FOR_CHALLENGES = 250;
|
export const MAX_SUMMARY_SIZE_FOR_CHALLENGES = 250;
|
||||||
export const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = 3;
|
export const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = 3;
|
||||||
|
|
||||||
|
export const CHAT_FLAG_LIMIT_FOR_HIDING = 2; // hide posts that have this many flags
|
||||||
|
export const CHAT_FLAG_FROM_MOD = 5; // a flag from a moderator counts as this many flags
|
||||||
|
export const CHAT_FLAG_FROM_SHADOW_MUTE = 10; // a shadow-muted user's post starts with this many flags
|
||||||
|
// @TODO use those constants to replace hard-coded numbers
|
||||||
|
|
||||||
export const SUPPORTED_SOCIAL_NETWORKS = [
|
export const SUPPORTED_SOCIAL_NETWORKS = [
|
||||||
{key: 'facebook', name: 'Facebook'},
|
{key: 'facebook', name: 'Facebook'},
|
||||||
{key: 'google', name: 'Google'},
|
{key: 'google', name: 'Google'},
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import {
|
|||||||
SUPPORTED_SOCIAL_NETWORKS,
|
SUPPORTED_SOCIAL_NETWORKS,
|
||||||
GUILDS_PER_PAGE,
|
GUILDS_PER_PAGE,
|
||||||
PARTY_LIMIT_MEMBERS,
|
PARTY_LIMIT_MEMBERS,
|
||||||
|
CHAT_FLAG_LIMIT_FOR_HIDING,
|
||||||
|
CHAT_FLAG_FROM_MOD,
|
||||||
|
CHAT_FLAG_FROM_SHADOW_MUTE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
api.constants = {
|
api.constants = {
|
||||||
@@ -40,6 +43,9 @@ api.constants = {
|
|||||||
SUPPORTED_SOCIAL_NETWORKS,
|
SUPPORTED_SOCIAL_NETWORKS,
|
||||||
GUILDS_PER_PAGE,
|
GUILDS_PER_PAGE,
|
||||||
PARTY_LIMIT_MEMBERS,
|
PARTY_LIMIT_MEMBERS,
|
||||||
|
CHAT_FLAG_LIMIT_FOR_HIDING,
|
||||||
|
CHAT_FLAG_FROM_MOD,
|
||||||
|
CHAT_FLAG_FROM_SHADOW_MUTE,
|
||||||
};
|
};
|
||||||
// TODO Move these under api.constants
|
// TODO Move these under api.constants
|
||||||
api.maxLevel = MAX_LEVEL;
|
api.maxLevel = MAX_LEVEL;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
|||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { chatModel as Chat } from '../../models/message';
|
import { chatModel as Chat } from '../../models/message';
|
||||||
|
import common from '../../../common';
|
||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -139,7 +140,7 @@ api.postChat = {
|
|||||||
{name: 'AUTHOR_USERNAME', content: user.profile.name},
|
{name: 'AUTHOR_USERNAME', content: user.profile.name},
|
||||||
{name: 'AUTHOR_UUID', content: user._id},
|
{name: 'AUTHOR_UUID', content: user._id},
|
||||||
{name: 'AUTHOR_EMAIL', content: authorEmail},
|
{name: 'AUTHOR_EMAIL', content: authorEmail},
|
||||||
{name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId=${user._id}`},
|
{name: 'AUTHOR_MODAL_URL', content: `/profile/${user._id}`},
|
||||||
|
|
||||||
{name: 'GROUP_NAME', content: group.name},
|
{name: 'GROUP_NAME', content: group.name},
|
||||||
{name: 'GROUP_TYPE', content: group.type},
|
{name: 'GROUP_TYPE', content: group.type},
|
||||||
@@ -162,12 +163,12 @@ api.postChat = {
|
|||||||
|
|
||||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
if (group.privacy !== 'private' && user.flags.chatRevoked) {
|
if (group.privacy === 'public' && user.flags.chatRevoked) {
|
||||||
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent banned words being posted, except in private guilds/parties and in certain public guilds with specific topics
|
// prevent banned words being posted, except in private guilds/parties and in certain public guilds with specific topics
|
||||||
if (group.privacy !== 'private' && !guildsAllowingBannedWords[group._id]) {
|
if (group.privacy === 'public' && !guildsAllowingBannedWords[group._id]) {
|
||||||
let matchedBadWords = getBannedWordsFromText(req.body.message);
|
let matchedBadWords = getBannedWordsFromText(req.body.message);
|
||||||
if (matchedBadWords.length > 0) {
|
if (matchedBadWords.length > 0) {
|
||||||
throw new BadRequest(res.t('bannedWordUsed', {swearWordsUsed: matchedBadWords.join(', ')}));
|
throw new BadRequest(res.t('bannedWordUsed', {swearWordsUsed: matchedBadWords.join(', ')}));
|
||||||
@@ -186,7 +187,43 @@ api.postChat = {
|
|||||||
if (client) {
|
if (client) {
|
||||||
client = client.replace('habitica-', '');
|
client = client.replace('habitica-', '');
|
||||||
}
|
}
|
||||||
const newChatMessage = group.sendChat({message: req.body.message, user, metaData: null, client});
|
|
||||||
|
let flagCount = 0;
|
||||||
|
if (group.privacy === 'public' && user.flags.chatShadowMuted) {
|
||||||
|
flagCount = common.constants.CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||||
|
let message = req.body.message;
|
||||||
|
|
||||||
|
// Email the mods
|
||||||
|
let authorEmail = getUserInfo(user, ['email']).email;
|
||||||
|
let groupUrl = getGroupUrl(group);
|
||||||
|
|
||||||
|
let report = [
|
||||||
|
{name: 'MESSAGE_TIME', content: (new Date()).toString()},
|
||||||
|
{name: 'MESSAGE_TEXT', content: message},
|
||||||
|
|
||||||
|
{name: 'AUTHOR_USERNAME', content: user.profile.name},
|
||||||
|
{name: 'AUTHOR_UUID', content: user._id},
|
||||||
|
{name: 'AUTHOR_EMAIL', content: authorEmail},
|
||||||
|
{name: 'AUTHOR_MODAL_URL', content: `/profile/${user._id}`},
|
||||||
|
|
||||||
|
{name: 'GROUP_NAME', content: group.name},
|
||||||
|
{name: 'GROUP_TYPE', content: group.type},
|
||||||
|
{name: 'GROUP_ID', content: group._id},
|
||||||
|
{name: 'GROUP_URL', content: groupUrl},
|
||||||
|
];
|
||||||
|
|
||||||
|
sendTxn(FLAG_REPORT_EMAILS, 'shadow-muted-post-report-to-mods', report);
|
||||||
|
|
||||||
|
// Slack the mods
|
||||||
|
slack.sendShadowMutedPostNotification({
|
||||||
|
authorEmail,
|
||||||
|
author: user,
|
||||||
|
group,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChatMessage = group.sendChat({message: req.body.message, user, flagCount, metaData: null, client});
|
||||||
let toSave = [newChatMessage.save()];
|
let toSave = [newChatMessage.save()];
|
||||||
|
|
||||||
if (group.type === 'party') {
|
if (group.type === 'party') {
|
||||||
@@ -372,12 +409,12 @@ api.clearChatFlags = {
|
|||||||
{name: 'ADMIN_USERNAME', content: user.profile.name},
|
{name: 'ADMIN_USERNAME', content: user.profile.name},
|
||||||
{name: 'ADMIN_UUID', content: user._id},
|
{name: 'ADMIN_UUID', content: user._id},
|
||||||
{name: 'ADMIN_EMAIL', content: adminEmailContent},
|
{name: 'ADMIN_EMAIL', content: adminEmailContent},
|
||||||
{name: 'ADMIN_MODAL_URL', content: `/static/front/#?memberId=${user._id}`},
|
{name: 'ADMIN_MODAL_URL', content: `/profile/${user._id}`},
|
||||||
|
|
||||||
{name: 'AUTHOR_USERNAME', content: message.user},
|
{name: 'AUTHOR_USERNAME', content: message.user},
|
||||||
{name: 'AUTHOR_UUID', content: message.uuid},
|
{name: 'AUTHOR_UUID', content: message.uuid},
|
||||||
{name: 'AUTHOR_EMAIL', content: authorEmail},
|
{name: 'AUTHOR_EMAIL', content: authorEmail},
|
||||||
{name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId=${message.uuid}`},
|
{name: 'AUTHOR_MODAL_URL', content: `/profile/${message.uuid}`},
|
||||||
|
|
||||||
{name: 'GROUP_NAME', content: group.name},
|
{name: 'GROUP_NAME', content: group.name},
|
||||||
{name: 'GROUP_TYPE', content: group.type},
|
{name: 'GROUP_TYPE', content: group.type},
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ api.getHeroes = {
|
|||||||
// Note, while the following routes are called getHero / updateHero
|
// Note, while the following routes are called getHero / updateHero
|
||||||
// they can be used by admins to get/update any user
|
// they can be used by admins to get/update any user
|
||||||
|
|
||||||
const heroAdminFields = 'contributor balance profile.name purchased items auth flags.chatRevoked';
|
const heroAdminFields = 'contributor balance profile.name purchased items auth flags.chatRevoked flags.chatShadowMuted';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID or Username
|
* @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID or Username
|
||||||
@@ -213,7 +213,10 @@ const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0};
|
|||||||
* {
|
* {
|
||||||
* "balance": 1000,
|
* "balance": 1000,
|
||||||
* "auth": {"blocked": false},
|
* "auth": {"blocked": false},
|
||||||
* "flags": {"chatRevoked": true},
|
* "flags": {
|
||||||
|
* "chatRevoked": true,
|
||||||
|
* "chatShadowMuted": true
|
||||||
|
* },
|
||||||
* "purchased": {"ads": true},
|
* "purchased": {"ads": true},
|
||||||
* "contributor": {
|
* "contributor": {
|
||||||
* "admin": true,
|
* "admin": true,
|
||||||
@@ -286,6 +289,7 @@ api.updateHero = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
||||||
|
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
|
||||||
|
|
||||||
let savedHero = await hero.save();
|
let savedHero = await hero.save();
|
||||||
let heroJSON = savedHero.toJSON();
|
let heroJSON = savedHero.toJSON();
|
||||||
|
|||||||
@@ -194,6 +194,49 @@ function sendSubscriptionNotification ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendShadowMutedPostNotification ({
|
||||||
|
authorEmail,
|
||||||
|
author,
|
||||||
|
group,
|
||||||
|
message,
|
||||||
|
}) {
|
||||||
|
if (SKIP_FLAG_METHODS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let titleLink;
|
||||||
|
let authorName;
|
||||||
|
let title = `Shadow-Muted Post in ${group.name}`;
|
||||||
|
let text = `@${author.auth.local.username} / ${author.profile.name} posted while shadow-muted`;
|
||||||
|
|
||||||
|
if (group.id === TAVERN_ID) {
|
||||||
|
titleLink = `${BASE_URL}/groups/tavern`;
|
||||||
|
} else {
|
||||||
|
titleLink = `${BASE_URL}/groups/guild/${group.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
authorName = formatUser({
|
||||||
|
name: author.auth.local.username,
|
||||||
|
displayName: author.profile.name,
|
||||||
|
email: authorEmail,
|
||||||
|
uuid: author.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
flagSlack.send({
|
||||||
|
text,
|
||||||
|
attachments: [{
|
||||||
|
fallback: 'Shadow-Muted Message',
|
||||||
|
color: 'danger',
|
||||||
|
author_name: authorName,
|
||||||
|
title,
|
||||||
|
title_link: titleLink,
|
||||||
|
text: message,
|
||||||
|
mrkdwn_in: [
|
||||||
|
'text',
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sendSlurNotification ({
|
function sendSlurNotification ({
|
||||||
authorEmail,
|
authorEmail,
|
||||||
author,
|
author,
|
||||||
@@ -243,6 +286,7 @@ module.exports = {
|
|||||||
sendFlagNotification,
|
sendFlagNotification,
|
||||||
sendInboxFlagNotification,
|
sendInboxFlagNotification,
|
||||||
sendSubscriptionNotification,
|
sendSubscriptionNotification,
|
||||||
|
sendShadowMutedPostNotification,
|
||||||
sendSlurNotification,
|
sendSlurNotification,
|
||||||
formatUser,
|
formatUser,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS
|
|||||||
const MAX_SUMMARY_SIZE_FOR_GUILDS = shared.constants.MAX_SUMMARY_SIZE_FOR_GUILDS;
|
const MAX_SUMMARY_SIZE_FOR_GUILDS = shared.constants.MAX_SUMMARY_SIZE_FOR_GUILDS;
|
||||||
const GUILDS_PER_PAGE = shared.constants.GUILDS_PER_PAGE;
|
const GUILDS_PER_PAGE = shared.constants.GUILDS_PER_PAGE;
|
||||||
|
|
||||||
|
const CHAT_FLAG_LIMIT_FOR_HIDING = shared.constants.CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||||
|
|
||||||
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||||
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||||
const MAX_UPDATE_RETRIES = 5;
|
const MAX_UPDATE_RETRIES = 5;
|
||||||
@@ -367,8 +369,8 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use
|
|||||||
chatMsg.flags = {};
|
chatMsg.flags = {};
|
||||||
if (chatMsg._meta) chatMsg._meta = undefined;
|
if (chatMsg._meta) chatMsg._meta = undefined;
|
||||||
|
|
||||||
// Messages with >= 2 flags are hidden to non admins and non authors
|
// Messages with too many flags are hidden to non-admins and non-authors
|
||||||
if (user._id !== chatMsg.uuid && chatMsg.flagCount >= 2) return undefined;
|
if (user._id !== chatMsg.uuid && chatMsg.flagCount >= CHAT_FLAG_LIMIT_FOR_HIDING) return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatMsg;
|
return chatMsg;
|
||||||
@@ -510,8 +512,8 @@ schema.methods.getMemberCount = async function getMemberCount () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.sendChat = function sendChat (options = {}) {
|
schema.methods.sendChat = function sendChat (options = {}) {
|
||||||
const {message, user, metaData, client, info = {}} = options;
|
const {message, user, metaData, client, flagCount = 0, info = {}} = options;
|
||||||
let newMessage = messageDefaults(message, user, client, info);
|
let newMessage = messageDefaults(message, user, client, flagCount, info);
|
||||||
let newChatMessage = new Chat();
|
let newChatMessage = new Chat();
|
||||||
newChatMessage = Object.assign(newChatMessage, newMessage);
|
newChatMessage = Object.assign(newChatMessage, newMessage);
|
||||||
newChatMessage.groupId = this._id;
|
newChatMessage.groupId = this._id;
|
||||||
@@ -528,8 +530,11 @@ schema.methods.sendChat = function sendChat (options = {}) {
|
|||||||
// newChatMessage is possibly returned
|
// newChatMessage is possibly returned
|
||||||
this.sendGroupChatReceivedWebhooks(newChatMessage);
|
this.sendGroupChatReceivedWebhooks(newChatMessage);
|
||||||
|
|
||||||
// do not send notifications for guilds with more than 5000 users and for the tavern
|
// do not send notifications for:
|
||||||
if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF) {
|
// - groups that never send notifications (e.g., Tavern)
|
||||||
|
// - groups with very many users
|
||||||
|
// - messages that have already been flagged to hide them
|
||||||
|
if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF || newChatMessage.flagCount >= CHAT_FLAG_LIMIT_FOR_HIDING) {
|
||||||
return newChatMessage;
|
return newChatMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function setUserStyles (newMessage, user) {
|
|||||||
newMessage.markModified('userStyles contributor');
|
newMessage.markModified('userStyles contributor');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function messageDefaults (msg, user, client, info = {}) {
|
export function messageDefaults (msg, user, client, flagCount = 0, info = {}) {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
const message = {
|
const message = {
|
||||||
id,
|
id,
|
||||||
@@ -118,7 +118,7 @@ export function messageDefaults (msg, user, client, info = {}) {
|
|||||||
timestamp: Number(new Date()),
|
timestamp: Number(new Date()),
|
||||||
likes: {},
|
likes: {},
|
||||||
flags: {},
|
flags: {},
|
||||||
flagCount: 0,
|
flagCount,
|
||||||
client,
|
client,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ let schema = new Schema({
|
|||||||
return {};
|
return {};
|
||||||
}},
|
}},
|
||||||
chatRevoked: Boolean,
|
chatRevoked: Boolean,
|
||||||
|
chatShadowMuted: Boolean,
|
||||||
// Used to track the status of recapture emails sent to each user,
|
// Used to track the status of recapture emails sent to each user,
|
||||||
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
|
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
|
||||||
recaptureEmailsPhase: {$type: Number, default: 0},
|
recaptureEmailsPhase: {$type: Number, default: 0},
|
||||||
|
|||||||
Reference in New Issue
Block a user