This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/plugins/chat/lib/message_mention_warnings.rb
Roman Rizzi b7c24e1d69
REFACTOR: Separate post-send warnings logic.
After #19666, we send notifications asynchronously. However, this has dramatically delayed the different post-send warnings we send.

This change moves all the post-send warnings logic from `Chat::ChatNotifier` into a separate service, allowing us to run this part synchronously, displaying the warnings almost instantly without doing all the heavy lifting for sending notifications. It also simplifies the notifier, making another refactor we planned simpler.

Finally, this removes the need to run jobs on the system specs that test the post-warning logic, hopefully making them faster.
2023-02-15 08:37:47 -03:00

178 lines
6.2 KiB
Ruby

# frozen_string_literal: true
# For various reasons, the sender may receive a warning when writing a mention:
#
# * The target user either cannot chat or cannot see the chat channel, in which case
# they are defined as `cannot_see`
# * The target user is not a member of the channel, in which case they are defined
# as `without_membership`
#
# For any users that fall under the `cannot_see` or `without_membership` umbrellas
# we send a MessageBus message to the UI and to inform the sender. The
# creating user can invite any `without_membership` users to the channel. Target
# users who are ignoring or muting the creating user _do not_ fall into this bucket.
class Chat::MessageMentionWarnings
def dispatch(chat_message)
direct_mentions = direct_mentions_from(chat_message)
group_mentions = group_mentions_from(chat_message)
if (direct_mentions.length + group_mentions.length) > SiteSetting.max_mentions_per_chat_message
return
end
warnings = { without_membership: [], cannot_see: [] }
append_direct_mention_warnings(warnings, chat_message, direct_mentions)
append_group_mention_warnings(warnings, chat_message, direct_mentions, group_mentions)
filter_users_ignoring_or_muting_creator(warnings, chat_message)
notify_creator_of_inaccessible_mentions(warnings, chat_message)
end
private
def not_participating_base_query(message)
User
.distinct
.real
.not_suspended
.joins(:user_option)
.where(user_options: { chat_enabled: true })
.where.not(id: message.user_id)
.includes(:user_chat_channel_memberships)
end
def normalized_mentions(raw_mentions)
raw_mentions.reduce([]) do |memo, mention|
%w[@here @all].include?(mention.downcase) ? memo : (memo << mention[1..-1].downcase)
end
end
### Direct mention warning methods
def direct_mentions_from(message)
normalized_mentions(Nokogiri::HTML5.fragment(message.cooked).css(".mention").map(&:text))
end
def direct_mentioned_users_not_participating(message, mentions)
not_participating_base_query(message)
.where.not(id: message.user_id)
.where(username_lower: mentions)
end
def append_direct_mention_warnings(warnings, message, mentions)
direct_mentioned_users_not_participating(message, mentions).each do |potential_participant|
guardian = Guardian.new(potential_participant)
if guardian.can_chat? && guardian.can_join_chat_channel?(message.chat_channel)
not_a_member =
potential_participant.user_chat_channel_memberships.none? do |m|
predicate = m.chat_channel_id == message.chat_channel_id
predicate = predicate && m.following == true if message.chat_channel.public_channel?
predicate
end
warnings[:without_membership] << potential_participant if not_a_member
else
warnings[:cannot_see] << potential_participant
end
end
end
### Group mention warning methods
def group_mentions_from(message)
normalized_mentions(Nokogiri::HTML5.fragment(message.cooked).css(".mention-group").map(&:text))
end
def group_members_not_participating(mentionable_groups, message, direct_mentions)
not_participating_base_query(message)
.where.not(id: message.user_id)
.where.not(username_lower: direct_mentions)
.joins(:group_users)
.where(group_users: { group_id: mentionable_groups.map(&:id) })
end
def append_group_mention_warnings(warnings, message, direct_mentions, group_mentions)
visible_groups = Group.where("LOWER(name) IN (?)", group_mentions).visible_groups(message.user)
return if visible_groups.empty?
mentionable_groups =
Group.mentionable(message.user, include_public: false).where(id: visible_groups.map(&:id))
mentions_disabled = visible_groups - mentionable_groups
too_many_members, mentionable =
mentionable_groups.partition do |group|
group.user_count > SiteSetting.max_users_notified_per_group_mention
end
warnings[:group_mentions_disabled] = mentions_disabled
warnings[:too_many_members] = too_many_members
group_members_not_participating(
mentionable,
message,
direct_mentions,
).each do |potential_participant|
guardian = Guardian.new(potential_participant)
if guardian.can_chat? && guardian.can_join_chat_channel?(message.chat_channel)
not_a_member =
potential_participant.user_chat_channel_memberships.none? do |m|
predicate = m.chat_channel_id == message.chat_channel_id
predicate = predicate && m.following == true if message.chat_channel.public_channel?
predicate
end
warnings[:without_membership] << potential_participant if not_a_member
else
warnings[:cannot_see] << potential_participant
end
end
end
### Apply ignore/mute filters
def filter_users_ignoring_or_muting_creator(warnings, message)
screen_targets = warnings[:without_membership].map(&:id)
return if screen_targets.blank?
screener = UserCommScreener.new(acting_user: message.user, target_user_ids: screen_targets)
warnings[:without_membership].reject! { |user| screener.ignoring_or_muting_actor?(user.id) }
end
### Notify client
def notify_creator_of_inaccessible_mentions(warnings, message)
return if warnings.values.all?(&:blank?)
warnings_payload = [
inaccessible_mention_payload(warnings, :cannot_see) { |user| user.username },
inaccessible_mention_payload(warnings, :without_membership, include_ids: true) do |user|
user.username
end,
inaccessible_mention_payload(warnings, :too_many_members) { |group| group.name },
inaccessible_mention_payload(warnings, :group_mentions_disabled) { |group| group.name },
].compact
ChatPublisher.publish_inaccessible_mentions(message.user_id, message, warnings_payload)
end
def inaccessible_mention_payload(warnings, type, include_ids: false)
return if warnings[type].blank?
payload = { type: type, mentions: [] }
payload[:mention_target_ids] = [] if include_ids
warnings[type].reduce(payload) do |memo, target|
memo[:mentions] << yield(target)
memo[:mention_target_ids] << target.id if include_ids
memo
end
end
end