mirror of
https://github.com/discourse/discourse.git
synced 2025-06-04 08:44:48 +08:00
FIX: Do not consider code-blocks when parsing mentions (#23280)
We have the max_mentions_per_chat_message site settings; when a user tries to mention more users than allowed, no one gets mentioned. Chat messages may contain code-blocks with strings that look like mentions: def foo @bar + @baz end The problem is that the parsing code considers these as real mentions and counts them when checking the limit. This commit fixes the problem.
This commit is contained in:

committed by
GitHub

parent
f165c99d77
commit
73781c8a96
@ -4,7 +4,7 @@ import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
||||
import Bookmark from "discourse/models/bookmark";
|
||||
import I18n from "I18n";
|
||||
import { generateCookFunction } from "discourse/lib/text";
|
||||
import { generateCookFunction, parseMentions } from "discourse/lib/text";
|
||||
import transformAutolinks from "discourse/plugins/chat/discourse/lib/transform-auto-links";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
@ -170,33 +170,11 @@ export default class ChatMessage {
|
||||
}
|
||||
|
||||
async cook() {
|
||||
const site = getOwner(this).lookup("service:site");
|
||||
|
||||
if (this.isDestroyed || this.isDestroying) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownOptions = {
|
||||
featuresOverride:
|
||||
site.markdown_additional_options?.chat?.limited_pretty_text_features,
|
||||
markdownItRules:
|
||||
site.markdown_additional_options?.chat
|
||||
?.limited_pretty_text_markdown_rules,
|
||||
hashtagTypesInPriorityOrder:
|
||||
site.hashtag_configurations?.["chat-composer"],
|
||||
hashtagIcons: site.hashtag_icons,
|
||||
};
|
||||
|
||||
if (ChatMessage.cookFunction) {
|
||||
this.cooked = ChatMessage.cookFunction(this.message);
|
||||
} else {
|
||||
const cookFunction = await generateCookFunction(markdownOptions);
|
||||
ChatMessage.cookFunction = (raw) => {
|
||||
return transformAutolinks(cookFunction(raw));
|
||||
};
|
||||
|
||||
this.cooked = ChatMessage.cookFunction(this.message);
|
||||
}
|
||||
await this.#ensureCookFunctionInitialized();
|
||||
this.cooked = ChatMessage.cookFunction(this.message);
|
||||
}
|
||||
|
||||
get read() {
|
||||
@ -263,6 +241,10 @@ export default class ChatMessage {
|
||||
this.version++;
|
||||
}
|
||||
|
||||
async parseMentions() {
|
||||
return await parseMentions(this.message, this.#markdownOptions);
|
||||
}
|
||||
|
||||
toJSONDraft() {
|
||||
if (
|
||||
this.message?.length === 0 &&
|
||||
@ -361,6 +343,31 @@ export default class ChatMessage {
|
||||
}
|
||||
}
|
||||
|
||||
async #ensureCookFunctionInitialized() {
|
||||
if (ChatMessage.cookFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cookFunction = await generateCookFunction(this.#markdownOptions);
|
||||
ChatMessage.cookFunction = (raw) => {
|
||||
return transformAutolinks(cookFunction(raw));
|
||||
};
|
||||
}
|
||||
|
||||
get #markdownOptions() {
|
||||
const site = getOwner(this).lookup("service:site");
|
||||
return {
|
||||
featuresOverride:
|
||||
site.markdown_additional_options?.chat?.limited_pretty_text_features,
|
||||
markdownItRules:
|
||||
site.markdown_additional_options?.chat
|
||||
?.limited_pretty_text_markdown_rules,
|
||||
hashtagTypesInPriorityOrder:
|
||||
site.hashtag_configurations?.["chat-composer"],
|
||||
hashtagIcons: site.hashtag_icons,
|
||||
};
|
||||
}
|
||||
|
||||
#initChatMessageReactionModel(reactions = []) {
|
||||
return reactions.map((reaction) => ChatMessageReaction.create(reaction));
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import Service, { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { mentionRegex } from "pretty-text/mentions";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
@ -34,11 +33,7 @@ export default class ChatComposerWarningsTracker extends Service {
|
||||
|
||||
@bind
|
||||
reset() {
|
||||
this.unreachableGroupMentions = [];
|
||||
this.unreachableGroupMentions = [];
|
||||
this.overMembersLimitGroupMentions = [];
|
||||
this.tooManyMentions = false;
|
||||
this.channelWideMentionDisallowed = false;
|
||||
this.#resetMentionStats();
|
||||
this.mentionsCount = 0;
|
||||
cancel(this.mentionsTimer);
|
||||
}
|
||||
@ -63,61 +58,42 @@ export default class ChatComposerWarningsTracker extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
const mentions = this._extractMentions(currentMessage.message);
|
||||
this.mentionsCount = mentions?.length;
|
||||
currentMessage.parseMentions().then((mentions) => {
|
||||
this.mentionsCount = mentions?.length;
|
||||
|
||||
if (this.mentionsCount > 0) {
|
||||
this.tooManyMentions =
|
||||
this.mentionsCount > this.siteSettings.max_mentions_per_chat_message;
|
||||
if (this.mentionsCount > 0) {
|
||||
this.tooManyMentions =
|
||||
this.mentionsCount > this.siteSettings.max_mentions_per_chat_message;
|
||||
|
||||
if (!this.tooManyMentions) {
|
||||
const newMentions = mentions.filter(
|
||||
(mention) => !(mention in this._mentionWarningsSeen)
|
||||
);
|
||||
if (!this.tooManyMentions) {
|
||||
const newMentions = mentions.filter(
|
||||
(mention) => !(mention in this._mentionWarningsSeen)
|
||||
);
|
||||
|
||||
this.channelWideMentionDisallowed =
|
||||
!currentMessage.channel.allowChannelWideMentions &&
|
||||
(mentions.includes("here") || mentions.includes("all"));
|
||||
this.channelWideMentionDisallowed =
|
||||
!currentMessage.channel.allowChannelWideMentions &&
|
||||
(mentions.includes("here") || mentions.includes("all"));
|
||||
|
||||
if (newMentions?.length > 0) {
|
||||
this._recordNewWarnings(newMentions, mentions);
|
||||
} else {
|
||||
this._rebuildWarnings(mentions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.tooManyMentions = false;
|
||||
this.channelWideMentionDisallowed = false;
|
||||
this.unreachableGroupMentions = [];
|
||||
this.overMembersLimitGroupMentions = [];
|
||||
}
|
||||
}
|
||||
|
||||
_extractMentions(message) {
|
||||
const regex = mentionRegex(this.siteSettings.unicode_usernames);
|
||||
const mentions = [];
|
||||
let mentionsLeft = true;
|
||||
|
||||
while (mentionsLeft) {
|
||||
const matches = message.match(regex);
|
||||
|
||||
if (matches) {
|
||||
const mention = matches[1] || matches[2];
|
||||
mentions.push(mention);
|
||||
message = message.replaceAll(`${mention}`, "");
|
||||
|
||||
if (mentions.length > this.siteSettings.max_mentions_per_chat_message) {
|
||||
mentionsLeft = false;
|
||||
if (newMentions?.length > 0) {
|
||||
this.#recordNewWarnings(newMentions, mentions);
|
||||
} else {
|
||||
this.#rebuildWarnings(mentions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mentionsLeft = false;
|
||||
this.#resetMentionStats();
|
||||
}
|
||||
}
|
||||
|
||||
return mentions;
|
||||
});
|
||||
}
|
||||
|
||||
_recordNewWarnings(newMentions, mentions) {
|
||||
#resetMentionStats() {
|
||||
this.tooManyMentions = false;
|
||||
this.channelWideMentionDisallowed = false;
|
||||
this.unreachableGroupMentions = [];
|
||||
this.overMembersLimitGroupMentions = [];
|
||||
}
|
||||
|
||||
#recordNewWarnings(newMentions, mentions) {
|
||||
ajax("/chat/api/mentions/groups.json", {
|
||||
data: { mentions: newMentions },
|
||||
})
|
||||
@ -135,12 +111,12 @@ export default class ChatComposerWarningsTracker extends Service {
|
||||
this._mentionWarningsSeen[warning] = MENTION_RESULT["invalid"];
|
||||
});
|
||||
|
||||
this._rebuildWarnings(mentions);
|
||||
this.#rebuildWarnings(mentions);
|
||||
})
|
||||
.catch(this._rebuildWarnings(mentions));
|
||||
.catch(this.#rebuildWarnings(mentions));
|
||||
}
|
||||
|
||||
_rebuildWarnings(mentions) {
|
||||
#rebuildWarnings(mentions) {
|
||||
const newWarnings = mentions.reduce(
|
||||
(memo, mention) => {
|
||||
if (
|
||||
|
106
plugins/chat/test/javascripts/acceptance/mentions-test.js
Normal file
106
plugins/chat/test/javascripts/acceptance/mentions-test.js
Normal file
@ -0,0 +1,106 @@
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import { fillIn, visit } from "@ember/test-helpers";
|
||||
import pretender, { response } from "discourse/tests/helpers/create-pretender";
|
||||
|
||||
acceptance("Chat | Mentions", function (needs) {
|
||||
const channelId = 1;
|
||||
const actingUser = {
|
||||
id: 1,
|
||||
username: "acting_user",
|
||||
};
|
||||
const channel = {
|
||||
id: channelId,
|
||||
chatable_id: 1,
|
||||
chatable_type: "Category",
|
||||
meta: { message_bus_last_ids: {}, can_delete_self: true },
|
||||
current_user_membership: { following: true },
|
||||
allow_channel_wide_mentions: false,
|
||||
chatable: { id: 1 },
|
||||
};
|
||||
|
||||
needs.settings({ chat_enabled: true });
|
||||
|
||||
needs.user({
|
||||
...actingUser,
|
||||
has_chat_enabled: true,
|
||||
chat_channels: {
|
||||
public_channels: [channel],
|
||||
direct_message_channels: [],
|
||||
meta: { message_bus_last_ids: {} },
|
||||
tracking: {},
|
||||
},
|
||||
});
|
||||
|
||||
needs.hooks.beforeEach(function () {
|
||||
pretender.post(`/chat/drafts`, () => response({}));
|
||||
pretender.get(`/chat/api/channels/${channelId}/messages`, () =>
|
||||
response({
|
||||
messages: [],
|
||||
meta: {
|
||||
can_load_more_future: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
pretender.get("/chat/api/mentions/groups.json", () =>
|
||||
response({
|
||||
unreachable: [],
|
||||
over_members_limit: [],
|
||||
invalid: ["and"],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("shows warning when mention limit exceeded", async function (assert) {
|
||||
this.siteSettings.max_mentions_per_chat_message = 2;
|
||||
|
||||
await visit(`/chat/c/-/${channelId}`);
|
||||
await fillIn(".chat-composer__input", `Hey @user1 @user2 @user3`);
|
||||
|
||||
assert.dom(".chat-mention-warnings").exists();
|
||||
});
|
||||
|
||||
test("shows warning for @here mentions when channel-wide mentions are disabled", async function (assert) {
|
||||
await visit(`/chat/c/-/${channelId}`);
|
||||
await fillIn(".chat-composer__input", `Hey @here`);
|
||||
|
||||
assert.dom(".chat-mention-warnings").exists();
|
||||
});
|
||||
|
||||
test("shows warning for @all mention when channel-wide mentions are disabled", async function (assert) {
|
||||
await visit(`/chat/c/-/${channelId}`);
|
||||
await fillIn(".chat-composer__input", `Hey @all`);
|
||||
|
||||
assert.dom(".chat-mention-warnings").exists();
|
||||
});
|
||||
|
||||
test("ignores duplicates when counting mentions", async function (assert) {
|
||||
this.siteSettings.max_mentions_per_chat_message = 2;
|
||||
|
||||
await visit(`/chat/c/-/${channelId}`);
|
||||
const mention = `@user1`;
|
||||
await fillIn(
|
||||
".chat-composer__input",
|
||||
`Hey ${mention} ${mention} ${mention}`
|
||||
);
|
||||
|
||||
assert.dom(".chat-mention-warnings").doesNotExist();
|
||||
});
|
||||
|
||||
test("doesn't consider code-blocks when counting mentions", async function (assert) {
|
||||
this.siteSettings.max_mentions_per_chat_message = 2;
|
||||
|
||||
await visit(`/chat/c/-/${channelId}`);
|
||||
// since @bar is inside a code-block it shouldn't be considered a mention
|
||||
const message = `Hey @user1 @user2
|
||||
\`\`\`
|
||||
def foo
|
||||
@bar = true
|
||||
end
|
||||
\`\`\`
|
||||
`;
|
||||
await fillIn(".chat-composer__input", message);
|
||||
|
||||
assert.dom(".chat-mention-warnings").doesNotExist();
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user