discourse/plugins/chat/lib/chat_sdk/message.rb
Joffrey JAFFEUX 26c8eab1f3
FIX: allows bots to create/update/stream messages (#26900)
Prior to this commit, only system users had this pass.

Another significant change of the PR, is to make membership of a channel the angular stone of the permission check to create/update/stop streaming a message. The idea being, if you are a member of a channel already we don't need to check if you can join it AGAIN.

We also have `Chat::AutoRemove::HandleCategoryUpdated` which will deal with permissions change so it's simpler and less prone to error to consider the membership as the only source of truth.
2024-05-07 15:17:42 +02:00

194 lines
7.3 KiB
Ruby

# frozen_string_literal: true
module ChatSDK
class Message
include WithServiceHelper
# Creates a new message in a chat channel.
#
# @param raw [String] The content of the message.
# @param channel_id [Integer] The ID of the chat channel.
# @param guardian [Guardian] The user's guardian object, for policy enforcement.
# @param in_reply_to_id [Integer, nil] The ID of the message this is in reply to (optional).
# @param thread_id [Integer, nil] The ID of the thread this message belongs to (optional).
# @param upload_ids [Array<Integer>, nil] The IDs of any uploads associated with the message (optional).
# @param streaming [Boolean] Whether the message is part of a streaming operation (default: false).
# @param enforce_membership [Boolean] Allows to ensure the guardian will be allowed in the channel (default: false).
# @yield [helper, message] Offers a block with a helper and the message for streaming operations.
# @yieldparam helper [Helper] The helper object for streaming operations.
# @yieldparam message [Message] The newly created message object.
# @return [Chat::Message] The created message object.
#
# @example Creating a simple message
# ChatSDK::Message.create(raw: "Hello, world!", channel_id: 1, guardian: Guardian.new)
#
# @example Creating a message with a block for streaming
# ChatSDK::Message.create_with_stream(raw: "Streaming message", channel_id: 1, guardian: Guardian.new) do |helper, message|
# helper.stream(raw: "Continuation of the message")
# end
def self.create(**params, &block)
new.create(**params, &block)
end
# Creates a new message with streaming enabled by default.
#
# This method is a convenience wrapper around `create` with `streaming: true` set by default.
# It supports all the same parameters and block usage as `create`.
#
# @see #create
def self.create_with_stream(**params, &block)
self.create(**params, streaming: true, strip_whitespaces: false, &block)
end
# Streams to a specific chat message.
#
# @param raw [String] text to append to the existing message.
# @param message_id [Integer] the ID of the message to stream.
# @param guardian [Guardian] an instance of the guardian class, representing the user's permissions.
# @return [Chat::Message] The message object.
# @example Streaming a message
# ChatSDK::Message.stream(message_id: 42, guardian: guardian, raw: "text")
def self.stream(raw:, message_id:, guardian:, &block)
new.stream(raw: raw, message_id: message_id, guardian: guardian, &block)
end
# Starts streaming for a specific chat message.
#
# @param message_id [Integer] the ID of the message for which streaming should be stopped.
# @param guardian [Guardian] an instance of the guardian class, representing the user's permissions.
# @return [Chat::Message] The message object.
# @example Starting the streaming of a message
# ChatSDK::Message.start_stream(message_id: 42, guardian: guardian)
def self.start_stream(message_id:, guardian:)
new.start_stream(message_id: message_id, guardian: guardian)
end
# Stops streaming for a specific chat message.
#
# @param message_id [Integer] the ID of the message for which streaming should be stopped.
# @param guardian [Guardian] an instance of the guardian class, representing the user's permissions.
# @return [Chat::Message] The message object.
# @example Stopping the streaming of a message
# ChatSDK::Message.stop_stream(message_id: 42, guardian: guardian)
def self.stop_stream(message_id:, guardian:)
new.stop_stream(message_id: message_id, guardian: guardian)
end
def start_stream(message_id:, guardian:)
message = Chat::Message.find(message_id)
guardian.ensure_can_edit_chat!(message)
message.update!(streaming: true)
::Chat::Publisher.publish_edit!(message.chat_channel, message.reload)
message
end
def stream(message_id:, raw:, guardian:, &block)
message = Chat::Message.find(message_id)
helper = StreamHelper.new(message, guardian)
helper.stream(raw: raw)
::Chat::Publisher.publish_edit!(message.chat_channel, message.reload)
message
end
def stop_stream(message_id:, guardian:)
with_service(Chat::StopMessageStreaming, message_id: message_id, guardian: guardian) do
on_success { result.message }
on_model_not_found(:message) { raise "Couldn't find message with id: `#{message_id}`" }
on_model_not_found(:membership) do
raise "Couldn't find membership for user with id: `#{guardian.user.id}`"
end
on_failed_policy(:can_join_channel) do
raise "User with id: `#{guardian.user.id}` can't join this channel"
end
on_failed_policy(:can_stop_streaming) do
raise "User with id: `#{guardian.user.id}` can't stop streaming this message"
end
on_failure { raise "Unexpected error" }
end
end
def create(
raw:,
channel_id:,
guardian:,
in_reply_to_id: nil,
thread_id: nil,
upload_ids: nil,
streaming: false,
enforce_membership: false,
force_thread: false,
strip_whitespaces: true,
&block
)
message =
with_service(
Chat::CreateMessage,
message: raw,
guardian: guardian,
chat_channel_id: channel_id,
in_reply_to_id: in_reply_to_id,
thread_id: thread_id,
upload_ids: upload_ids,
streaming: streaming,
enforce_membership: enforce_membership,
force_thread: force_thread,
strip_whitespaces: strip_whitespaces,
) do
on_model_not_found(:channel) { raise "Couldn't find channel with id: `#{channel_id}`" }
on_model_not_found(:membership) do
raise "Couldn't find membership for user with id: `#{guardian.user.id}`"
end
on_failed_policy(:ensure_valid_thread_for_channel) do
raise "Couldn't find thread with id: `#{thread_id}`"
end
on_failed_policy(:allowed_to_join_channel) do
raise "User with id: `#{guardian.user.id}` can't join this channel"
end
on_failed_contract { |contract| raise contract.errors.full_messages.join(", ") }
on_success { result.message_instance }
on_failure { raise "Unexpected error" }
end
if streaming && block_given?
helper = StreamHelper.new(message, guardian)
block.call(helper, message)
end
message
ensure
if message && streaming
message.update!(streaming: false)
::Chat::Publisher.publish_edit!(message.chat_channel, message.reload)
end
end
end
class StreamHelper
include WithServiceHelper
attr_reader :message
attr_reader :guardian
def initialize(message, guardian)
@message = message.reload
@guardian = guardian
end
def stream(raw: nil)
return false if !self.message.streaming
return false if !raw
with_service(
Chat::UpdateMessage,
message_id: self.message.id,
message: self.message.message + raw,
guardian: self.guardian,
streaming: true,
strip_whitespaces: false,
) { on_failure { raise "Unexpected error" } }
self.message
end
end
end