From be2eb3df44a0e49fbedf7b2489fdfcf2abe99d82 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Wed, 8 Nov 2023 23:13:25 +0400 Subject: [PATCH] FIX: user got notified about a mention inside a chat message quote (#24229) When quoting a chat message in a post, if that message contains a mention, that mention should be ignored. But we've been detecting them and sending notifications to users. This PR fixes the problem. Since this fix is for the chat plugin, I had to introduce a new API for plugins: # We strip posts before detecting mentions, oneboxes, attachments etc. # We strip those elements that shouldn't be detected. For example, # a mention inside a quote should be ignored, so we strip it off. # Using this API plugins can register their own post strippers. def register_post_stripper(&block) end --- app/models/post_analyzer.rb | 8 ++--- app/models/post_stripper.rb | 28 ++++++++++++++++ lib/discourse_plugin_registry.rb | 2 ++ lib/plugin/instance.rb | 8 +++++ plugins/chat/plugin.rb | 4 +++ spec/models/post_stripper_spec.rb | 54 +++++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 app/models/post_stripper.rb create mode 100644 spec/models/post_stripper_spec.rb diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 2d2622c2070..a129f442d9e 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -146,11 +146,9 @@ class PostAnalyzer def cooked_stripped @cooked_stripped ||= begin - doc = Nokogiri::HTML5.fragment(cook(@raw, topic_id: @topic_id)) - doc.css( - "pre .mention, aside.quote > .title, aside.quote .mention, aside.quote .mention-group, .onebox, .elided", - ).remove - doc + cooked = cook(@raw, topic_id: @topic_id) + fragment = Nokogiri::HTML5.fragment(cooked) + PostStripper.strip(fragment) end end diff --git a/app/models/post_stripper.rb b/app/models/post_stripper.rb new file mode 100644 index 00000000000..9611aba4893 --- /dev/null +++ b/app/models/post_stripper.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# We strip posts before detecting mentions, oneboxes, attachments etc. +# We strip those elements that shouldn't be detected. For example, +# a mention inside a quote should be ignored, so we strip it off. +class PostStripper + def self.strip(nokogiri_fragment) + run_core_strippers(nokogiri_fragment) + run_plugin_strippers(nokogiri_fragment) + nokogiri_fragment + end + + private + + def self.run_core_strippers(nokogiri_fragment) + nokogiri_fragment.css( + "pre .mention, aside.quote > .title, aside.quote .mention, aside.quote .mention-group, .onebox, .elided", + ).remove + end + + def self.run_plugin_strippers(nokogiri_fragment) + DiscoursePluginRegistry.post_strippers.each do |stripper| + stripper[:block].call(nokogiri_fragment) + end + end + + private_class_method :run_core_strippers, :run_plugin_strippers +end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 6925e2ba720..c847d151f1a 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -122,6 +122,8 @@ class DiscoursePluginRegistry define_filtered_register :post_action_notify_user_handlers + define_filtered_register :post_strippers + def self.register_auth_provider(auth_provider) self.auth_providers << auth_provider end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index beb19cde414..324b555f990 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -1254,6 +1254,14 @@ class Plugin::Instance DiscoursePluginRegistry.register_post_action_notify_user_handler(handler, self) end + # We strip posts before detecting mentions, oneboxes, attachments etc. + # We strip those elements that shouldn't be detected. For example, + # a mention inside a quote should be ignored, so we strip it off. + # Using this API plugins can register their own post strippers. + def register_post_stripper(&block) + DiscoursePluginRegistry.register_post_stripper({ block: block }, self) + end + protected def self.js_path diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb index 16b7edeb410..cdb0aacccfe 100644 --- a/plugins/chat/plugin.rb +++ b/plugins/chat/plugin.rb @@ -493,6 +493,10 @@ after_initialize do register_hashtag_type_priority_for_context("tag", "chat-composer", 50) register_hashtag_type_priority_for_context("channel", "topic-composer", 10) + register_post_stripper do |nokogiri_fragment| + nokogiri_fragment.css(".chat-transcript .mention").remove + end + Site.markdown_additional_options["chat"] = { limited_pretty_text_features: Chat::Message::MARKDOWN_FEATURES, limited_pretty_text_markdown_rules: Chat::Message::MARKDOWN_IT_RULES, diff --git a/spec/models/post_stripper_spec.rb b/spec/models/post_stripper_spec.rb new file mode 100644 index 00000000000..c1da8925d83 --- /dev/null +++ b/spec/models/post_stripper_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe PostStripper do + it "strips mentions in quotes" do + mention = '@andrei' + cooked = "" + fragment = Nokogiri::HTML5.fragment(cooked) + + PostStripper.strip(fragment) + + expect(fragment.to_s).to_not include(mention) + end + + it "strips group mentions in quotes" do + group_mention = '@moderators' + cooked = "" + fragment = Nokogiri::HTML5.fragment(cooked) + + PostStripper.strip(fragment) + + expect(fragment.to_s).to_not include(group_mention) + end + + it "strips oneboxes" do + onebox = + '' + cooked = "

#{onebox}

" + fragment = Nokogiri::HTML5.fragment(cooked) + + PostStripper.strip(fragment) + + expect(fragment.to_s).to_not include(onebox) + end + + context "with plugins" do + after { DiscoursePluginRegistry.reset_register!(:post_strippers) } + + it "runs strippers registered by plugins" do + plugin_element = '
' + + block = Proc.new { |nokogiri_fragment| nokogiri_fragment.css(".plugin_class").remove } + plugin = OpenStruct.new(enabled?: true) + DiscoursePluginRegistry.register_post_stripper({ block: block }, plugin) + + fragment = Nokogiri::HTML5.fragment("

#{plugin_element}

") + + PostStripper.strip(fragment) + + expect(fragment.to_s).to_not include(plugin_element) + end + end +end