# frozen_string_literal: true require "rails_helper" describe Chat::ChatMessageUpdater do let(:guardian) { Guardian.new(user1) } fab!(:admin1) { Fabricate(:admin) } fab!(:admin2) { Fabricate(:admin) } fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } fab!(:user3) { Fabricate(:user) } fab!(:user4) { Fabricate(:user) } fab!(:admin_group) do Fabricate( :public_group, users: [admin1, admin2], mentionable_level: Group::ALIAS_LEVELS[:everyone], ) end fab!(:user_without_memberships) { Fabricate(:user) } fab!(:public_chat_channel) { Fabricate(:category_channel) } before do SiteSetting.chat_enabled = true SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone] SiteSetting.chat_duplicate_message_sensitivity = 0 Jobs.run_immediately! [admin1, admin2, user1, user2, user3, user4].each do |user| Fabricate(:user_chat_channel_membership, chat_channel: public_chat_channel, user: user) end Group.refresh_automatic_groups! @direct_message_channel = Chat::DirectMessageChannelCreator.create!(acting_user: user1, target_users: [user1, user2]) end def create_chat_message(user, message, channel, upload_ids: nil) creator = Chat::ChatMessageCreator.create( chat_channel: channel, user: user, in_reply_to_id: nil, content: message, upload_ids: upload_ids, ) creator.chat_message end it "errors when length is less than `chat_minimum_message_length`" do SiteSetting.chat_minimum_message_length = 10 og_message = "This won't be changed!" chat_message = create_chat_message(user1, og_message, public_chat_channel) new_message = "2 short" updater = Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: new_message, ) expect(updater.failed?).to eq(true) expect(updater.error.message).to match( I18n.t( "chat.errors.minimum_length_not_met", { minimum: SiteSetting.chat_minimum_message_length }, ), ) expect(chat_message.reload.message).to eq(og_message) end it "errors when length is greater than `chat_maximum_message_length`" do SiteSetting.chat_maximum_message_length = 100 og_message = "This won't be changed!" chat_message = create_chat_message(user1, og_message, public_chat_channel) new_message = "2 long" * 100 updater = Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: new_message, ) expect(updater.failed?).to eq(true) expect(updater.error.message).to match( I18n.t("chat.errors.message_too_long", { maximum: SiteSetting.chat_maximum_message_length }), ) expect(chat_message.reload.message).to eq(og_message) end it "errors if a user other than the message user is trying to edit the message" do og_message = "This won't be changed!" chat_message = create_chat_message(user1, og_message, public_chat_channel) new_message = "2 short" updater = Chat::ChatMessageUpdater.update( guardian: Guardian.new(Fabricate(:user)), chat_message: chat_message, new_content: new_message, ) expect(updater.failed?).to eq(true) expect(updater.error).to match(Discourse::InvalidAccess) end it "it updates a messages content" do chat_message = create_chat_message(user1, "This will be changed", public_chat_channel) new_message = "Change to this!" Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: new_message, ) expect(chat_message.reload.message).to eq(new_message) end it "publishes a DiscourseEvent for updated messages" do chat_message = create_chat_message(user1, "This will be changed", public_chat_channel) events = DiscourseEvent.track_events do Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "Change to this!", ) end expect(events.map { _1[:event_name] }).to include(:chat_message_edited) end it "creates mention notifications for unmentioned users" do chat_message = create_chat_message(user1, "This will be changed", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "this is a message with @system @mentions @#{user2.username} and @#{user3.username}", ) }.to change { user2.chat_mentions.count }.by(1).and change { user3.chat_mentions.count }.by(1) end it "doesn't create mentions for already mentioned users" do message = "ping @#{user2.username} @#{user3.username}" chat_message = create_chat_message(user1, message, public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: message + " editedddd", ) }.not_to change { ChatMention.count } end it "doesn't create mentions for users without access" do message = "ping" chat_message = create_chat_message(user1, message, public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: message + " @#{user_without_memberships.username}", ) }.not_to change { ChatMention.count } end it "destroys mention notifications that should be removed" do chat_message = create_chat_message(user1, "ping @#{user2.username} @#{user3.username}", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping @#{user3.username}", ) }.to change { user2.chat_mentions.count }.by(-1).and not_change { user3.chat_mentions.count } end it "creates new, leaves existing, and removes old mentions all at once" do chat_message = create_chat_message(user1, "ping @#{user2.username} @#{user3.username}", public_chat_channel) Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping @#{user3.username} @#{user4.username}", ) expect(user2.chat_mentions.where(chat_message: chat_message)).not_to be_present expect(user3.chat_mentions.where(chat_message: chat_message)).to be_present expect(user4.chat_mentions.where(chat_message: chat_message)).to be_present end it "does not create new mentions in direct message for users who don't have access" do chat_message = create_chat_message(user1, "ping nobody", @direct_message_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping @#{admin1.username}", ) }.not_to change { ChatMention.count } end describe "group mentions" do it "creates group mentions on update" do chat_message = create_chat_message(user1, "ping nobody", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping @#{admin_group.name}", ) }.to change { ChatMention.where(chat_message: chat_message).count }.by(2) expect(admin1.chat_mentions.where(chat_message: chat_message)).to be_present expect(admin2.chat_mentions.where(chat_message: chat_message)).to be_present end it "doesn't duplicate mentions when the user is already direct mentioned and then group mentioned" do chat_message = create_chat_message(user1, "ping @#{admin2.username}", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping @#{admin_group.name} @#{admin2.username}", ) }.to change { admin1.chat_mentions.count }.by(1).and not_change { admin2.chat_mentions.count } end it "deletes old mentions when group mention is removed" do chat_message = create_chat_message(user1, "ping @#{admin_group.name}", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "ping nobody anymore!", ) }.to change { ChatMention.where(chat_message: chat_message).count }.by(-2) expect(admin1.chat_mentions.where(chat_message: chat_message)).not_to be_present expect(admin2.chat_mentions.where(chat_message: chat_message)).not_to be_present end end it "creates a chat_message_revision record and sets last_editor_id for the message" do old_message = "It's a thrsday!" new_message = "It's a thursday!" chat_message = create_chat_message(user1, old_message, public_chat_channel) Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: new_message, ) revision = chat_message.revisions.last expect(revision.old_message).to eq(old_message) expect(revision.new_message).to eq(new_message) expect(revision.user_id).to eq(guardian.user.id) expect(chat_message.reload.last_editor_id).to eq(guardian.user.id) end describe "duplicates" do fab!(:upload1) { Fabricate(:upload, user: user1) } fab!(:upload2) { Fabricate(:upload, user: user1) } before do SiteSetting.chat_duplicate_message_sensitivity = 1.0 public_chat_channel.update!(user_count: 50) end it "errors when editing the message to be the same as one that was posted recently" do chat_message_1 = create_chat_message(user1, "this is some chat message", public_chat_channel) chat_message_2 = create_chat_message( Fabricate(:user), "another different chat message here", public_chat_channel, ) chat_message_1.update!(created_at: 30.seconds.ago) chat_message_2.update!(created_at: 20.seconds.ago) updater = Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message_1, new_content: "another different chat message here", ) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq(I18n.t("chat.errors.duplicate_message")) end it "does not count the message as a duplicate when editing leaves the message the same but changes uploads" do chat_message = create_chat_message( user1, "this is some chat message", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) chat_message.update!(created_at: 30.seconds.ago) updater = Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "this is some chat message", upload_ids: [upload2.id], ) expect(updater.failed?).to eq(false) expect(chat_message.reload.uploads.count).to eq(1) end end describe "uploads" do fab!(:upload1) { Fabricate(:upload, user: user1) } fab!(:upload2) { Fabricate(:upload, user: user1) } it "does nothing if the passed in upload_ids match the existing upload_ids" do chat_message = create_chat_message( user1, "something", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [upload2.id, upload1.id], ) }.to not_change { chat_upload_count }.and not_change { UploadReference.count } end it "removes uploads that should be removed" do chat_message = create_chat_message( user1, "something", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 DB.exec(<<~SQL) INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW()) SQL DB.exec(<<~SQL) INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW()) SQL expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [upload1.id], ) }.to change { chat_upload_count([upload2]) }.by(-1).and change { UploadReference.where(upload_id: upload2.id).count }.by(-1) end it "removes all uploads if they should be removed" do chat_message = create_chat_message( user1, "something", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 DB.exec(<<~SQL) INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW()) SQL DB.exec(<<~SQL) INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW()) SQL expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [], ) }.to change { chat_upload_count([upload1, upload2]) }.by(-2).and change { UploadReference.where(target: chat_message).count }.by(-2) end it "adds one upload if none exist" do chat_message = create_chat_message(user1, "something", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [upload1.id], ) }.to not_change { chat_upload_count([upload1]) }.and change { UploadReference.where(target: chat_message).count }.by(1) end it "adds multiple uploads if none exist" do chat_message = create_chat_message(user1, "something", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [upload1.id, upload2.id], ) }.to not_change { chat_upload_count([upload1, upload2]) }.and change { UploadReference.where(target: chat_message).count }.by(2) end it "doesn't remove existing uploads when upload ids that do not exist are passed in" do chat_message = create_chat_message(user1, "something", public_chat_channel, upload_ids: [upload1.id]) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [0], ) }.to not_change { chat_upload_count }.and not_change { UploadReference.where(target: chat_message).count } end it "doesn't add uploads if `chat_allow_uploads` is false" do SiteSetting.chat_allow_uploads = false chat_message = create_chat_message(user1, "something", public_chat_channel) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [upload1.id, upload2.id], ) }.to not_change { chat_upload_count([upload1, upload2]) }.and not_change { UploadReference.where(target: chat_message).count } end it "doesn't remove existing uploads if `chat_allow_uploads` is false" do SiteSetting.chat_allow_uploads = false chat_message = create_chat_message( user1, "something", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) expect { Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: "I guess this is different", upload_ids: [], ) }.to not_change { chat_upload_count }.and not_change { UploadReference.where(target: chat_message).count } end it "updates if upload is present even if length is less than `chat_minimum_message_length`" do chat_message = create_chat_message( user1, "something", public_chat_channel, upload_ids: [upload1.id, upload2.id], ) SiteSetting.chat_minimum_message_length = 10 new_message = "hi :)" Chat::ChatMessageUpdater.update( guardian: guardian, chat_message: chat_message, new_content: new_message, upload_ids: [upload1.id], ) expect(chat_message.reload.message).to eq(new_message) end end describe "watched words" do fab!(:watched_word) { Fabricate(:watched_word) } it "errors when a blocked word is present" do chat_message = create_chat_message(user1, "something", public_chat_channel) creator = Chat::ChatMessageCreator.create( chat_channel: public_chat_channel, user: user1, content: "bad word - #{watched_word.word}", ) expect(creator.failed?).to eq(true) expect(creator.error.message).to match( I18n.t("contains_blocked_word", { word: watched_word.word }), ) end end describe "channel statuses" do fab!(:message) { Fabricate(:chat_message, user: user1, chat_channel: public_chat_channel) } def update_message(user) message.update(user: user) Chat::ChatMessageUpdater.update( guardian: Guardian.new(user), chat_message: message, new_content: "I guess this is different", ) end context "when channel is closed" do before { public_chat_channel.update(status: :closed) } it "errors when trying to update the message for non-staff" do updater = update_message(user1) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq( I18n.t( "chat.errors.channel_modify_message_disallowed", status: public_chat_channel.status_name, ), ) end it "does not error when trying to create a message for staff" do update_message(admin1) expect(message.reload.message).to eq("I guess this is different") end end context "when channel is read_only" do before { public_chat_channel.update(status: :read_only) } it "errors when trying to update the message for all users" do updater = update_message(user1) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq( I18n.t( "chat.errors.channel_modify_message_disallowed", status: public_chat_channel.status_name, ), ) updater = update_message(admin1) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq( I18n.t( "chat.errors.channel_modify_message_disallowed", status: public_chat_channel.status_name, ), ) end end context "when channel is archived" do before { public_chat_channel.update(status: :archived) } it "errors when trying to update the message for all users" do updater = update_message(user1) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq( I18n.t( "chat.errors.channel_modify_message_disallowed", status: public_chat_channel.status_name, ), ) updater = update_message(admin1) expect(updater.failed?).to eq(true) expect(updater.error.message).to eq( I18n.t( "chat.errors.channel_modify_message_disallowed", status: public_chat_channel.status_name, ), ) end end end # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 def chat_upload_count(uploads = nil) return DB.query_single("SELECT COUNT(*) FROM chat_uploads").first if !uploads DB.query_single( "SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})", ).first end end