Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV: Add structure for errors in spam #1054

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/controllers/discourse_ai/admin/ai_spam_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ def test
render json: result
end

def fix_errors
case params[:error]
when "spam_scanner_not_admin"
begin
DiscourseAi::AiModeration::SpamScanner.fix_spam_scanner_not_admin
render json: success_json
rescue ActiveRecord::RecordInvalid
render_json_error(
I18n.t("discourse_ai.spam_detection.bot_user_update_failed"),
status: :unprocessable_entity,
)
rescue StandardError
render_json_error(
I18n.t("discourse_ai.spam_detection.unexpected"),
status: :internal_server_error,
)
end
else
render_json_error(
I18n.t("discourse_ai.spam_detection.invalid_error_type"),
status: :bad_request,
)
end
end

private

def allowed_params
Expand Down
9 changes: 8 additions & 1 deletion app/serializers/ai_spam_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class AiSpamSerializer < ApplicationSerializer
:available_llms,
:stats,
:flagging_username,
:spam_score_type
:spam_score_type,
:spam_scanning_user

def is_enabled
object[:enabled]
Expand Down Expand Up @@ -47,4 +48,10 @@ def stats
def settings
object[:settings]
end

def spam_scanning_user
user = DiscourseAi::AiModeration::SpamScanner.flagging_user

user.serializable_hash(only: %i[id username name admin]) if user.present?
end
end
55 changes: 54 additions & 1 deletion assets/javascripts/discourse/components/ai-spam.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DTooltip from "discourse/components/d-tooltip";
import withEventValue from "discourse/helpers/with-event-value";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import dIcon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import getURL from "discourse-common/lib/get-url";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
Expand All @@ -35,10 +36,51 @@ export default class AiSpam extends Component {
@tracked isEnabled = false;
@tracked selectedLLM = null;
@tracked customInstructions = "";
@tracked errors = [];

constructor() {
super(...arguments);
this.initializeFromModel();

if (this.args.model?.spam_scanning_user?.admin === false) {
this.errors.push({
message: i18n("discourse_ai.spam.errors.scan_not_admin.message"),
button: {
label: i18n("discourse_ai.spam.errors.scan_not_admin.action"),
action: this.fixScanUserNotAdmin,
},
});
}
}

@action
async fixScanUserNotAdmin() {
const spamScanningUser = this.args.model.spam_scanning_user;
if (!spamScanningUser || spamScanningUser.admin) {
return;
}
try {
const response = await ajax(
`/admin/plugins/discourse-ai/ai-spam/fix-errors`,
{
type: "POST",
data: {
error: "spam_scanner_not_admin",
},
}
);

if (response.success) {
this.toasts.success({
data: { message: i18n("discourse_ai.spam.errors.resolved") },
duration: 2000,
});
}
} catch (error) {
popupAjaxError(error);
} finally {
window.location.reload();
}
}

@action
Expand Down Expand Up @@ -165,11 +207,22 @@ export default class AiSpam extends Component {
<template>
<div class="ai-spam">
<section class="ai-spam__settings">
<div class="ai-spam__errors">
{{#each this.errors as |e|}}
<div class="alert alert-error">
{{dIcon "triangle-exclamation"}}
<p>{{e.message}}</p>
<DButton
@action={{e.button.action}}
@translatedLabel={{e.button.label}}
/>
</div>
{{/each}}
</div>
<DPageSubheader
@titleLabel={{i18n "discourse_ai.spam.title"}}
@descriptionLabel={{i18n "discourse_ai.spam.spam_description"}}
/>

<div class="control-group ai-spam__enabled">
<DToggleSwitch
class="ai-spam__toggle"
Expand Down
12 changes: 12 additions & 0 deletions assets/stylesheets/modules/llms/common/spam.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@
&__stats {
margin-top: 2em;
}

&__errors {
.alert {
display: flex;
align-items: center;
gap: 0.5rem;

.btn {
margin-left: auto;
}
}
}
}

.spam-test-modal {
Expand Down
5 changes: 5 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ en:
stat_tooltips:
incorrectly_flagged: "Items that the AI bot flagged as spam where moderators disagreed"
missed_spam: "Items flagged by the community as spam that were not detected by the AI bot, which moderators agreed with"
errors:
scan_not_admin:
message: "Warning: spam scanning will not work correctly because the spam scan account is not an admin"
action: "Fix"
resolved: "The error has been resolved!"

usage:
short_title: "Usage"
Expand Down
4 changes: 4 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ en:
spam_detection:
flag_reason: "Flagged as spam by <a href='%{url}'>Discourse AI</a>"
silence_reason: "User silenced automatically by <a href='%{url}'>Discourse AI</a>"
invalid_error_type: "Invalid error type provided"
unexpected: "An unexpected error occured"
bot_user_update_failed: "Failed to update the spam scanning bot user"

ai_bot:
reply_error: "Sorry, it looks like our system encountered an unexpected issue while trying to reply.\n\n[details='Error details']\n%{details}\n[/details]"
default_pm_prefix: "[Untitled AI bot PM]"
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
get "/ai-spam", to: "discourse_ai/admin/ai_spam#show"
put "/ai-spam", to: "discourse_ai/admin/ai_spam#update"
post "/ai-spam/test", to: "discourse_ai/admin/ai_spam#test"
post "/ai-spam/fix-errors", to: "discourse_ai/admin/ai_spam#fix_errors"

resources :ai_llms,
only: %i[index new create edit update destroy],
Expand Down
10 changes: 10 additions & 0 deletions lib/ai_moderation/spam_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ def self.perform_scan(post)
end
end

def self.fix_spam_scanner_not_admin
user = DiscourseAi::AiModeration::SpamScanner.flagging_user

if user.present?
user.update!(admin: true)
else
raise Discourse::NotFound
end
end

private

def self.check_if_spam(result)
Expand Down
45 changes: 45 additions & 0 deletions spec/requests/admin/ai_spam_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,49 @@
end
end
end

describe "#fix_errors" do
fab!(:setting) do
AiModerationSetting.create(
{
setting_type: :spam,
llm_model_id: llm_model.id,
data: {
custom_instructions: "custom instructions",
},
},
)
fab!(:llm_model)

before do
sign_in(admin)
DiscourseAi::AiModeration::SpamScanner.flagging_user.update!(admin: false)
end

it "resolves spam scanner not admin error" do
post "/admin/plugins/discourse-ai/ai-spam/fix-errors",
params: {
error: "spam_scanner_not_admin",
}

expect(response.status).to eq(200)
expect(DiscourseAi::AiModeration::SpamScanner.flagging_user.reload.admin).to eq(true)
end

it "returns an error when it can't update the user" do
DiscourseAi::AiModeration::SpamScanner.flagging_user.destroy

post "/admin/plugins/discourse-ai/ai-spam/fix-errors",
params: {
error: "spam_scanner_not_admin",
}

expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to be_present
expect(response.parsed_body["errors"].first).to eq(
I18n.t("discourse_ai.spam_detection.bot_user_update_failed"),
)
end
end
end
end
Loading