diff --git a/test/api/v3/integration/user/PUT-user.test.js b/test/api/v3/integration/user/PUT-user.test.js index ced94b2232..315c8026e2 100644 --- a/test/api/v3/integration/user/PUT-user.test.js +++ b/test/api/v3/integration/user/PUT-user.test.js @@ -94,14 +94,6 @@ describe('PUT /user', () => { message: t('bannedSlurUsedInProfile'), }); - await expect(user.put('/user', { - 'profile.name': 'TESTPLACEHOLDERSWEARWORDHERE', - })).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('bannedWordUsedInProfile'), - }); - await expect(user.put('/user', { 'profile.name': 'namecontainsnewline\n', })).to.eventually.be.rejected.and.eql({ diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index d1b6a85aff..95c18aaff1 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -1222,9 +1222,10 @@ export default { const { nextRewardAt } = currentLoginDay; return ((nextRewardAt - previousRewardDay)); }, - save () { + async save () { const values = {}; const edits = cloneDeep(this.editingProfile); + const oldProfile = cloneDeep(this.user.profile); each(edits, (value, key) => { // Using toString because we need to compare two arrays (websites) @@ -1235,7 +1236,12 @@ export default { } }); - this.$store.dispatch('user:set', values); + await this.$store.dispatch('user:set', values).catch(() => { + this.user.profile = oldProfile; + this.editingProfile.name = this.user.profile.name; + this.editingProfile.imageUrl = this.user.profile.imageUrl; + this.editingProfile.blurb = this.user.profile.blurb; + }); this.editing = false; }, diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index bc7c6eee03..777cb6a908 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -16,7 +16,7 @@ "wiki": "Wiki", "resources": "Resources", "communityGuidelines": "Community Guidelines", - "bannedWordUsed": "Oops! Looks like this post contains a swearword or reference to an addictive substance or adult topic (<%= swearWordsUsed %>). Habitica keeps our chat very clean. Feel free to edit your message so you can post it! You must remove the word, not just censor it.", + "bannedWordUsed": "Oops! Looks like this post contains a swear word or reference to an addictive substance or adult topic (<%= swearWordsUsed %>). Habitica keeps our chat very clean. Feel free to edit your message so you can post it! You must remove the word, not just censor it.", "bannedSlurUsed": "Your post contained inappropriate language, and your chat privileges have been revoked.", "party": "Party", "usernameCopied": "Username copied to clipboard.", diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index c94dbc8add..a2614c1ee3 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -17,9 +17,6 @@ import { removeFromArray } from '../../libs/collectionManipulators'; import { getUserInfo } from '../../libs/email'; import * as slack from '../../libs/slack'; import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory'; -import bannedWords from '../../libs/bannedWords'; -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'; @@ -50,11 +47,6 @@ const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE')); const api = {}; -function textContainsBannedSlur (message) { - const bannedSlursMatched = getMatchesByWordArray(message, bannedSlurs); - return bannedSlursMatched.length > 0; -} - /** * @api {get} /api/v3/groups/:groupId/chat Get chat messages from a group * @apiName GetChat @@ -90,10 +82,6 @@ api.getChat = { }, }; -function getBannedWordsFromText (message) { - return getMatchesByWordArray(message, bannedWords); -} - /** * @api {post} /api/v3/groups/:groupId/chat Post chat message to a group * @apiName PostChat @@ -137,39 +125,6 @@ api.postChat = { throw new BadRequest(res.t('featureRetired')); } - // Check message for banned slurs - if (group && group.privacy !== 'private' && textContainsBannedSlur(req.body.message)) { - const { message } = req.body; - user.flags.chatRevoked = true; - await user.save(); - - // Email the mods - const authorEmail = getUserInfo(user, ['email']).email; - - // Slack the mods - slack.sendSlurNotification({ - authorEmail, - author: user, - group, - message, - }); - - throw new BadRequest(res.t('bannedSlurUsed')); - } - - if (group.privacy === 'public' && user.flags.chatRevoked) { - throw new NotAuthorized(res.t('chatPrivilegesRevoked')); - } - - // prevent banned words being posted, except in private guilds/parties - // and in certain public guilds with specific topics - if (group.privacy === 'public' && !group.bannedWordsAllowed) { - const matchedBadWords = getBannedWordsFromText(req.body.message); - if (matchedBadWords.length > 0) { - throw new BadRequest(res.t('bannedWordUsed', { swearWordsUsed: matchedBadWords.join(', ') })); - } - } - const chatRes = await Group.toJSONCleanChat(group, user); const lastClientMsg = req.query.previousMsg; const chatUpdated = !!( diff --git a/website/server/libs/bannedSlurs.js b/website/server/libs/bannedSlurs.js index 5dcdeb669c..c74c651b26 100644 --- a/website/server/libs/bannedSlurs.js +++ b/website/server/libs/bannedSlurs.js @@ -15,7 +15,7 @@ // Email admin@habitica.com to discuss the change you want made. // // All updates to this file must be done through a direct commit to limit -// the words visibility in GitHub to protect our coders, socialites, and +// the words visibility in GitHub to protect our coders and // wiki editors who look through PRs for information. // // When adding words that contain asterisks, put two backslashes before them. diff --git a/website/server/libs/bannedWords.js b/website/server/libs/bannedWords.js index 2b16f1ffdf..09d44a4fcc 100644 --- a/website/server/libs/bannedWords.js +++ b/website/server/libs/bannedWords.js @@ -2,8 +2,7 @@ // CONTENT WARNING: // -// This file contains slurs, swear words, religious oaths, and words related -// to addictive substance and adult topics. +// This file contains slurs, swear words, and religious oaths. // Do not read this file if you do not want to be exposed to those words. // // The words are stored in an array called `bannedWords` which is then @@ -18,7 +17,7 @@ // Email admin@habitica.com to discuss the change you want made. // // All updates to this file must be done through a direct commit to limit -// the words visibility in GitHub to protect our coders, socialites, and +// the words visibility in GitHub to protect our coders and // wiki editors who look through PRs for information. // // When adding words that contain asterisks, put two backslashes before them. @@ -85,13 +84,20 @@ const bannedWords = [ - 'damn', - 'goddamn', - 'damnit', - 'dammit', - 'damned', - 'omfg', - + 'ass', + 'arse', + 'arsehole', + 'asshole', + 'badarse', + 'badass', + 'bastard', + 'bastards', + 'bitch', + 'bitchy', + 'bitches', + 'bitching', + 'b\\*tch', + 'blowjob', 'bugger', 'buggery', 'buggering', @@ -101,6 +107,57 @@ const bannedWords = [ 'bullshitter', 'bullshiting', 'bullshitting', + 'cocksucker', + 'cocksucking', + 'cunnilingus', + 'dafuq', + 'fag', + 'fap', + 'fapping', + 'fellatio', + 'fuck', + 'fucks', + 'fucking', + 'fucked', + 'fuckwit', + 'fucker', + 'fuckers', + 'f\\*ck', + 'fuckhead', + 'fuckheads', + 'goddamn', + 'handjob', + 'kickarse', + 'kickass', + 'lmao', + 'lmfao', + 'omfg', + 'masturbate', + 'masturbates', + 'masturbating', + 'masturbation', + 'milf', + 'motherfucker', + 'motherfuckers', + 'motherfucking', + 'muthafucka', + 'nigga', + 'niggas', + 'nofap', + 'no fap', + 'no-fap', + 'no nut', + 'no-nut', + 'no-nut-november', + 'nutting', + 'nuttin', + 'rape', + 'raped', + 'raping', + 'r\\*pe', + 'r\\*ped', + 'r\\*ping', + 'rimjob', 'shiz', 'shit', 'shite', @@ -114,81 +171,12 @@ const bannedWords = [ 'sh\\*t', 'sh\\*tty', 'sh\\*tting', - 'fuck', - 'fucks', - 'fucking', - 'fucked', - 'fuckwit', - 'fucker', - 'fuckers', - 'f\\*ck', - 'fuckhead', - 'fuckheads', - 'motherfucker', - 'motherfuckers', - 'motherfucking', - 'muthafucka', - 'dafuq', - 'wtf', - 'stfu', - - 'ass', - 'arse', - 'asshole', - 'badass', - 'kickass', - 'arsehole', - 'badarse', - 'kickarse', - 'lmao', - 'lmfao', - - 'bitch', - 'bitchy', - 'bitches', - 'bitching', - 'b\\*tch', - - 'fag', 'slut', 'sluts', - 'nigga', - 'niggas', - 'bastard', - 'bastards', - - 'rape', - 'raped', - 'raping', - 'r\\*pe', - 'r\\*ped', - 'r\\*ping', - 'blowjob', - 'rimjob', - 'handjob', - 'cunnilingus', - 'fellatio', 'sodomy', - 'milf', - 'cocksucker', - 'cocksucking', - 'fap', - 'nofap', - 'no fap', - 'no-fap', - 'fapping', - 'no nut', - 'no-nut', - 'no-nut-november', - 'nutting', - 'nuttin', - 'masturbate', - 'masturbates', - 'masturbating', - 'masturbation', + 'stfu', + 'wtf', - 'heroin', - 'cocaine', ]; export default bannedWords; diff --git a/website/server/libs/slack.js b/website/server/libs/slack.js index 69c072e2a4..7143c33932 100644 --- a/website/server/libs/slack.js +++ b/website/server/libs/slack.js @@ -324,34 +324,21 @@ function sendShadowMutedPostNotification ({ .catch(err => logger.error(err, 'Error while sending flag data to Slack.')); } -function sendSlurNotification ({ +// slack slur notification for Profiles +function sendProfileSlurNotification ({ authorEmail, author, - group, - message, + uuid, + language, + problemContent, }) { if (SKIP_FLAG_METHODS) { return; } - const text = `${author.profile.name} (${author._id}) tried to post a slur`; + const title = 'User Profile Report: Slur'; + const titleLink = `${BASE_URL}/profile/${uuid}`; - let titleLink; - let title = `Slur in ${group.name}`; - - if (group.id === TAVERN_ID) { - titleLink = `${BASE_URL}/groups/tavern`; - } else if (group.privacy === 'public') { - titleLink = `${BASE_URL}/groups/guild/${group.id}`; - } else { - title += ` - (${group.privacy} ${group.type})`; - } - - const authorName = formatUser({ - name: author.auth.local.username, - displayName: author.profile.name, - email: authorEmail, - uuid: author.id, - }); + const text = `@${author} ${authorEmail} (${uuid}, ${language}) tried to post a slur in their Profile.`; flagSlack .send({ @@ -359,10 +346,10 @@ function sendSlurNotification ({ attachments: [{ fallback: 'Slur Message', color: 'danger', - author_name: authorName, + author_email: authorEmail, title, title_link: titleLink, - text: message, + text: problemContent, mrkdwn_in: [ 'text', ], @@ -378,6 +365,6 @@ export { sendProfileFlagNotification, sendSubscriptionNotification, sendShadowMutedPostNotification, - sendSlurNotification, + sendProfileSlurNotification, formatUser, }; diff --git a/website/server/libs/user/index.js b/website/server/libs/user/index.js index 118e0586d9..8abbfd429e 100644 --- a/website/server/libs/user/index.js +++ b/website/server/libs/user/index.js @@ -1,4 +1,6 @@ import _ from 'lodash'; +import * as slack from '../slack'; +import { getUserInfo } from '../email'; import common from '../../../common'; import * as Tasks from '../../models/task'; import { model as Groups } from '../../models/group'; @@ -105,19 +107,6 @@ function checkPreferencePurchase (user, path, item) { return _.get(user.purchased, itemPath); } -async function checkNewInputForProfanity (user, res, newValue) { - const containsSlur = stringContainsProfanity(newValue, 'slur'); - const containsBannedWord = stringContainsProfanity(newValue); - if (containsSlur || containsBannedWord) { - if (containsSlur) { - user.flags.chatRevoked = true; - await user.save(); - throw new BadRequest(res.t('bannedSlurUsedInProfile')); - } - throw new BadRequest(res.t('bannedWordUsedInProfile')); - } -} - export async function update (req, res, { isV3 = false }) { const { user } = res.locals; @@ -134,17 +123,40 @@ export async function update (req, res, { isV3 = false }) { }); } + let slurWasUsed = false; + let problemContent = ''; + if (req.body['profile.name'] !== undefined) { const newName = req.body['profile.name']; if (newName === null) throw new BadRequest(res.t('invalidReqParams')); if (newName.length > 30) throw new BadRequest(res.t('displaynameIssueLength')); if (nameContainsNewline(newName)) throw new BadRequest(res.t('displaynameIssueNewline')); - await checkNewInputForProfanity(user, res, newName); + if (stringContainsProfanity(newName, 'slur')) { + slurWasUsed = true; + problemContent += `Profile Name: ${newName}\n\n`; + } } if (req.body['profile.blurb'] !== undefined) { const newBlurb = req.body['profile.blurb']; - await checkNewInputForProfanity(user, res, newBlurb); + if (stringContainsProfanity(newBlurb, 'slur')) { + slurWasUsed = true; + problemContent += `Profile Blurb: ${newBlurb}`; + } + } + + if (slurWasUsed) { + const authorEmail = getUserInfo(user, ['email']).email; + user.flags.chatRevoked = true; + await user.save(); + slack.sendProfileSlurNotification({ + authorEmail, + author: user.auth.local.username, + uuid: user.id, + language: user.preferences.language, + problemContent, + }); + throw new BadRequest(res.t('bannedSlurUsedInProfile')); } let groupsToMirror; diff --git a/website/server/libs/user/validation.js b/website/server/libs/user/validation.js index 3c3defb088..82f66b6134 100644 --- a/website/server/libs/user/validation.js +++ b/website/server/libs/user/validation.js @@ -30,8 +30,8 @@ export function nameContainsNewline (username) { } function usernameIsForbidden (username) { - const forbidddenWordsMatched = getMatchesByWordArray(username, forbiddenUsernames); - return forbidddenWordsMatched.length > 0; + const forbiddenWordsMatched = getMatchesByWordArray(username, forbiddenUsernames); + return forbiddenWordsMatched.length > 0; } const invalidCharsRegex = new RegExp('[^a-z0-9_-]', 'i'); @@ -43,7 +43,6 @@ function usernameContainsInvalidCharacters (username) { export function verifyDisplayName (displayName, res) { const issues = []; if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displaynameIssueLength')); - if (stringContainsProfanity(displayName)) issues.push(res.t('bannedWordUsedInProfile')); if (stringContainsProfanity(displayName, 'slur')) issues.push(res.t('bannedSlurUsedInProfile')); if (nameContainsNewline(displayName)) issues.push(res.t('displaynameIssueNewline'));