mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 13:17:24 +01:00
Server setting to disallow chat from new accounts (#13952)
* feat(chat): server setting to disallow chat from new accounts * fix(tests): many adjustments to handle chat minimum age * fix(tests): address issues outside of chat posting * chore(analytics): add incident logging * fix(config): allow instant chat for dev purposes * fix(test): finely age one more user * fix(test): member not leader Co-authored-by: SabreCat <sabe@habitica.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"ACCOUNT_MIN_CHAT_AGE": "0",
|
||||||
"ADMIN_EMAIL": "you@example.com",
|
"ADMIN_EMAIL": "you@example.com",
|
||||||
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
|
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
|
||||||
"AMAZON_PAYMENTS_MODE": "sandbox",
|
"AMAZON_PAYMENTS_MODE": "sandbox",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
|
|||||||
type: 'guild',
|
type: 'guild',
|
||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
},
|
},
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
balance: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
groupWithChat = group;
|
groupWithChat = group;
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ describe('POST /chat/:chatId/flag', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Flags a chat when the author\'s account was deleted', async () => {
|
it('Flags a chat when the author\'s account was deleted', async () => {
|
||||||
const deletedUser = await generateUser();
|
const deletedUser = await generateUser({
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
});
|
||||||
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
|
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
|
||||||
await deletedUser.del('/user', {
|
await deletedUser.del('/user', {
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ describe('POST /chat/:chatId/like', () => {
|
|||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
balance: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
user = groupLeader;
|
user = groupLeader;
|
||||||
groupWithChat = group;
|
groupWithChat = group;
|
||||||
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
|
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
|
||||||
|
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns an error when chat message is not found', async () => {
|
it('Returns an error when chat message is not found', async () => {
|
||||||
|
|||||||
@@ -38,10 +38,15 @@ describe('POST /chat', () => {
|
|||||||
members: 2,
|
members: 2,
|
||||||
});
|
});
|
||||||
user = groupLeader;
|
user = groupLeader;
|
||||||
await user.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); // prevent tests accidentally throwing messageGroupChatSpam
|
await user.update({
|
||||||
|
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
}); // prevent tests accidentally throwing messageGroupChatSpam
|
||||||
groupWithChat = group;
|
groupWithChat = group;
|
||||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||||
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
|
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
|
||||||
|
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
|
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns an error when no message is provided', async () => {
|
it('Returns an error when no message is provided', async () => {
|
||||||
@@ -104,7 +109,10 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const privateGuildMemberWithChatsRevoked = members[0];
|
const privateGuildMemberWithChatsRevoked = members[0];
|
||||||
await privateGuildMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
|
await privateGuildMemberWithChatsRevoked.update({
|
||||||
|
'flags.chatRevoked': true,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
});
|
||||||
|
|
||||||
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
|
|
||||||
@@ -122,7 +130,10 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const privatePartyMemberWithChatsRevoked = members[0];
|
const privatePartyMemberWithChatsRevoked = members[0];
|
||||||
await privatePartyMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
|
await privatePartyMemberWithChatsRevoked.update({
|
||||||
|
'flags.chatRevoked': true,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
});
|
||||||
|
|
||||||
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
|
|
||||||
@@ -183,7 +194,10 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userWithChatShadowMuted = members[0];
|
const userWithChatShadowMuted = members[0];
|
||||||
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
|
await userWithChatShadowMuted.update({
|
||||||
|
'flags.chatShadowMuted': true,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
});
|
||||||
|
|
||||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
|
|
||||||
@@ -202,7 +216,10 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userWithChatShadowMuted = members[0];
|
const userWithChatShadowMuted = members[0];
|
||||||
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
|
await userWithChatShadowMuted.update({
|
||||||
|
'flags.chatShadowMuted': true,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
});
|
||||||
|
|
||||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
|
|
||||||
@@ -312,6 +329,7 @@ describe('POST /chat', () => {
|
|||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
});
|
});
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
|
|
||||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||||
|
|
||||||
@@ -330,6 +348,7 @@ describe('POST /chat', () => {
|
|||||||
|
|
||||||
// Update the bannedWordsAllowed property for the group
|
// Update the bannedWordsAllowed property for the group
|
||||||
group.update({ bannedWordsAllowed: true });
|
group.update({ bannedWordsAllowed: true });
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
|
|
||||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||||
|
|
||||||
@@ -345,6 +364,7 @@ describe('POST /chat', () => {
|
|||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
});
|
});
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
|
|
||||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||||
|
|
||||||
@@ -411,6 +431,7 @@ describe('POST /chat', () => {
|
|||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
});
|
});
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
|
|
||||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage });
|
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage });
|
||||||
|
|
||||||
@@ -430,6 +451,16 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors when user account is too young', async () => {
|
||||||
|
const brandNewUser = await generateUser();
|
||||||
|
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('chatTemporarilyUnavailable'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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`);
|
||||||
@@ -492,6 +523,7 @@ describe('POST /chat', () => {
|
|||||||
'items.currentMount': mount,
|
'items.currentMount': mount,
|
||||||
'items.currentPet': pet,
|
'items.currentPet': pet,
|
||||||
'preferences.style': style,
|
'preferences.style': style,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
});
|
});
|
||||||
await userWithStyle.sync();
|
await userWithStyle.sync();
|
||||||
|
|
||||||
@@ -517,6 +549,7 @@ describe('POST /chat', () => {
|
|||||||
};
|
};
|
||||||
const backer = await generateUser({
|
const backer = await generateUser({
|
||||||
backer: backerInfo,
|
backer: backerInfo,
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||||
@@ -587,6 +620,9 @@ describe('POST /chat', () => {
|
|||||||
privacy: 'private',
|
privacy: 'private',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
|
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ describe('POST /groups/:id/chat/seen', () => {
|
|||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
balance: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
guild = group;
|
guild = group;
|
||||||
@@ -51,6 +55,9 @@ describe('POST /groups/:id/chat/seen', () => {
|
|||||||
privacy: 'private',
|
privacy: 'private',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 1,
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
party = group;
|
party = group;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
|||||||
type: 'guild',
|
type: 'guild',
|
||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
},
|
},
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
balance: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
groupWithChat = group;
|
groupWithChat = group;
|
||||||
@@ -65,6 +69,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
|||||||
members: 1,
|
members: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
|
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
|
||||||
privateMessage = privateMessage.message;
|
privateMessage = privateMessage.message;
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe('POST /groups/:groupId/leave', () => {
|
|||||||
leader = groupLeader;
|
leader = groupLeader;
|
||||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||||
memberCount = group.memberCount;
|
memberCount = group.memberCount;
|
||||||
|
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents non members from leaving', async () => {
|
it('prevents non members from leaving', async () => {
|
||||||
@@ -152,6 +153,10 @@ describe('POST /groups/:groupId/leave', () => {
|
|||||||
type: 'guild',
|
type: 'guild',
|
||||||
},
|
},
|
||||||
invites: 1,
|
invites: 1,
|
||||||
|
leaderDetails: {
|
||||||
|
'auth.timestamps.created': new Date('2022-01-01'),
|
||||||
|
balance: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
privateGuild = group;
|
privateGuild = group;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
|||||||
},
|
},
|
||||||
invites: 1,
|
invites: 1,
|
||||||
members: 2,
|
members: 2,
|
||||||
|
leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') },
|
||||||
});
|
});
|
||||||
|
|
||||||
party = group;
|
party = group;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe('Prevent multiple notifications', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < 4; i += 1) {
|
for (let i = 0; i < 4; i += 1) {
|
||||||
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) {
|
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) {
|
||||||
|
await partyMembers[memberIndex].update({ 'auth.timestamps.created': new Date('2022-01-01') }); // eslint-disable-line no-await-in-loop
|
||||||
multipleChatMessages.push(
|
multipleChatMessages.push(
|
||||||
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
|
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc
|
|||||||
setupNconf('./config.json.example');
|
setupNconf('./config.json.example');
|
||||||
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
|
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
|
||||||
nconf.set('NODE_ENV', 'test');
|
nconf.set('NODE_ENV', 'test');
|
||||||
|
nconf.set('ACCOUNT_MIN_CHAT_AGE', '2');
|
||||||
nconf.set('IS_TEST', true);
|
nconf.set('IS_TEST', true);
|
||||||
// We require src/server and not src/index because
|
// We require src/server and not src/index because
|
||||||
// 1. nconf is already setup
|
// 1. nconf is already setup
|
||||||
|
|||||||
@@ -364,5 +364,6 @@
|
|||||||
"managerNotes": "Manager's Notes",
|
"managerNotes": "Manager's Notes",
|
||||||
"assignedDateOnly": "Assigned on <strong><%= date %></strong>",
|
"assignedDateOnly": "Assigned on <strong><%= date %></strong>",
|
||||||
"assignedDateAndUser": "Assigned by <strong>@<%- username %></strong> on <strong><%= date %></strong>",
|
"assignedDateAndUser": "Assigned by <strong>@<%- username %></strong> on <strong><%= date %></strong>",
|
||||||
"claimRewards": "Claim Rewards"
|
"claimRewards": "Claim Rewards",
|
||||||
|
"chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
@@ -22,7 +23,11 @@ import { getMatchesByWordArray } from '../../libs/stringUtils';
|
|||||||
import bannedSlurs from '../../libs/bannedSlurs';
|
import bannedSlurs from '../../libs/bannedSlurs';
|
||||||
import apiError from '../../libs/apiError';
|
import apiError from '../../libs/apiError';
|
||||||
import highlightMentions from '../../libs/highlightMentions';
|
import highlightMentions from '../../libs/highlightMentions';
|
||||||
|
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
|
||||||
|
|
||||||
|
const analytics = getAnalyticsServiceByEnvironment();
|
||||||
|
|
||||||
|
const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE'));
|
||||||
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true }));
|
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true }));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,6 +193,17 @@ api.postChat = {
|
|||||||
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if account is newer than the minimum age for chat participation
|
||||||
|
if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) {
|
||||||
|
analytics.track('chat age error', {
|
||||||
|
uuid: user._id,
|
||||||
|
hitType: 'event',
|
||||||
|
category: 'behavior',
|
||||||
|
headers: req.headers,
|
||||||
|
});
|
||||||
|
throw new BadRequest(res.t('chatTemporarilyUnavailable'));
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizedMessageText = sanitizeMessageText(req.body.message);
|
const sanitizedMessageText = sanitizeMessageText(req.body.message);
|
||||||
const [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText);
|
const [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText);
|
||||||
let client = req.headers['x-client'] || '3rd Party';
|
let client = req.headers['x-client'] || '3rd Party';
|
||||||
|
|||||||
Reference in New Issue
Block a user