mirror of
https://github.com/discourse/discourse.git
synced 2025-05-30 07:11:34 +08:00
DEV: chat streaming (#25736)
This commit introduces the possibility to stream messages. To allow plugins to use streaming this commit also ships a `ChatSDK` library to allow to interact with few parts of discourse chat. ```ruby ChatSDK::Message.create_with_stream(raw: "test") do |helper| 5.times do |i| is_streaming = helper.stream(raw: "more #{i}") next if !is_streaming sleep 2 end end ``` This commit also introduces all the frontend parts: - messages can now be marked as streaming - when streaming their content will be updated when a new content is appended - a special UI will be showing (a blinking indicator) - a cancel button allows the user to stop the streaming, when cancelled `helper.stream(...)` will return `false`, and the plugin can decide exit early
This commit is contained in:
38
plugins/chat/lib/chat_sdk/channel.rb
Normal file
38
plugins/chat/lib/chat_sdk/channel.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ChatSDK
|
||||
class Channel
|
||||
include Chat::WithServiceHelper
|
||||
|
||||
# Retrieves messages from a specified channel.
|
||||
#
|
||||
# @param channel_id [Integer] The ID of the chat channel from which to fetch messages.
|
||||
# @param guardian [Guardian] The guardian object representing the user's permissions.
|
||||
# @return [Array<ChMessage>] An array of message objects from the specified channel.
|
||||
#
|
||||
# @example Fetching messages from a channel with additional parameters
|
||||
# ChatSDK::Channel.messages(channel_id: 1, guardian: Guardian.new)
|
||||
#
|
||||
def self.messages(channel_id:, guardian:, **params)
|
||||
new.messages(channel_id: channel_id, guardian: guardian, **params)
|
||||
end
|
||||
|
||||
def messages(channel_id:, guardian:, **params)
|
||||
with_service(
|
||||
Chat::ListChannelMessages,
|
||||
channel_id: channel_id,
|
||||
guardian: guardian,
|
||||
**params,
|
||||
direction: "future",
|
||||
) do
|
||||
on_success { result.messages }
|
||||
on_failure do
|
||||
p Chat::StepsInspector.new(result)
|
||||
raise "Unexpected error"
|
||||
end
|
||||
on_failed_policy(:can_view_channel) { raise "Guardian can't view channel" }
|
||||
on_failed_policy(:target_message_exists) { raise "Target message doesn't exist" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
126
plugins/chat/lib/chat_sdk/message.rb
Normal file
126
plugins/chat/lib/chat_sdk/message.rb
Normal file
@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ChatSDK
|
||||
class Message
|
||||
include Chat::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 [ChMessage] 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
|
||||
# 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, &block)
|
||||
end
|
||||
|
||||
def create(
|
||||
raw:,
|
||||
channel_id:,
|
||||
guardian:,
|
||||
in_reply_to_id: nil,
|
||||
thread_id: nil,
|
||||
upload_ids: nil,
|
||||
streaming: false,
|
||||
enforce_membership: false,
|
||||
&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,
|
||||
) do
|
||||
on_model_not_found(:channel) { raise "Couldn't find channel with id: `#{channel_id}`" }
|
||||
on_model_not_found(:channel_membership) do
|
||||
raise "User with id: `#{guardian.user.id}` has no membership to this channel"
|
||||
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 do
|
||||
p Chat::StepsInspector.new(result)
|
||||
raise "Unexpected error"
|
||||
end
|
||||
end
|
||||
|
||||
if streaming && block_given?
|
||||
helper = Helper.new(message)
|
||||
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 Helper
|
||||
include Chat::WithServiceHelper
|
||||
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
|
||||
def stream(raw: nil)
|
||||
return false unless self.message.reload.streaming
|
||||
|
||||
with_service(
|
||||
Chat::UpdateMessage,
|
||||
message_id: self.message.id,
|
||||
message: raw ? self.message.reload.message + " " + raw : self.message.message,
|
||||
guardian: self.message.user.guardian,
|
||||
streaming: true,
|
||||
) do
|
||||
on_failure do
|
||||
p Chat::StepsInspector.new(result)
|
||||
raise "Unexpected error"
|
||||
end
|
||||
end
|
||||
|
||||
self.message
|
||||
end
|
||||
end
|
||||
end
|
80
plugins/chat/lib/chat_sdk/thread.rb
Normal file
80
plugins/chat/lib/chat_sdk/thread.rb
Normal file
@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ChatSDK
|
||||
class Thread
|
||||
include Chat::WithServiceHelper
|
||||
|
||||
# Updates the title of a specified chat thread.
|
||||
#
|
||||
# @param title [String] The new title for the chat thread.
|
||||
# @param thread_id [Integer] The ID of the chat thread to be updated.
|
||||
# @param guardian [Guardian] The guardian object representing the user's permissions.
|
||||
# @return [Chat::Thread] The updated thread object with the new title.
|
||||
#
|
||||
# @example Updating the title of a chat thread
|
||||
# ChatSDK::Thread.update_title(title: "New Thread Title", thread_id: 1, guardian: Guardian.new)
|
||||
def self.update_title(**params)
|
||||
new.update(title: params[:title], thread_id: params[:thread_id], guardian: params[:guardian])
|
||||
end
|
||||
|
||||
def self.update(**params)
|
||||
new.update(**params)
|
||||
end
|
||||
|
||||
# Retrieves messages from a specified thread.
|
||||
#
|
||||
# @param thread_id [Integer] The ID of the chat thread from which to fetch messages.
|
||||
# @param guardian [Guardian] The guardian object representing the user's permissions.
|
||||
# @return [Array<Chat::Message>] An array of message objects from the specified thread.
|
||||
#
|
||||
# @example Fetching messages from a thread with additional parameters
|
||||
# ChatSDK::Thread.messages(thread_id: 1, guardian: Guardian.new)
|
||||
#
|
||||
def self.messages(thread_id:, guardian:, **params)
|
||||
new.messages(thread_id: thread_id, guardian: guardian, **params)
|
||||
end
|
||||
|
||||
def messages(thread_id:, guardian:, **params)
|
||||
with_service(
|
||||
Chat::ListChannelThreadMessages,
|
||||
thread_id: thread_id,
|
||||
guardian: guardian,
|
||||
**params,
|
||||
direction: "future",
|
||||
) do
|
||||
on_success { result.messages }
|
||||
on_failed_policy(:can_view_thread) { raise "Guardian can't view thread" }
|
||||
on_failed_policy(:target_message_exists) { raise "Target message doesn't exist" }
|
||||
on_failed_policy(:ensure_thread_enabled) do
|
||||
raise "Threading is not enabled for this channel"
|
||||
end
|
||||
on_failure do
|
||||
p Chat::StepsInspector.new(result)
|
||||
raise "Unexpected error"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update(**params)
|
||||
with_service(Chat::UpdateThread, **params) do
|
||||
on_model_not_found(:channel) do
|
||||
raise "Couldn’t find channel with id: `#{params[:channel_id]}`"
|
||||
end
|
||||
on_model_not_found(:thread) do
|
||||
raise "Couldn’t find thread with id: `#{params[:thread_id]}`"
|
||||
end
|
||||
on_failed_policy(:can_view_channel) { raise "Guardian can't view channel" }
|
||||
on_failed_policy(:can_edit_thread) { raise "Guardian can't edit thread" }
|
||||
on_failed_policy(:threading_enabled_for_channel) do
|
||||
raise "Threading is not enabled for this channel"
|
||||
end
|
||||
on_failed_contract { |contract| raise contract.errors.full_messages.join(", ") }
|
||||
on_success { result.thread_instance }
|
||||
on_failure do
|
||||
p Chat::StepsInspector.new(result)
|
||||
raise "Unexpected error"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user