diff --git a/app/models/rule.rb b/app/models/rule.rb index 275c9ba..f8c5243 100644 --- a/app/models/rule.rb +++ b/app/models/rule.rb @@ -37,7 +37,8 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel WHEN value::json->>'filter' = 'mute' THEN 1 WHEN value::json->>'filter' = 'thread' THEN 2 WHEN value::json->>'filter' = 'watch' THEN 3 - WHEN value::json->>'filter' = 'follow' THEN 4 + WHEN value::json->>'filter' = 'tag_added' THEN 4 + WHEN value::json->>'filter' = 'follow' THEN 5 END ", ) @@ -47,7 +48,7 @@ class DiscourseChatIntegration::Rule < DiscourseChatIntegration::PluginModel validates :filter, inclusion: { - in: %w[thread watch follow mute], + in: %w[thread watch follow tag_added mute], message: "%{value} is not a valid filter", } diff --git a/app/services/manager.rb b/app/services/manager.rb index a0c4e0c..e8c370f 100644 --- a/app/services/manager.rb +++ b/app/services/manager.rb @@ -12,8 +12,14 @@ module DiscourseChatIntegration # Abort if the chat_user doesn't have permission to see the post return if !guardian.can_see?(post) - # Abort if the post is blank, or is non-regular (e.g. a "topic closed" notification) - return if post.blank? || post.post_type != Post.types[:regular] + # Abort if the post is blank + return if post.blank? + + # Abort if post is not either regular, or a 'tags_changed' small action + if (post.post_type != Post.types[:regular]) && + !(post.post_type == Post.types[:small_action] && post.action_code == "tags_changed") + return + end topic = post.topic return if topic.blank? @@ -46,6 +52,33 @@ module DiscourseChatIntegration end end + if post.action_code == "tags_changed" + # Post is a small_action post regarding tags changing for the topic. Check if any tags were _added_ + # and if so, corresponding rules with `filter: tag_added` + tags_added = post.custom_fields["tags_added"] + tags_added = [tags_added].compact if !tags_added.is_a?(Array) + return if tags_added.blank? + + tags_removed = post.custom_fields["tags_removed"] + tags_removed = [tags_removed].compact if !tags_removed.is_a?(Array) + + unchanged_tags = topic.tags.map(&:name) - tags_added - tags_removed + + matching_rules = + matching_rules.select do |rule| + # Only rules that match this post, are ones where the filter is "tag_added" + next false if rule.filter != "tag_added" + next true if rule.tags.blank? + + # Skip if the topic already has one of the tags in the rule, applied + next false if unchanged_tags.any? && (unchanged_tags & rule.tags).any? + + # We don't need to do any additional filtering here because topics are filtered + # by tag later + true + end + end + # If tagging is enabled, thow away rules that don't apply to this topic if SiteSetting.tagging_enabled topic_tags = topic.tags.present? ? topic.tags.pluck(:name) : [] @@ -59,7 +92,7 @@ module DiscourseChatIntegration # Sort by order of precedence t_prec = { "group_message" => 0, "group_mention" => 1, "normal" => 2 } # Group things win - f_prec = { "mute" => 0, "thread" => 1, "watch" => 2, "follow" => 3 } #(mute always wins; thread beats watch beats follow) + f_prec = { "mute" => 0, "thread" => 1, "watch" => 2, "follow" => 3, "tag_added" => 4 } #(mute always wins; thread beats watch beats follow) sort_func = proc { |a, b| [t_prec[a.type], f_prec[a.filter]] <=> [t_prec[b.type], f_prec[b.filter]] } matching_rules = matching_rules.sort(&sort_func) diff --git a/assets/javascripts/admin/models/rule.js b/assets/javascripts/admin/models/rule.js index 67a3649..c37dff6 100644 --- a/assets/javascripts/admin/models/rule.js +++ b/assets/javascripts/admin/models/rule.js @@ -46,6 +46,11 @@ export default class Rule extends RestModel { name: I18n.t("chat_integration.filter.follow"), icon: "circle", }, + { + id: "tag_added", + name: I18n.t("chat_integration.filter.tag_added"), + icon: "tag", + }, { id: "mute", name: I18n.t("chat_integration.filter.mute"), diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7c3583d..18abb5d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -36,6 +36,7 @@ en: mute: 'Mute' follow: 'First post only' watch: 'All posts and replies' + tag_added: 'Tag added to topic' thread: 'All posts with threaded replies' rule_table: filter: "Filter" diff --git a/spec/services/manager_spec.rb b/spec/services/manager_spec.rb index c749cd9..f0823bd 100644 --- a/spec/services/manager_spec.rb +++ b/spec/services/manager_spec.rb @@ -345,6 +345,69 @@ RSpec.describe DiscourseChatIntegration::Manager do expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) end + + describe "with create_small_action_post_for_tag_changes enabled" do + fab!(:admin) { Fabricate(:admin) } + fab!(:additional_tag) { Fabricate(:tag) } + + before { SiteSetting.create_post_for_category_and_tag_changes = true } + + def set_new_tags_and_return_small_action_post(tags) + PostRevisor.new(tagged_first_post).revise!(admin, tags: tags) + + tagged_topic.ordered_posts.last + end + + it "should notify when rule is set up for tag additions for a category with no tag filter" do + post = set_new_tags_and_return_small_action_post([tag.name, additional_tag.name]) + + DiscourseChatIntegration::Rule.create!( + channel: chan1, + filter: "tag_added", + category_id: category.id, + ) + + manager.trigger_notifications(post.id) + expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) + end + + it "notifies when topic has a tag added that matches the rule" do + post = set_new_tags_and_return_small_action_post([tag.name, additional_tag.name]) + + DiscourseChatIntegration::Rule.create!( + channel: chan1, + filter: "tag_added", + category_id: category.id, + tags: [additional_tag.name], + ) + + manager.trigger_notifications(post.id) + expect(provider.sent_to_channel_ids).to contain_exactly(chan1.id) + end + + it "doesn't notify when topic has an unchanged tag present in the rule, even if a new tag is added" do + post = set_new_tags_and_return_small_action_post([tag.name, additional_tag.name]) + + DiscourseChatIntegration::Rule.create!( + channel: chan1, + filter: "tag_added", + category_id: category.id, + tags: [tag.name], + ) + + manager.trigger_notifications(post.id) + expect(provider.sent_to_channel_ids).to contain_exactly + end + + it "doesn't notify for small action 'tags_changed' posts unless a matching rule exists" do + post = set_new_tags_and_return_small_action_post([additional_tag.name]) + + DiscourseChatIntegration::Rule.create!(channel: chan1, filter: "watch", category_id: nil) # Wildcard watch + + manager.trigger_notifications(post.id) + expect(provider.sent_to_channel_ids).to contain_exactly + end + end end end end