# frozen_string_literal: true require "rails_helper" describe Chat::ChatReviewQueue do fab!(:message_poster) { Fabricate(:user) } fab!(:flagger) { Fabricate(:user) } fab!(:chat_channel) { Fabricate(:category_channel) } fab!(:message) { Fabricate(:chat_message, user: message_poster, chat_channel: chat_channel) } fab!(:admin) { Fabricate(:admin) } let(:guardian) { Guardian.new(flagger) } let(:admin_guardian) { Guardian.new(admin) } subject(:queue) { described_class.new } before do chat_channel.add(message_poster) chat_channel.add(flagger) Group.refresh_automatic_groups! end describe "#flag_message" do it "raises an error when the user is not allowed to flag" do UserSilencer.new(flagger).silence expect { queue.flag_message(message, guardian, ReviewableScore.types[:spam]) }.to raise_error( Discourse::InvalidAccess, ) end it "stores the message cooked content inside the reviewable" do queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) reviewable = ReviewableChatMessage.last expect(reviewable.payload["message_cooked"]).to eq(message.cooked) end context "when the user already flagged the post" do let(:second_flag_result) do queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) end before { queue.flag_message(message, guardian, ReviewableScore.types[:spam]) } it "returns an error" do expect(second_flag_result).to include success: false, errors: [I18n.t("chat.reviewables.message_already_handled")] end it "returns an error when trying to use notify_moderators and the previous flag is still pending" do notify_moderators_result = queue.flag_message( message, guardian, ReviewableScore.types[:notify_moderators], message: "Look at this please, moderators", ) expect(notify_moderators_result).to include success: false, errors: [I18n.t("chat.reviewables.message_already_handled")] end end context "when a different user already flagged the post" do let(:second_flag_result) { queue.flag_message(message, admin_guardian, second_flag_type) } before { queue.flag_message(message, guardian, ReviewableScore.types[:spam]) } it "appends a new score to the existing reviewable" do second_flag_result = queue.flag_message(message, admin_guardian, ReviewableScore.types[:off_topic]) expect(second_flag_result).to include success: true reviewable = ReviewableChatMessage.find_by(target: message) scores = reviewable.reviewable_scores expect(scores.size).to eq(2) expect(scores.map(&:reviewable_score_type)).to contain_exactly( *ReviewableScore.types.slice(:off_topic, :spam).values, ) end it "returns an error when someone already used the same flag type" do second_flag_result = queue.flag_message(message, admin_guardian, ReviewableScore.types[:spam]) expect(second_flag_result).to include success: false, errors: [I18n.t("chat.reviewables.message_already_handled")] end end context "when a flags exists but staff already handled it" do let(:second_flag_result) do queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) end before do queue.flag_message(message, guardian, ReviewableScore.types[:spam]) reviewable = ReviewableChatMessage.last reviewable.perform(admin, :ignore) end it "raises an error when we are inside the cooldown window" do expect(second_flag_result).to include success: false, errors: [I18n.t("chat.reviewables.message_already_handled")] end it "allows the user to re-flag after the cooldown period" do reviewable = ReviewableChatMessage.last reviewable.update!(updated_at: (SiteSetting.cooldown_hours_until_reflag.to_i + 1).hours.ago) expect(second_flag_result).to include success: true end it "ignores the cooldown window when the message is edited" do Chat::ChatMessageUpdater.update( chat_message: message, new_content: "I'm editing this message. Please flag it.", ) expect(second_flag_result).to include success: true end it "ignores the cooldown window when using the notify_moderators flag type" do notify_moderators_result = queue.flag_message( message, guardian, ReviewableScore.types[:notify_moderators], message: "Look at this please, moderators", ) expect(notify_moderators_result).to include success: true end end it "publishes a message to the flagger" do messages = MessageBus .track_publish { queue.flag_message(message, guardian, ReviewableScore.types[:spam]) } .map(&:data) self_flag_msg = messages.detect { |m| m["type"] == "self_flagged" } expect(self_flag_msg["user_flag_status"]).to eq(ReviewableScore.statuses[:pending]) expect(self_flag_msg["chat_message_id"]).to eq(message.id) end it "publishes a message to tell staff there is a new reviewable" do messages = MessageBus .track_publish { queue.flag_message(message, guardian, ReviewableScore.types[:spam]) } .map(&:data) flag_msg = messages.detect { |m| m["type"] == "flag" } new_reviewable = ReviewableChatMessage.find_by(target: message) expect(flag_msg["chat_message_id"]).to eq(message.id) expect(flag_msg["reviewable_id"]).to eq(new_reviewable.id) end let(:flag_message) { "I just flagged your chat message..." } context "when creating a notify_user flag" do it "creates a companion PM" do queue.flag_message( message, guardian, ReviewableScore.types[:notify_user], message: flag_message, ) pm_topic = Topic.includes(:posts).find_by(user: guardian.user, archetype: Archetype.private_message) pm_post = pm_topic.first_post expect(pm_topic.allowed_users).to include(message.user) expect(pm_topic.subtype).to eq(TopicSubtype.notify_user) expect(pm_post.raw).to include(flag_message) expect(pm_topic.title).to eq("Your chat message in \"#{chat_channel.title(message.user)}\"") end it "doesn't create a PM if there is no message" do queue.flag_message(message, guardian, ReviewableScore.types[:notify_user]) pm_topic = Topic.includes(:posts).find_by(user: guardian.user, archetype: Archetype.private_message) expect(pm_topic).to be_nil end it "allow staff to tag PM as a warning" do queue.flag_message( message, admin_guardian, ReviewableScore.types[:notify_user], message: flag_message, is_warning: true, ) expect(UserWarning.exists?(user: message.user)).to eq(true) end it "only allows staff members to send warnings" do expect do queue.flag_message( message, guardian, ReviewableScore.types[:notify_user], message: flag_message, is_warning: true, ) end.to raise_error(Discourse::InvalidAccess) end end context "when creating a notify_moderators flag" do it "creates a companion PM and gives moderators access to it" do queue.flag_message( message, guardian, ReviewableScore.types[:notify_moderators], message: flag_message, ) pm_topic = Topic.includes(:posts).find_by(user: guardian.user, archetype: Archetype.private_message) pm_post = pm_topic.first_post expect(pm_topic.allowed_groups).to contain_exactly(Group[:moderators]) expect(pm_topic.subtype).to eq(TopicSubtype.notify_moderators) expect(pm_post.raw).to include(flag_message) expect(pm_topic.title).to eq( "A chat message in \"#{chat_channel.title(message.user)}\" requires staff attention", ) end it "ignores the is_warning flag when notifying moderators" do queue.flag_message( message, guardian, ReviewableScore.types[:notify_moderators], message: flag_message, is_warning: true, ) expect(UserWarning.exists?(user: message.user)).to eq(false) end end context "when immediately taking action" do it "agrees with the flag and deletes the chat message" do queue.flag_message( message, admin_guardian, ReviewableScore.types[:off_topic], take_action: true, ) reviewable = ReviewableChatMessage.find_by(target: message) expect(reviewable.approved?).to eq(true) expect(message.reload.trashed?).to eq(true) end it "publishes an when deleting the message" do messages = MessageBus .track_publish do queue.flag_message( message, admin_guardian, ReviewableScore.types[:off_topic], take_action: true, ) end .map(&:data) delete_msg = messages.detect { |m| m[:type] == "delete" } expect(delete_msg[:deleted_id]).to eq(message.id) end it "agrees with other flags on the same message" do queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) reviewable = ReviewableChatMessage.includes(:reviewable_scores).find_by(target: message) scores = reviewable.reviewable_scores expect(scores.size).to eq(1) expect(scores.all?(&:pending?)).to eq(true) queue.flag_message(message, admin_guardian, ReviewableScore.types[:spam], take_action: true) scores = reviewable.reload.reviewable_scores expect(scores.size).to eq(2) expect(scores.all?(&:agreed?)).to eq(true) end it "raises an exception if the user is not a staff member" do expect do queue.flag_message( message, guardian, ReviewableScore.types[:off_topic], take_action: true, ) end.to raise_error(Discourse::InvalidAccess) end end context "when queueing for review" do it "sets a reason on the score" do queue.flag_message( message, admin_guardian, ReviewableScore.types[:off_topic], queue_for_review: true, ) reviewable = ReviewableChatMessage.includes(:reviewable_scores).find_by(target: message) score = reviewable.reviewable_scores.first expect(score.reason).to eq("chat_message_queued_by_staff") end it "only allows staff members to queue for review" do expect do queue.flag_message( message, guardian, ReviewableScore.types[:off_topic], queue_for_review: true, ) end.to raise_error(Discourse::InvalidAccess) end end context "when the auto silence threshold is met" do it "silences the user" do SiteSetting.chat_auto_silence_from_flags_duration = 1 flagger.update!(trust_level: TrustLevel[4]) # Increase Score due to TL Bonus. queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) expect(message_poster.reload.silenced?).to eq(true) end it "does nothing if the new score is less than the auto-silence threshold" do SiteSetting.chat_auto_silence_from_flags_duration = 50 queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) expect(message_poster.reload.silenced?).to eq(false) end it "does nothing if the silence duration is set to 0" do SiteSetting.chat_auto_silence_from_flags_duration = 0 flagger.update!(trust_level: TrustLevel[4]) # Increase Score due to TL Bonus. queue.flag_message(message, guardian, ReviewableScore.types[:off_topic]) expect(message_poster.reload.silenced?).to eq(false) end end context "when flagging a DM" do fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [message_poster, flagger]) } 12.times do |i| fab!("dm_message_#{i + 1}") do Fabricate( :chat_message, user: message_poster, chat_channel: dm_channel, message: "This is my message number #{i + 1}. Hello chat!", ) end end it "raises an exception when using the notify_moderators flag type" do expect { queue.flag_message(dm_message_1, guardian, ReviewableScore.types[:notify_moderators]) }.to raise_error(Discourse::InvalidParameters) end it "raises an exception when using the notify_user flag type" do expect { queue.flag_message(dm_message_1, guardian, ReviewableScore.types[:notify_user]) }.to raise_error(Discourse::InvalidParameters) end it "includes a transcript of the previous 10 message for the rest of the flags" do queue.flag_message(dm_message_12, guardian, ReviewableScore.types[:off_topic]) reviewable = ReviewableChatMessage.last expect(reviewable.target).to eq(dm_message_12) transcript_post = Post.find_by(topic_id: reviewable.payload["transcript_topic_id"]) expect(transcript_post.cooked).to include(dm_message_2.message) expect(transcript_post.cooked).to include(dm_message_5.message) expect(transcript_post.cooked).not_to include(dm_message_1.message) end it "doesn't include a transcript if there a no previous messages" do queue.flag_message(dm_message_1, guardian, ReviewableScore.types[:off_topic]) reviewable = ReviewableChatMessage.last expect(reviewable.payload["transcript_topic_id"]).to be_nil end it "the transcript is only available to moderators and the system user" do moderator = Fabricate(:moderator) admin = Fabricate(:admin) leader = Fabricate(:leader) tl4 = Fabricate(:trust_level_4) queue.flag_message(dm_message_12, guardian, ReviewableScore.types[:off_topic]) reviewable = ReviewableChatMessage.last transcript_topic = Topic.find(reviewable.payload["transcript_topic_id"]) expect(guardian.can_see_topic?(transcript_topic)).to eq(false) expect(Guardian.new(leader).can_see_topic?(transcript_topic)).to eq(false) expect(Guardian.new(tl4).can_see_topic?(transcript_topic)).to eq(false) expect(Guardian.new(dm_message_12.user).can_see_topic?(transcript_topic)).to eq(false) expect(Guardian.new(moderator).can_see_topic?(transcript_topic)).to eq(true) expect(Guardian.new(admin).can_see_topic?(transcript_topic)).to eq(true) expect(Guardian.new(Discourse.system_user).can_see_topic?(transcript_topic)).to eq(true) end end end end