# frozen_string_literal: true module DiscourseChatIntegration module Manager def self.guardian Guardian.new(User.find_by(username: SiteSetting.chat_integration_discourse_username)) end def self.trigger_notifications(post_id) post = Post.find_by(id: post_id) # 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 return if post.blank? # Abort if post is not either regular, or a 'tags_changed'/'category_changed' small action if (post.post_type != Post.types[:regular]) && !( post.post_type == Post.types[:small_action] && %w[tags_changed category_changed].include?(post.action_code) ) return end topic = post.topic return if topic.blank? # If it's a private message, filter rules by groups, otherwise filter rules by category if topic.archetype == Archetype.private_message group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id) return if group_ids_with_access.empty? matching_rules = DiscourseChatIntegration::Rule.with_type("group_message").with_group_ids( group_ids_with_access, ) else matching_rules = DiscourseChatIntegration::Rule.with_type("normal").with_category_id(topic.category_id) if topic.category # Also load the rules for the wildcard category matching_rules += DiscourseChatIntegration::Rule.with_type("normal").with_category_id(nil) end # If groups are mentioned, check for any matching rules and append them mentions = post.raw_mentions if mentions && mentions.length > 0 groups = Group.where("LOWER(name) IN (?)", mentions) if groups.exists? matching_rules += DiscourseChatIntegration::Rule.with_type("group_mention").with_group_ids( groups.map(&:id), ) end 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 else matching_rules = matching_rules.select { |rule| rule.filter != "tag_added" } 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) : [] matching_rules = matching_rules.select do |rule| next true if rule.tags.nil? || rule.tags.empty? # Filter has no tags specified any_tags_match = !((rule.tags & topic_tags).empty?) next any_tags_match # If any tags match, keep this filter, otherwise throw away end end # 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, "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) # Take the first rule for each channel uniq_func = proc { |rule| [rule.channel_id] } matching_rules = matching_rules.uniq(&uniq_func) # If a matching rule is set to mute, we can discard it now matching_rules = matching_rules.select { |rule| rule.filter != "mute" } # If this is not the first post, discard all "follow" rules. Unless it's a # category_changed action post. If category changed, filter out and rules # that aren't specific to a category if !post.is_first_post? matching_rules = if post.action_code == "category_changed" matching_rules.select { |rule| rule.category_id.present? } else matching_rules.select { |rule| rule.filter != "follow" } end end # All remaining rules now require a notification to be sent # If there are none left, abort return false if matching_rules.empty? # Loop through each rule, and trigger appropriate notifications matching_rules.each do |rule| # If there are any issues, skip to the next rule next unless channel = rule.channel next unless provider = ::DiscourseChatIntegration::Provider.get_by_name(channel.provider) next unless is_enabled = ::DiscourseChatIntegration::Provider.is_enabled(provider) begin provider.trigger_notification(post, channel, rule) channel.update_attribute("error_key", nil) if channel.error_key rescue => e if e.class == (DiscourseChatIntegration::ProviderError) && e.info.key?(:error_key) && !e.info[:error_key].nil? channel.update_attribute("error_key", e.info[:error_key]) else channel.update_attribute("error_key", "chat_integration.channel_exception") end channel.update_attribute("error_info", JSON.pretty_generate(e.try(:info))) # Log the error # Discourse.handle_job_exception(e, # message: "Triggering notifications failed", # extra: { provider_name: provider::PROVIDER_NAME, # channel: rule.channel, # post_id: post.id, # error_info: e.class == DiscourseChatIntegration::ProviderError ? e.info : nil } # ) end end end end end