diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index 73abe0728c..8334af8a07 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -13,7 +13,7 @@ import { SPAM_MIN_EXEMPT_CONTRIB_LEVEL, TAVERN_ID, } from '../../../../../website/server/models/group'; -import { CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants'; +import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants'; import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils'; import bannedWords from '../../../../../website/server/libs/bannedWords'; import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords'; @@ -494,6 +494,7 @@ describe('POST /chat', () => { 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789. THIS PART WON'T BE IN THE MESSAGE (over 3000) `; + expect(veryLongMessage.length > MAX_MESSAGE_LENGTH).to.equal(true); const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: veryLongMessage }); const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`); @@ -501,9 +502,27 @@ describe('POST /chat', () => { expect(newMessage.message.id).to.exist; expect(groupMessages[0].id).to.exist; - expect(newMessage.message.text.length).to.eql(3000); + expect(newMessage.message.text.length).to.eql(MAX_MESSAGE_LENGTH); expect(newMessage.message.text).to.not.contain('MESSAGE'); - expect(groupMessages[0].text.length).to.eql(3000); + expect(groupMessages[0].text.length).to.eql(MAX_MESSAGE_LENGTH); + }); + + it('chat message with mentions - mention link should not count towards 3000 chars limit', async () => { + const memberUsername = 'memberUsername'; + await member.update({ 'auth.local.username': memberUsername }); + + const messageWithMentions = `hi @${memberUsername} 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 89 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345678 END.`; + expect(messageWithMentions.length).to.equal(MAX_MESSAGE_LENGTH); + const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions }); + const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`); + + const mentionLink = `[@${memberUsername}](/profile/${member._id})`; + expect(newMessage.message.text).to.include(mentionLink); + expect(newMessage.message.text).to.include(' END.'); + expect(newMessage.message.text.length) + .to.eql(messageWithMentions.length - (`@${memberUsername}`).length + mentionLink.length); + expect(groupMessages[0].text.length).to.eql(newMessage.message.text.length); }); it('creates a chat with user styles', async () => { diff --git a/website/client/src/components/groups/chat.vue b/website/client/src/components/groups/chat.vue index 80d1a6a8f2..8990e5a39d 100644 --- a/website/client/src/components/groups/chat.vue +++ b/website/client/src/components/groups/chat.vue @@ -20,7 +20,7 @@ v-model="newMessage" :placeholder="placeholder" :class="{'user-entry': newMessage}" - maxlength="3000" + :maxlength="MAX_MESSAGE_LENGTH" @keydown="updateCarretPosition" @keyup.ctrl.enter="sendMessageShortcut()" @keydown.tab="handleTab($event)" @@ -30,7 +30,7 @@ @keydown.esc="handleEscape($event)" @paste="disableMessageSendShortcut()" > - {{ currentLength }} / 3000 + {{ currentLength }} / {{ MAX_MESSAGE_LENGTH }} @@ -518,6 +518,7 @@ import groupBy from 'lodash/groupBy'; import orderBy from 'lodash/orderBy'; import habiticaMarkdown from 'habitica-markdown'; import axios from 'axios'; +import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants'; import { mapState } from '@/libs/store'; import styleHelper from '@/mixins/styleHelper'; import toggleSwitch from '@/components/ui/toggleSwitch'; @@ -563,6 +564,7 @@ export default { messagesLoading: false, initiatedConversation: null, updateConversationsCounter: 0, + MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(), }; }, async mounted () { diff --git a/website/common/script/constants.js b/website/common/script/constants.js index 52c894f8ac..03c3d1e4f4 100644 --- a/website/common/script/constants.js +++ b/website/common/script/constants.js @@ -9,6 +9,7 @@ export const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = 5000; export const MAX_SUMMARY_SIZE_FOR_GUILDS = 250; export const MAX_SUMMARY_SIZE_FOR_CHALLENGES = 250; export const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = 3; +export const MAX_MESSAGE_LENGTH = 3000; 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 diff --git a/website/common/script/index.js b/website/common/script/index.js index dfdd5aab6e..5c12e7c987 100644 --- a/website/common/script/index.js +++ b/website/common/script/index.js @@ -18,6 +18,7 @@ import { MINIMUM_PASSWORD_LENGTH, SUPPORTED_SOCIAL_NETWORKS, TAVERN_ID, + MAX_MESSAGE_LENGTH, } from './constants'; import content from './content/index'; import * as count from './count'; @@ -108,6 +109,7 @@ api.constants = { CHAT_FLAG_FROM_MOD, CHAT_FLAG_FROM_SHADOW_MUTE, MINIMUM_PASSWORD_LENGTH, + MAX_MESSAGE_LENGTH, }; // TODO Move these under api.constants api.maxLevel = MAX_LEVEL; diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index 1a4466e0cb..60e5284594 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -2,7 +2,10 @@ import nconf from 'nconf'; import { authWithHeaders } from '../../middlewares/auth'; import { model as Group } from '../../models/group'; import { model as User } from '../../models/user'; -import { chatModel as Chat } from '../../models/message'; +import { + chatModel as Chat, + sanitizeText as sanitizeMessageText, +} from '../../models/message'; import common from '../../../common'; import { BadRequest, @@ -187,7 +190,8 @@ api.postChat = { throw new NotAuthorized(res.t('messageGroupChatSpam')); } - const [message, mentions, mentionedMembers] = await highlightMentions(req.body.message); + const sanitizedMessageText = sanitizeMessageText(req.body.message); + const [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText); let client = req.headers['x-client'] || '3rd Party'; if (client) { client = client.replace('habitica-', ''); diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index e7d3e0dfe9..bc2e5b3065 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -21,6 +21,9 @@ import { import { sendNotification as sendPushNotification } from '../../libs/pushNotifications'; import common from '../../../common'; import { sentMessage } from '../../libs/inbox'; +import { + sanitizeText as sanitizeMessageText, +} from '../../models/message'; import { highlightMentions } from '../../libs/highlightMentions'; const { achievements } = common; @@ -677,7 +680,8 @@ api.sendPrivateMessage = { if (validationErrors) throw validationErrors; const sender = res.locals.user; - const message = (await highlightMentions(req.body.message))[0]; + const sanitizedMessageText = sanitizeMessageText(req.body.message); + const message = (await highlightMentions(sanitizedMessageText))[0]; const receiver = await User.findById(req.body.toUserId).exec(); if (!receiver) throw new NotFound(res.t('userNotFound')); diff --git a/website/server/models/message.js b/website/server/models/message.js index d3a99dae1d..2a818f813b 100644 --- a/website/server/models/message.js +++ b/website/server/models/message.js @@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid'; import { defaults } from 'lodash'; import removeMd from 'remove-markdown'; import baseModel from '../libs/baseModel'; +import shared from '../../common'; const defaultSchema = () => ({ id: String, @@ -113,14 +114,20 @@ export function setUserStyles (newMessage, user) { } } +// Sanitize an input message, separate from messageDefaults because +// it must run before mentions are highlighted +export function sanitizeText (msg) { + // Trim messages longer than the MAX_MESSAGE_LENGTH + return msg.substring(0, shared.constants.MAX_MESSAGE_LENGTH); +} + export function messageDefaults (msg, user, client, flagCount = 0, info = {}) { const id = uuid(); - const trimmedMessage = msg.substring(0, 3000); const message = { id, _id: id, - text: trimmedMessage, - unformattedText: removeMd(trimmedMessage), + text: msg, + unformattedText: removeMd(msg), info, timestamp: Number(new Date()), likes: {}, diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 6a165f458c..2bf8227e3a 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -110,7 +110,7 @@ schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction /** - * Sends a message to a this. Archives a copy in sender's inbox. + * Sends a message to a user. Archives a copy in sender's inbox. * * @param userToReceiveMessage The receiver * @param options