diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index 9fb625edba..59ac15f2f8 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -10,6 +10,8 @@ import { TAVERN_ID, } from '../../../../../website/server/models/group'; import { v4 as generateUUID } from 'uuid'; +import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils'; +import bannedWords from '../../../../../website/server/libs/bannedWords'; import * as email from '../../../../../website/server/libs/email'; import { IncomingWebhook } from '@slack/client'; import nconf from 'nconf'; @@ -21,6 +23,9 @@ describe('POST /chat', () => { let testMessage = 'Test Message'; let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE'; let testSlurMessage = 'message with TEST_PLACEHOLDER_SLUR_WORD_HERE'; + let bannedWordErrorMessage = t('bannedWordUsed').split('.'); + bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`; + bannedWordErrorMessage = bannedWordErrorMessage.join('.'); before(async () => { let { group, groupLeader, members } = await createAndPopulateGroup({ @@ -31,7 +36,6 @@ describe('POST /chat', () => { }, members: 2, }); - user = groupLeader; groupWithChat = group; member = members[0]; @@ -85,11 +89,11 @@ describe('POST /chat', () => { context('banned word', () => { it('returns an error when chat message contains a banned word in tavern', async () => { await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage})) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('bannedWordUsed'), - }); + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: bannedWordErrorMessage, + }); }); it('errors when word is part of a phrase', async () => { @@ -98,7 +102,7 @@ describe('POST /chat', () => { .to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', - message: t('bannedWordUsed'), + message: bannedWordErrorMessage, }); }); @@ -108,10 +112,26 @@ describe('POST /chat', () => { .to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', - message: t('bannedWordUsed'), + message: bannedWordErrorMessage, }); }); + it('checks error message has the banned words used', async () => { + let randIndex = Math.floor(Math.random() * (bannedWords.length + 1)); + let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, '')); + let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`; + await expect(user.post('/groups/habitrpg/chat', { message: chatMessage})) + .to.eventually.be.rejected + .and.have.property('message') + .that.includes(testBannedWords.join(', ')); + }); + + it('check all banned words are matched', async () => { + let message = bannedWords.join(',').replace(/\\/g, ''); + let matches = getMatchesByWordArray(message, bannedWords); + expect(matches.length).to.equal(bannedWords.length); + }); + it('does not error when bad word is suffix of a word', async () => { let wordAsSuffix = `prefix${testBannedWordMessage}`; let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix}); diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index 20d3ed885d..74600c32ea 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -14,6 +14,7 @@ import pusher from '../../libs/pusher'; import nconf from 'nconf'; import Bluebird from 'bluebird'; import bannedWords from '../../libs/bannedWords'; +import { getMatchesByWordArray } from '../../libs/stringUtils'; import { TAVERN_ID } from '../../models/group'; import bannedSlurs from '../../libs/bannedSlurs'; @@ -54,42 +55,9 @@ async function getAuthorEmailFromMessage (message) { } } -// @TODO: Probably move this to a library -function matchExact (r, str) { - let match = str.match(r); - return match !== null && match[0] !== null; -} - -let bannedWordRegexs = []; -for (let i = 0; i < bannedWords.length; i += 1) { - let word = bannedWords[i]; - let regEx = new RegExp(`\\b([^a-z]+)?${word.toLowerCase()}([^a-z]+)?\\b`); - bannedWordRegexs.push(regEx); -} - -function textContainsBannedWords (message) { - for (let i = 0; i < bannedWordRegexs.length; i += 1) { - let regEx = bannedWordRegexs[i]; - if (matchExact(regEx, message.toLowerCase())) return true; - } - - return false; -} - -let bannedSlurRegexs = []; -for (let i = 0; i < bannedSlurs.length; i += 1) { - let word = bannedSlurs[i]; - let regEx = new RegExp(`\\b([^a-z]+)?${word.toLowerCase()}([^a-z]+)?\\b`); - bannedSlurRegexs.push(regEx); -} - function textContainsBannedSlur (message) { - for (let i = 0; i < bannedSlurRegexs.length; i += 1) { - let regEx = bannedSlurRegexs[i]; - if (matchExact(regEx, message.toLowerCase())) return true; - } - - return false; + let bannedSlursMatched = getMatchesByWordArray(message, bannedSlurs); + return bannedSlursMatched.length > 0; } /** @@ -124,6 +92,10 @@ api.getChat = { }, }; +function getBannedWordsFromText (message) { + return getMatchesByWordArray(message, bannedWords); +} + /** * @api {post} /api/v3/groups/:groupId/chat Post chat message to a group * @apiName PostChat @@ -199,8 +171,13 @@ api.postChat = { throw new NotAuthorized(res.t('chatPrivilegesRevoked')); } - if (group._id === TAVERN_ID && textContainsBannedWords(req.body.message)) { - throw new BadRequest(res.t('bannedWordUsed')); + if (group._id === TAVERN_ID) { + let matchedBadWords = getBannedWordsFromText(req.body.message); + if (matchedBadWords.length > 0) { + let message = res.t('bannedWordUsed').split('.'); + message[0] += ` (${matchedBadWords.join(', ')})`; + throw new BadRequest(message.join('.')); + } } let lastClientMsg = req.query.previousMsg; diff --git a/website/server/libs/stringUtils.js b/website/server/libs/stringUtils.js new file mode 100644 index 0000000000..abed3995d6 --- /dev/null +++ b/website/server/libs/stringUtils.js @@ -0,0 +1,18 @@ + +export function removePunctuationFromString (str) { + return str.replace(/[.,\/#!@$%\^&;:{}=\-_`~()]/g, ' '); +} + +export function getMatchesByWordArray (str, wordsToMatch) { + let matchedWords = []; + let wordRegexs = wordsToMatch.map((word) => new RegExp(`\\b([^a-z]+)?${word.toLowerCase()}([^a-z]+)?\\b`)); + for (let i = 0; i < wordRegexs.length; i += 1) { + let regEx = wordRegexs[i]; + let match = str.toLowerCase().match(regEx); + if (match !== null && match[0] !== null) { + let trimmedMatch = removePunctuationFromString(match[0]).trim(); + matchedWords.push(trimmedMatch); + } + } + return matchedWords; +}