mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
Previously, `POST /posts.json` accepted an `ai_agent_id` topic custom field of any size and content, storing it unvalidated since it is registered as a publicly editable topic custom field. Additionally, a PM could be created directly to an agent's dedicated bot user even when that agent disallowed personal messages or the sender was outside its allowed groups, by simply omitting the field. This change validates agent access during topic creation via `TopicAgentValidator`: a supplied `ai_agent_id` must be a numeric id within 20 bytes resolving to an agent the user can access (and one allowing personal messages when the topic is a PM), and PMs targeting an agent's dedicated user now enforce the same group and PM policy from the recipient side, returning a 422 otherwise. PMs to default LLM bot users without an agent id remain unaffected.
87 lines
2.6 KiB
Ruby
Vendored
87 lines
2.6 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module AiBot
|
|
class TopicAgentValidator
|
|
def self.validate(topic, topic_creator)
|
|
new(topic, topic_creator).validate
|
|
end
|
|
|
|
def initialize(topic, topic_creator)
|
|
@topic = topic
|
|
@user = topic_creator.user
|
|
@opts = topic_creator.opts
|
|
end
|
|
|
|
def validate
|
|
validate_agent_field
|
|
validate_agent_recipients if @topic.private_message?
|
|
end
|
|
|
|
private
|
|
|
|
def validate_agent_field
|
|
# in PostCreator opts, topic custom fields live under topic_opts and
|
|
# are merged over the top-level (post) custom_fields at create time
|
|
fields = @opts.dig(:topic_opts, :custom_fields) || @opts[:custom_fields]
|
|
raw_agent_id = fields && (fields[TOPIC_AI_AGENT_ID_FIELD] || fields[:ai_agent_id])
|
|
return if raw_agent_id.blank?
|
|
|
|
raw_agent_id = raw_agent_id.to_s
|
|
|
|
if raw_agent_id.bytesize > TOPIC_AI_AGENT_ID_MAX_LENGTH
|
|
@topic.errors.add(
|
|
:base,
|
|
I18n.t(
|
|
"custom_fields.validations.max_value_length",
|
|
max_value_length: TOPIC_AI_AGENT_ID_MAX_LENGTH,
|
|
),
|
|
)
|
|
return
|
|
end
|
|
|
|
agent = accessible_agent(raw_agent_id.to_i) if raw_agent_id.match?(/\A-?\d+\z/)
|
|
|
|
if agent.blank? || (@topic.private_message? && !agent.allow_personal_messages)
|
|
@topic.errors.add(:base, I18n.t("discourse_ai.ai_bot.errors.invalid_agent_id"))
|
|
end
|
|
end
|
|
|
|
# an agent's dedicated user can be targeted directly, bypassing the
|
|
# custom field entirely, so the agent's PM policy must also be enforced
|
|
# from the recipient side
|
|
def validate_agent_recipients
|
|
targeted_agents.each do |targeted_agent|
|
|
agent = accessible_agent(targeted_agent.id)
|
|
|
|
if agent.blank? || !agent.allow_personal_messages
|
|
@topic.errors.add(:base, I18n.t("discourse_ai.ai_bot.errors.cannot_send_pm_to_agent"))
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
def targeted_agents
|
|
usernames = @opts[:target_usernames]
|
|
user_ids = @opts[:target_user_ids]
|
|
|
|
if usernames.present?
|
|
usernames = usernames.split(",") if usernames.is_a?(String)
|
|
AiAgent.joins(:user).where(
|
|
users: {
|
|
username_lower: usernames.map { |username| username.to_s.strip.downcase },
|
|
},
|
|
)
|
|
elsif user_ids.present?
|
|
AiAgent.where(user_id: user_ids)
|
|
else
|
|
AiAgent.none
|
|
end
|
|
end
|
|
|
|
def accessible_agent(id)
|
|
DiscourseAi::Agents::Agent.find_by(user: @user, id: id)
|
|
end
|
|
end
|
|
end
|
|
end
|