diff --git a/config.json.example b/config.json.example
index 04c1c535b4..3875097d6c 100644
--- a/config.json.example
+++ b/config.json.example
@@ -1,4 +1,5 @@
{
+ "ACCOUNT_MIN_CHAT_AGE": "0",
"ADMIN_EMAIL": "you@example.com",
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
"AMAZON_PAYMENTS_MODE": "sandbox",
diff --git a/test/api/v3/integration/chat/DELETE-chat_id.test.js b/test/api/v3/integration/chat/DELETE-chat_id.test.js
index 0d53dfa999..7a9756e673 100644
--- a/test/api/v3/integration/chat/DELETE-chat_id.test.js
+++ b/test/api/v3/integration/chat/DELETE-chat_id.test.js
@@ -15,6 +15,10 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
type: 'guild',
privacy: 'public',
},
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ balance: 10,
+ },
});
groupWithChat = group;
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 acff8b507b..909fbfa7a0 100644
--- a/test/api/v3/integration/chat/POST-chat.flag.test.js
+++ b/test/api/v3/integration/chat/POST-chat.flag.test.js
@@ -117,7 +117,9 @@ describe('POST /chat/:chatId/flag', () => {
});
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 });
await deletedUser.del('/user', {
password: 'password',
diff --git a/test/api/v3/integration/chat/POST-chat.like.test.js b/test/api/v3/integration/chat/POST-chat.like.test.js
index 192f3e3aa7..beab851bb0 100644
--- a/test/api/v3/integration/chat/POST-chat.like.test.js
+++ b/test/api/v3/integration/chat/POST-chat.like.test.js
@@ -18,11 +18,16 @@ describe('POST /chat/:chatId/like', () => {
privacy: 'public',
},
members: 1,
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ balance: 10,
+ },
});
user = groupLeader;
groupWithChat = group;
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 () => {
diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js
index 24283de89e..73ee343c79 100644
--- a/test/api/v3/integration/chat/POST-chat.test.js
+++ b/test/api/v3/integration/chat/POST-chat.test.js
@@ -38,10 +38,15 @@ describe('POST /chat', () => {
members: 2,
});
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;
member = members[0]; // 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 () => {
@@ -104,7 +109,10 @@ describe('POST /chat', () => {
});
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 });
@@ -122,7 +130,10 @@ describe('POST /chat', () => {
});
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 });
@@ -183,7 +194,10 @@ describe('POST /chat', () => {
});
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 });
@@ -202,7 +216,10 @@ describe('POST /chat', () => {
});
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 });
@@ -312,6 +329,7 @@ describe('POST /chat', () => {
},
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 });
@@ -330,6 +348,7 @@ describe('POST /chat', () => {
// Update the bannedWordsAllowed property for the group
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 });
@@ -345,6 +364,7 @@ describe('POST /chat', () => {
},
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 });
@@ -411,6 +431,7 @@ describe('POST /chat', () => {
},
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 });
@@ -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 () => {
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
@@ -492,6 +523,7 @@ describe('POST /chat', () => {
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
+ 'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
@@ -517,6 +549,7 @@ describe('POST /chat', () => {
};
const backer = await generateUser({
backer: backerInfo,
+ 'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
@@ -587,6 +620,9 @@ describe('POST /chat', () => {
privacy: 'private',
},
members: 1,
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ },
});
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
diff --git a/test/api/v3/integration/chat/POST-chat_seen.test.js b/test/api/v3/integration/chat/POST-chat_seen.test.js
index f64df56146..9cd2103bc6 100644
--- a/test/api/v3/integration/chat/POST-chat_seen.test.js
+++ b/test/api/v3/integration/chat/POST-chat_seen.test.js
@@ -15,6 +15,10 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'public',
},
members: 1,
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ balance: 10,
+ },
});
guild = group;
@@ -51,6 +55,9 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'private',
},
members: 1,
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ },
});
party = group;
diff --git a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js
index e9f77e5561..4bb99cc33f 100644
--- a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js
+++ b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js
@@ -18,6 +18,10 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
type: 'guild',
privacy: 'public',
},
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ balance: 10,
+ },
});
groupWithChat = group;
@@ -65,6 +69,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
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' });
privateMessage = privateMessage.message;
diff --git a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js
index 56de50b822..b7b16a7f04 100644
--- a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js
+++ b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js
@@ -37,6 +37,7 @@ describe('POST /groups/:groupId/leave', () => {
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
memberCount = group.memberCount;
+ await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
@@ -152,6 +153,10 @@ describe('POST /groups/:groupId/leave', () => {
type: 'guild',
},
invites: 1,
+ leaderDetails: {
+ 'auth.timestamps.created': new Date('2022-01-01'),
+ balance: 10,
+ },
});
privateGuild = group;
diff --git a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js
index 7b7fc0e56e..c8a903d891 100644
--- a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js
+++ b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js
@@ -153,6 +153,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
},
invites: 1,
members: 2,
+ leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') },
});
party = group;
diff --git a/test/api/v3/integration/notifications/prevent-multiple-notification.js b/test/api/v3/integration/notifications/prevent-multiple-notification.js
index cc4fd20a3d..31da8c1e16 100644
--- a/test/api/v3/integration/notifications/prevent-multiple-notification.js
+++ b/test/api/v3/integration/notifications/prevent-multiple-notification.js
@@ -25,6 +25,7 @@ describe('Prevent multiple notifications', () => {
for (let i = 0; i < 4; i += 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(
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
);
diff --git a/test/helpers/start-server.js b/test/helpers/start-server.js
index 8830224f70..570f53461f 100644
--- a/test/helpers/start-server.js
+++ b/test/helpers/start-server.js
@@ -11,6 +11,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc
setupNconf('./config.json.example');
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
nconf.set('NODE_ENV', 'test');
+ nconf.set('ACCOUNT_MIN_CHAT_AGE', '2');
nconf.set('IS_TEST', true);
// We require src/server and not src/index because
// 1. nconf is already setup
diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json
index e5186a08f8..e6c0f943cc 100644
--- a/website/common/locales/en/groups.json
+++ b/website/common/locales/en/groups.json
@@ -364,5 +364,6 @@
"managerNotes": "Manager's Notes",
"assignedDateOnly": "Assigned on <%= date %>",
"assignedDateAndUser": "Assigned by @<%- username %> on <%= date %>",
- "claimRewards": "Claim Rewards"
+ "claimRewards": "Claim Rewards",
+ "chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later."
}
diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js
index c19a16e5be..72df0f719d 100644
--- a/website/server/controllers/api-v3/chat.js
+++ b/website/server/controllers/api-v3/chat.js
@@ -1,3 +1,4 @@
+import moment from 'moment';
import nconf from 'nconf';
import { authWithHeaders } from '../../middlewares/auth';
import { model as Group } from '../../models/group';
@@ -22,7 +23,11 @@ import { getMatchesByWordArray } from '../../libs/stringUtils';
import bannedSlurs from '../../libs/bannedSlurs';
import apiError from '../../libs/apiError';
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 }));
/**
@@ -188,6 +193,17 @@ api.postChat = {
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 [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText);
let client = req.headers['x-client'] || '3rd Party';