0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-ai/lib/agents/tools/silence_user.rb
Gabriel Grubba cc14e6e402
FEATURE: Add AI agent suspend/silence tools with inline approval review (#41497)
Previously, AI agents had no way to suspend or silence users, and
approving any moderation tool action meant leaving the conversation for
the `/review` queue.

This change adds `suspend_user`/`silence_user` tools that always require
moderator approval, and lets moderators approve or reject those actions
from an inline review card right in the bot conversation — credited to
the approving moderator (not the bot) in the staff action log — instead
of switching to the `/review` queue.


Demo of this feature:



https://github.com/user-attachments/assets/b9605511-e53d-483d-b435-b212694cdf53



https://github.com/user-attachments/assets/383bda5d-93ee-4758-a51e-6162ea0d57ee

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-07-10 10:59:51 -03:00

152 lines
4.7 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Agents
module Tools
class SilenceUser < Tool
# ~100 years — an effectively permanent silence, while keeping
# `duration_days.days.from_now` inside a representable time range.
MAX_DURATION_DAYS = 36_500
def self.signature
{
name: name,
description:
"Silences a user, blocking them from creating new posts/replies while they can still read the forum.",
parameters: [
{
name: "username",
description: "The username of the user to silence",
type: "string",
required: true,
},
{
name: "duration_days",
description:
"How many days to silence the user for. Use a very large number (e.g. 36500) for an effectively permanent silence.",
type: "integer",
required: true,
},
{
name: "reason",
description: "Short explanation of why the user is being silenced",
type: "string",
required: true,
},
{
name: "message",
description: "Optional message sent to the user explaining the silence",
type: "string",
},
],
}
end
def self.name
"silence_user"
end
def self.requires_approval?
true
end
def self.attribute_to_approver?
true
end
def invoke
if (error = validation_error)
return error
end
perform_silence
end
# Returns an error response when the request cannot proceed, or nil
# when it is valid. Checked before the action is queued for approval
# (and again at approval-replay time), so a bad username, missing
# reason, or out-of-range duration never creates a review item that
# could only fail. The approver's permission is intentionally not
# checked here — it is enforced against the approving moderator in
# #invoke.
def validation_error
if User.find_by_username(parameters[:username]).blank?
return error_response(I18n.t("discourse_ai.ai_bot.silence_user.errors.not_found"))
end
if reason.blank?
return error_response(I18n.t("discourse_ai.ai_bot.silence_user.errors.no_reason"))
end
if invalid_duration?
return(
error_response(
I18n.t(
"discourse_ai.ai_bot.silence_user.errors.invalid_duration",
max: MAX_DURATION_DAYS,
),
)
)
end
nil
end
def description_args
{ username: parameters[:username], duration_days: parameters[:duration_days] }
end
private
def perform_silence
user = User.find_by_username(parameters[:username])
if !guardian.can_silence_user?(user)
return error_response(I18n.t("discourse_ai.ai_bot.silence_user.errors.not_allowed"))
end
result =
User::Silence.call(
guardian: guardian,
params: {
user_id: user.id,
reason: reason,
silenced_till: duration_days.days.from_now,
message: parameters[:message],
reviewable_id: context.reviewable_id,
},
)
return error_response(silence_error_message(result)) if result.failure?
{
status: "success",
message: I18n.t("discourse_ai.ai_bot.silence_user.success", username: user.username),
}
end
def duration_days
Integer(parameters[:duration_days], exception: false)
end
def invalid_duration?
days = duration_days
days.nil? || days <= 0 || days > MAX_DURATION_DAYS
end
def silence_error_message(result)
contract_result = result["result.contract.default"]
return contract_result.errors.full_messages.to_sentence if contract_result&.failure?
%i[not_silenced_already can_silence_all_users].each do |policy_name|
policy_result = result["result.policy.#{policy_name}"]
next if !policy_result&.failure?
return policy_result.reason if policy_result.reason.present?
return I18n.t("discourse_ai.ai_bot.silence_user.errors.failed")
end
I18n.t("discourse_ai.ai_bot.silence_user.errors.failed")
end
end
end
end
end