# frozen_string_literal: true require "rails_helper" describe Chat::ChatNotifier do describe "#notify_new" do fab!(:channel) { Fabricate(:category_channel) } fab!(:user_1) { Fabricate(:user) } fab!(:user_2) { Fabricate(:user) } before do @chat_group = Fabricate( :group, users: [user_1, user_2], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) SiteSetting.chat_allowed_groups = @chat_group.id [user_1, user_2].each do |u| Fabricate(:user_chat_channel_membership, chat_channel: channel, user: u) end end def build_cooked_msg(message_body, user, chat_channel: channel) ChatMessage.new( chat_channel: chat_channel, user: user, message: message_body, created_at: 5.minutes.ago, ).tap(&:cook) end shared_examples "channel-wide mentions" do it "returns an empty list when the message doesn't include a channel mention" do msg = build_cooked_msg(mention.gsub("@", ""), user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "will never include someone who is not accepting channel-wide notifications" do user_2.user_option.update!(ignore_channel_wide_mention: true) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "will never mention when channel is not accepting channel wide mentions" do channel.update!(allow_channel_wide_mentions: false) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "includes all members of a channel except the sender" do msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to contain_exactly(user_2.id) end end shared_examples "ensure only channel members are notified" do it "will never include someone outside the channel" do user3 = Fabricate(:user) @chat_group.add(user3) another_channel = Fabricate(:category_channel) Fabricate(:user_chat_channel_membership, chat_channel: another_channel, user: user3) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to contain_exactly(user_2.id) end it "will never include someone not following the channel anymore" do user3 = Fabricate(:user) @chat_group.add(user3) Fabricate( :user_chat_channel_membership, following: false, chat_channel: channel, user: user3, ) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to contain_exactly(user_2.id) end it "will never include someone who is suspended" do user3 = Fabricate(:user, suspended_till: 2.years.from_now) @chat_group.add(user3) Fabricate( :user_chat_channel_membership, following: true, chat_channel: channel, user: user3, ) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to contain_exactly(user_2.id) end end describe "global_mentions" do let(:mention) { "hello @all!" } let(:list_key) { :global_mentions } include_examples "channel-wide mentions" include_examples "ensure only channel members are notified" describe "users ignoring or muting the user creating the message" do it "does not send notifications to the user who is muting the acting user" do Fabricate(:muted_user, user: user_2, muted_user: user_1) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "does not send notifications to the user who is ignoring the acting user" do Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end end end describe "here_mentions" do let(:mention) { "hello @here!" } let(:list_key) { :here_mentions } before { user_2.update!(last_seen_at: 4.minutes.ago) } include_examples "channel-wide mentions" include_examples "ensure only channel members are notified" it "includes users seen less than 5 minutes ago" do msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to contain_exactly(user_2.id) end it "excludes users seen more than 5 minutes ago" do user_2.update!(last_seen_at: 6.minutes.ago) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "excludes users mentioned directly" do msg = build_cooked_msg("hello @here @#{user_2.username}!", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end describe "users ignoring or muting the user creating the message" do it "does not send notifications to the user who is muting the acting user" do Fabricate(:muted_user, user: user_2, muted_user: user_1) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[list_key]).to be_empty end it "does not send notifications to the user who is ignoring the acting user" do Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now) msg = build_cooked_msg(mention, user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end end end describe "direct_mentions" do it "only include mentioned users who are already in the channel" do user_3 = Fabricate(:user) @chat_group.add(user_3) another_channel = Fabricate(:category_channel) Fabricate(:user_chat_channel_membership, chat_channel: another_channel, user: user_3) msg = build_cooked_msg("Is @#{user_3.username} here? And @#{user_2.username}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id) end it "include users as direct mentions even if there's a @here mention" do msg = build_cooked_msg("Hello @here and @#{user_2.username}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:here_mentions]).to be_empty expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id) end it "include users as direct mentions even if there's a @all mention" do msg = build_cooked_msg("Hello @all and @#{user_2.username}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:global_mentions]).to be_empty expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id) end describe "users ignoring or muting the user creating the message" do it "does not publish new mentions to these users" do Fabricate(:muted_user, user: user_2, muted_user: user_1) msg = build_cooked_msg("hey @#{user_2.username} stop muting me!", user_1) ChatPublisher.expects(:publish_new_mention).never to_notify = described_class.new(msg, msg.created_at).notify_new end it "does not send notifications to the user who is muting the acting user" do Fabricate(:muted_user, user: user_2, muted_user: user_1) msg = build_cooked_msg("hey @#{user_2.username} stop muting me!", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end it "does not send notifications to the user who is ignoring the acting user" do Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now) msg = build_cooked_msg("hey @#{user_2.username} stop ignoring me!", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end end end describe "group mentions" do fab!(:user_3) { Fabricate(:user) } fab!(:group) do Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) end fab!(:other_channel) { Fabricate(:category_channel) } before { @chat_group.add(user_3) } let(:mention) { "hello @#{group.name}!" } let(:list_key) { group.name } include_examples "ensure only channel members are notified" it 'calls guardian can_join_chat_channel?' do Guardian.any_instance.expects(:can_join_chat_channel?).at_least_once msg = build_cooked_msg("Hello @#{group.name} and @#{user_2.username}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new end it "establishes a far-left precedence among group mentions" do Fabricate( :user_chat_channel_membership, chat_channel: channel, user: user_3, following: true, ) msg = build_cooked_msg("Hello @#{@chat_group.name} and @#{group.name}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[@chat_group.name]).to contain_exactly(user_2.id, user_3.id) expect(to_notify[list_key]).to be_empty second_msg = build_cooked_msg("Hello @#{group.name} and @#{@chat_group.name}", user_1) to_notify_2 = described_class.new(second_msg, second_msg.created_at).notify_new expect(to_notify_2[list_key]).to contain_exactly(user_2.id, user_3.id) expect(to_notify_2[@chat_group.name]).to be_empty end it "skips groups with too many members" do SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1) msg = build_cooked_msg("Hello @#{group.name}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[group.name]).to be_nil end it "respects the 'max_mentions_per_chat_message' setting and skips notifications" do SiteSetting.max_mentions_per_chat_message = 1 msg = build_cooked_msg("Hello @#{user_2.username} and @#{user_3.username}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty expect(to_notify[group.name]).to be_nil end it "respects the max mentions setting and skips notifications when mixing users and groups" do SiteSetting.max_mentions_per_chat_message = 1 msg = build_cooked_msg("Hello @#{user_2.username} and @#{group.name}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty expect(to_notify[group.name]).to be_nil end describe "users ignoring or muting the user creating the message" do it "does not send notifications to the user inside the group who is muting the acting user" do group.add(user_3) Fabricate(:user_chat_channel_membership, chat_channel: channel, user: user_3) Fabricate(:muted_user, user: user_2, muted_user: user_1) msg = build_cooked_msg("Hello @#{group.name}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty expect(to_notify[group.name]).to contain_exactly(user_3.id) end it "does not send notifications to the user inside the group who is ignoring the acting user" do group.add(user_3) Fabricate(:user_chat_channel_membership, chat_channel: channel, user: user_3) Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now) msg = build_cooked_msg("Hello @#{group.name}", user_1) to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty expect(to_notify[group.name]).to contain_exactly(user_3.id) end end end describe "unreachable users" do fab!(:user_3) { Fabricate(:user) } it "notify poster of users who are not allowed to use chat" do msg = build_cooked_msg("Hello @#{user_3.username}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end unreachable_msg = messages.first expect(unreachable_msg).to be_present expect(unreachable_msg.data[:without_membership]).to be_empty unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } expect(unreachable_users).to contain_exactly(user_3.id) end context "when in a personal message" do let(:personal_chat_channel) do Group.refresh_automatic_groups! Chat::DirectMessageChannelCreator.create!( acting_user: user_1, target_users: [user_1, user_2], ) end before { @chat_group.add(user_3) } it "notify posts of users who are not participating in a personal message" do msg = build_cooked_msg( "Hello @#{user_3.username}", user_1, chat_channel: personal_chat_channel, ) messages = MessageBus.track_publish("/chat/#{personal_chat_channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end unreachable_msg = messages.first expect(unreachable_msg).to be_present expect(unreachable_msg.data[:without_membership]).to be_empty unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } expect(unreachable_users).to contain_exactly(user_3.id) end it "notify posts of users who are part of the mentioned group but participating" do group = Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) msg = build_cooked_msg("Hello @#{group.name}", user_1, chat_channel: personal_chat_channel) messages = MessageBus.track_publish("/chat/#{personal_chat_channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[group.name]).to contain_exactly(user_2.id) end unreachable_msg = messages.first expect(unreachable_msg).to be_present expect(unreachable_msg.data[:without_membership]).to be_empty unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } expect(unreachable_users).to contain_exactly(user_3.id) end end end describe "users who can be invited to join the channel" do fab!(:user_3) { Fabricate(:user) } before { @chat_group.add(user_3) } it "can invite chat user without channel membership" do msg = build_cooked_msg("Hello @#{user_3.username}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end not_participating_msg = messages.first expect(not_participating_msg).to be_present expect(not_participating_msg.data[:cannot_see]).to be_empty not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_users).to contain_exactly(user_3.id) end it "cannot invite chat user without channel membership if they are ignoring the user who created the message" do Fabricate(:ignored_user, user: user_3, ignored_user: user_1) msg = build_cooked_msg("Hello @#{user_3.username}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end expect(messages).to be_empty end it "cannot invite chat user without channel membership if they are muting the user who created the message" do Fabricate(:muted_user, user: user_3, muted_user: user_1) msg = build_cooked_msg("Hello @#{user_3.username}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end expect(messages).to be_empty end it "can invite chat user who no longer follows the channel" do Fabricate( :user_chat_channel_membership, chat_channel: channel, user: user_3, following: false, ) msg = build_cooked_msg("Hello @#{user_3.username}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end not_participating_msg = messages.first expect(not_participating_msg).to be_present expect(not_participating_msg.data[:cannot_see]).to be_empty not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_users).to contain_exactly(user_3.id) end it "can invite other group members to channel" do group = Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) msg = build_cooked_msg("Hello @#{group.name}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end not_participating_msg = messages.first expect(not_participating_msg).to be_present expect(not_participating_msg.data[:cannot_see]).to be_empty not_participating_users = not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_users).to contain_exactly(user_3.id) end it "cannot invite a member of a group who is ignoring the user who created the message" do group = Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) Fabricate(:ignored_user, user: user_3, ignored_user: user_1, expiring_at: 1.day.from_now) msg = build_cooked_msg("Hello @#{group.name}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end expect(messages).to be_empty end it "cannot invite a member of a group who is muting the user who created the message" do group = Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) Fabricate(:muted_user, user: user_3, muted_user: user_1) msg = build_cooked_msg("Hello @#{group.name}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[:direct_mentions]).to be_empty end expect(messages).to be_empty end end describe "enforcing limits when mentioning groups" do fab!(:user_3) { Fabricate(:user) } fab!(:group) do Fabricate( :public_group, users: [user_2, user_3], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) end it "sends a message to the client signaling the group has too many members" do SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1) msg = build_cooked_msg("Hello @#{group.name}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[group.name]).to be_nil end too_many_members_msg = messages.first expect(too_many_members_msg).to be_present too_many_members = too_many_members_msg.data[:groups_with_too_many_members] expect(too_many_members).to contain_exactly(group.name) end it "sends a message to the client signaling the group doesn't allow mentions" do group.update!(mentionable_level: Group::ALIAS_LEVELS[:only_admins]) msg = build_cooked_msg("Hello @#{group.name}", user_1) messages = MessageBus.track_publish("/chat/#{channel.id}") do to_notify = described_class.new(msg, msg.created_at).notify_new expect(to_notify[group.name]).to be_nil end mentions_disabled_msg = messages.first expect(mentions_disabled_msg).to be_present mentions_disabled = mentions_disabled_msg.data[:group_mentions_disabled] expect(mentions_disabled).to contain_exactly(group.name) end end end end