From cbaa40edc5f57b0bc0e7653d90728aed6bb57578 Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Wed, 8 Mar 2023 12:39:03 -0300 Subject: [PATCH] FIX: Do not inherit from classes defined by plugins (#6) --- app/models/reviewable_a_i_chat_message.rb | 147 +++++++++++++++++- app/models/reviewable_a_i_post.rb | 2 + .../a_i_chat_channel_serializer.rb | 5 + .../reviewable_a_i_chat_message_serializer.rb | 18 ++- .../reviewable_a_i_post_serializer.rb | 2 + .../components/reviewable-aichat-message.js | 12 +- .../toxicity_classify_chat_message_spec.rb | 10 +- .../reviewable_a_i_chat_message_spec.rb | 79 ++++++++++ 8 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 app/serializers/a_i_chat_channel_serializer.rb create mode 100644 spec/models/reviewable_a_i_chat_message_spec.rb diff --git a/app/models/reviewable_a_i_chat_message.rb b/app/models/reviewable_a_i_chat_message.rb index ef92dda0..d56bc1aa 100644 --- a/app/models/reviewable_a_i_chat_message.rb +++ b/app/models/reviewable_a_i_chat_message.rb @@ -1,4 +1,149 @@ # frozen_string_literal: true -class ReviewableAIChatMessage < ReviewableChatMessage +require_dependency "reviewable" + +class ReviewableAIChatMessage < Reviewable + def self.action_aliases + { + agree_and_keep_hidden: :agree_and_delete, + agree_and_silence: :agree_and_delete, + agree_and_suspend: :agree_and_delete, + delete_and_agree: :agree_and_delete, + } + end + + def self.score_to_silence_user + sensitivity_score(SiteSetting.chat_silence_user_sensitivity, scale: 0.6) + end + + def chat_message + @chat_message ||= (target || ChatMessage.with_deleted.find_by(id: target_id)) + end + + def chat_message_creator + @chat_message_creator ||= chat_message.user + end + + def flagged_by_user_ids + @flagged_by_user_ids ||= reviewable_scores.map(&:user_id) + end + + def post + nil + end + + def build_actions(actions, guardian, args) + return unless pending? + return if chat_message.blank? + + agree = + actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title") + + if chat_message.deleted_at? + build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree) + build_action(actions, :agree_and_keep_deleted, icon: "thumbs-up", bundle: agree) + build_action(actions, :disagree_and_restore, icon: "thumbs-down") + else + build_action(actions, :agree_and_delete, icon: "far-eye-slash", bundle: agree) + build_action(actions, :agree_and_keep_message, icon: "thumbs-up", bundle: agree) + build_action(actions, :disagree, icon: "thumbs-down") + end + + if guardian.can_suspend?(chat_message_creator) + build_action( + actions, + :agree_and_suspend, + icon: "ban", + bundle: agree, + client_action: "suspend", + ) + build_action( + actions, + :agree_and_silence, + icon: "microphone-slash", + bundle: agree, + client_action: "silence", + ) + end + + build_action(actions, :ignore, icon: "external-link-alt") + + build_action(actions, :delete_and_agree, icon: "far-trash-alt") unless chat_message.deleted_at? + end + + def perform_agree_and_keep_message(performed_by, args) + agree + end + + def perform_agree_and_restore(performed_by, args) + agree { chat_message.recover! } + end + + def perform_agree_and_delete(performed_by, args) + agree { chat_message.trash!(performed_by) } + end + + def perform_disagree_and_restore(performed_by, args) + disagree { chat_message.recover! } + end + + def perform_disagree(performed_by, args) + disagree + end + + def perform_ignore(performed_by, args) + ignore + end + + def perform_delete_and_ignore(performed_by, args) + ignore { chat_message.trash!(performed_by) } + end + + private + + def agree + yield if block_given? + create_result(:success, :approved) do |result| + result.update_flag_stats = { status: :agreed, user_ids: flagged_by_user_ids } + result.recalculate_score = true + end + end + + def disagree + yield if block_given? + + UserSilencer.unsilence(chat_message_creator) + + create_result(:success, :rejected) do |result| + result.update_flag_stats = { status: :disagreed, user_ids: flagged_by_user_ids } + result.recalculate_score = true + end + end + + def ignore + yield if block_given? + create_result(:success, :ignored) do |result| + result.update_flag_stats = { status: :ignored, user_ids: flagged_by_user_ids } + end + end + + def build_action( + actions, + id, + icon:, + button_class: nil, + bundle: nil, + client_action: nil, + confirm: false + ) + actions.add(id, bundle: bundle) do |action| + prefix = "reviewables.actions.#{id}" + action.icon = icon + action.button_class = button_class + action.label = "chat.#{prefix}.title" + action.description = "chat.#{prefix}.description" + action.client_action = client_action + action.confirm_message = "#{prefix}.confirm" if confirm + end + end end diff --git a/app/models/reviewable_a_i_post.rb b/app/models/reviewable_a_i_post.rb index 3ecd3474..87470d52 100644 --- a/app/models/reviewable_a_i_post.rb +++ b/app/models/reviewable_a_i_post.rb @@ -1,5 +1,7 @@ # frozen_string_literal:true +require_dependency "reviewable" + class ReviewableAIPost < Reviewable # Penalties are handled by the modal after the action is performed def self.action_aliases diff --git a/app/serializers/a_i_chat_channel_serializer.rb b/app/serializers/a_i_chat_channel_serializer.rb new file mode 100644 index 00000000..a47ddce7 --- /dev/null +++ b/app/serializers/a_i_chat_channel_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class AIChatChannelSerializer < ApplicationSerializer + attributes :id, :chatable, :chatable_type, :chatable_url, :title, :slug +end diff --git a/app/serializers/reviewable_a_i_chat_message_serializer.rb b/app/serializers/reviewable_a_i_chat_message_serializer.rb index 8033a112..a41f39c9 100644 --- a/app/serializers/reviewable_a_i_chat_message_serializer.rb +++ b/app/serializers/reviewable_a_i_chat_message_serializer.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true -class ReviewableAIChatMessageSerializer < ReviewableChatMessageSerializer - payload_attributes :accuracies +require_dependency "reviewable_serializer" + +class ReviewableAIChatMessageSerializer < ReviewableSerializer + payload_attributes :accuracies, :message_cooked + target_attributes :cooked + attributes :target_id + + has_one :chat_channel, serializer: AIChatChannelSerializer, root: false, embed: :objects + + def chat_channel + object.chat_message.chat_channel + end + + def target_id + object.target&.id + end end diff --git a/app/serializers/reviewable_a_i_post_serializer.rb b/app/serializers/reviewable_a_i_post_serializer.rb index 255cb94f..1f7480b0 100644 --- a/app/serializers/reviewable_a_i_post_serializer.rb +++ b/app/serializers/reviewable_a_i_post_serializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_dependency "reviewable_flagged_post_serializer" + class ReviewableAIPostSerializer < ReviewableFlaggedPostSerializer payload_attributes :accuracies end diff --git a/assets/javascripts/discourse/components/reviewable-aichat-message.js b/assets/javascripts/discourse/components/reviewable-aichat-message.js index 140ca4f4..756b8f81 100644 --- a/assets/javascripts/discourse/components/reviewable-aichat-message.js +++ b/assets/javascripts/discourse/components/reviewable-aichat-message.js @@ -1,3 +1,13 @@ import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; -export default class ReviewableAIChatMessage extends Component {} +export default class ReviewableAIChatMessage extends Component { + @service store; + + get chatChannel() { + return this.store.createRecord( + "chat-channel", + this.args.reviewable.chat_channel + ); + } +} diff --git a/spec/lib/modules/toxicity/jobs/regular/toxicity_classify_chat_message_spec.rb b/spec/lib/modules/toxicity/jobs/regular/toxicity_classify_chat_message_spec.rb index 11399b7f..22592a82 100644 --- a/spec/lib/modules/toxicity/jobs/regular/toxicity_classify_chat_message_spec.rb +++ b/spec/lib/modules/toxicity/jobs/regular/toxicity_classify_chat_message_spec.rb @@ -18,19 +18,19 @@ describe Jobs::ToxicityClassifyChatMessage do subject.execute({ chat_message_id: chat_message.id }) - expect(ReviewableChatMessage.where(target: chat_message).count).to be_zero + expect(ReviewableAIChatMessage.where(target: chat_message).count).to be_zero end it "does nothing if there's no arg called post_id" do subject.execute({}) - expect(ReviewableChatMessage.where(target: chat_message).count).to be_zero + expect(ReviewableAIChatMessage.where(target: chat_message).count).to be_zero end it "does nothing if no post match the given id" do subject.execute({ chat_message_id: nil }) - expect(ReviewableChatMessage.where(target: chat_message).count).to be_zero + expect(ReviewableAIChatMessage.where(target: chat_message).count).to be_zero end it "does nothing if the post content is blank" do @@ -38,7 +38,7 @@ describe Jobs::ToxicityClassifyChatMessage do subject.execute({ chat_message_id: chat_message.id }) - expect(ReviewableChatMessage.where(target: chat_message).count).to be_zero + expect(ReviewableAIChatMessage.where(target: chat_message).count).to be_zero end end @@ -47,7 +47,7 @@ describe Jobs::ToxicityClassifyChatMessage do subject.execute({ chat_message_id: chat_message.id }) - expect(ReviewableChatMessage.where(target: chat_message).count).to eq(1) + expect(ReviewableAIChatMessage.where(target: chat_message).count).to eq(1) end end end diff --git a/spec/models/reviewable_a_i_chat_message_spec.rb b/spec/models/reviewable_a_i_chat_message_spec.rb new file mode 100644 index 00000000..0796d140 --- /dev/null +++ b/spec/models/reviewable_a_i_chat_message_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ReviewableAIChatMessage, type: :model do + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + fab!(:chat_channel) { Fabricate(:chat_channel) } + fab!(:chat_message) { Fabricate(:chat_message, chat_channel: chat_channel, user: user) } + fab!(:reviewable) { described_class.needs_review!(target: chat_message, created_by: moderator) } + + it "agree_and_keep agrees with the flag and doesn't delete the message" do + reviewable.perform(moderator, :agree_and_keep_message) + + expect(reviewable).to be_approved + expect(chat_message.reload.deleted_at).not_to be_present + end + + it "agree_and_delete agrees with the flag and deletes the message" do + chat_message_id = chat_message.id + reviewable.perform(moderator, :agree_and_delete) + + expect(reviewable).to be_approved + expect(ChatMessage.with_deleted.find_by(id: chat_message_id).deleted_at).to be_present + end + + it "agree_and_restore agrees with the flag and restores the message" do + chat_message.trash!(user) + reviewable.perform(moderator, :agree_and_restore) + + expect(reviewable).to be_approved + expect(chat_message.reload.deleted_at).to be_nil + end + + it "perform_disagree disagrees with the flag and does nothing" do + reviewable.perform(moderator, :disagree) + + expect(reviewable).to be_rejected + end + + it "perform_disagree_and_restore disagrees with the flag and restores the message" do + chat_message.trash!(user) + reviewable.perform(moderator, :disagree_and_restore) + + expect(reviewable).to be_rejected + expect(chat_message.reload.deleted_at).to be_nil + end + + it "perform_ignore ignores the flag and does nothing" do + reviewable.perform(moderator, :ignore) + + expect(reviewable).to be_ignored + expect(chat_message.reload.deleted_at).not_to be_present + end + + context "when the flagged message author is silenced" do + before do + UserSilencer.silence( + user, + Discourse.system_user, + silenced_till: 10.minutes.from_now, + reason: I18n.t("chat.errors.auto_silence_from_flags"), + ) + end + + it "perform_disagree unsilences the user" do + reviewable.perform(moderator, :disagree) + + expect(user.reload.silenced?).to eq(false) + end + + it "perform_disagree_and_restore unsilences the user" do + chat_message.trash!(user) + reviewable.perform(moderator, :disagree_and_restore) + + expect(user.reload.silenced?).to eq(false) + end + end +end