DEV: Provide user input to services using params key

Currently in services, we don’t make a distinction between input
parameters, options and dependencies.

This can lead to user input modifying the service behavior, whereas it
was not the developer intention.

This patch addresses the issue by changing how data is provided to
services:
- `params` is now used to hold all data coming from outside (typically
  user input from a controller) and a contract will take its values from
  `params`.
- `options` is a new key to provide options to a service. This typically
  allows changing a service behavior at runtime. It is, of course,
  totally optional.
- `dependencies` is actually anything else provided to the service (like
  `guardian`) and available directly from the context object.

The `service_params` helper in controllers has been updated to reflect
those changes, so most of the existing services didn’t need specific
changes.

The options block has the same DSL as contracts, as it’s also based on
`ActiveModel`. There aren’t any validations, though. Here’s an example:
```ruby
options do
  attribute :allow_changing_hidden, :boolean, default: false
end
```
And here’s an example of how to call a service with the new keys:
```ruby
MyService.call(params: { key1: value1, … }, options: { my_option: true }, guardian:, …)
```
This commit is contained in:
Loïc Guitaut
2024-10-18 17:45:47 +02:00
committed by Loïc Guitaut
parent a89767913d
commit 41584ab40c
115 changed files with 1152 additions and 895 deletions

View File

@ -67,8 +67,7 @@ class Chat::Api::ChannelMessagesController < Chat::ApiController
def create
Chat::MessageRateLimiter.run!(current_user)
# users can't force a thread through JSON API
Chat::CreateMessage.call(service_params.merge(force_thread: false)) do
Chat::CreateMessage.call(service_params) do
on_success do |message_instance:|
render json: success_json.merge(message_id: message_instance.id)
end

View File

@ -55,7 +55,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
# at the moment. This may change in future, at which point we will need to pass in
# a chatable_type param as well and switch to the correct service here.
Chat::CreateCategoryChannel.call(
service_params.merge(channel_params.merge(category_id: channel_params[:chatable_id])),
service_params.merge(params: channel_params.merge(category_id: channel_params[:chatable_id])),
) do
on_success do |channel:, membership:|
render_serialized(channel, Chat::ChannelSerializer, root: "channel", membership:)
@ -95,7 +95,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
auto_join_limiter(channel_from_params).performed!
end
Chat::UpdateChannel.call(service_params.merge(params_to_edit)) do
Chat::UpdateChannel.call(service_params.deep_merge(params: params_to_edit.to_unsafe_h)) do
on_success do |channel:|
render_serialized(
channel,

View File

@ -56,12 +56,12 @@ module Chat
webhook.chat_channel.add(Discourse.system_user)
Chat::CreateMessage.call(
service_params.merge(
params: {
chat_channel_id: webhook.chat_channel_id,
guardian: Discourse.system_user.guardian,
message: text,
incoming_chat_webhook: webhook,
),
},
guardian: Discourse.system_user.guardian,
incoming_chat_webhook: webhook,
) do
on_success { render json: success_json }
on_failure { render(json: failed_json, status: 422) }

View File

@ -3,8 +3,8 @@
module Jobs
module Chat
class AutoJoinChannelBatch < ::Jobs::Base
def execute(*)
::Chat::AutoJoinChannelBatch.call(*) do
def execute(args)
::Chat::AutoJoinChannelBatch.call(params: args) do
on_failure { Rails.logger.error("Failed with unexpected error") }
on_failed_contract do |contract|
Rails.logger.error(contract.errors.full_messages.join(", "))

View File

@ -4,7 +4,7 @@ module Jobs
module Chat
class AutoRemoveMembershipHandleCategoryUpdated < ::Jobs::Base
def execute(args)
::Chat::AutoRemove::HandleCategoryUpdated.call(**args)
::Chat::AutoRemove::HandleCategoryUpdated.call(params: args)
end
end
end

View File

@ -4,7 +4,7 @@ module Jobs
module Chat
class AutoRemoveMembershipHandleChatAllowedGroupsChange < ::Jobs::Base
def execute(args)
::Chat::AutoRemove::HandleChatAllowedGroupsChange.call(**args)
::Chat::AutoRemove::HandleChatAllowedGroupsChange.call(params: args)
end
end
end

View File

@ -4,7 +4,7 @@ module Jobs
module Chat
class AutoRemoveMembershipHandleDestroyedGroup < ::Jobs::Base
def execute(args)
::Chat::AutoRemove::HandleDestroyedGroup.call(**args)
::Chat::AutoRemove::HandleDestroyedGroup.call(params: args)
end
end
end

View File

@ -4,7 +4,7 @@ module Jobs
module Chat
class AutoRemoveMembershipHandleUserRemovedFromGroup < ::Jobs::Base
def execute(args)
::Chat::AutoRemove::HandleUserRemovedFromGroup.call(**args)
::Chat::AutoRemove::HandleUserRemovedFromGroup.call(params: args)
end
end
end

View File

@ -8,19 +8,21 @@ module Chat
# @example
# ::Chat::AddUsersToChannel.call(
# guardian: guardian,
# channel_id: 1,
# usernames: ["bob", "alice"]
# params: {
# channel_id: 1,
# usernames: ["bob", "alice"],
# }
# )
#
class AddUsersToChannel
include Service::Base
# @!method call(guardian:, **params_to_create)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] id of the channel
# @param [Hash] params_to_create
# @option params_to_create [Array<String>] usernames
# @option params_to_create [Array<String>] groups
# @param [Hash] params
# @option params [Integer] :channel_id ID of the channel
# @option params [Array<String>] :usernames
# @option params [Array<String>] :groups
# @return [Service::Base::Context]
contract do
attribute :usernames, :array
@ -123,14 +125,16 @@ module Chat
::Chat::CreateMessage.call(
guardian: Discourse.system_user.guardian,
chat_channel_id: channel.id,
message:
I18n.t(
"chat.channel.users_invited_to_channel",
invited_users: added_users.map { |u| "@#{u.username}" }.join(", "),
inviting_user: "@#{guardian.user.username}",
count: added_users.count,
),
params: {
chat_channel_id: channel.id,
message:
I18n.t(
"chat.channel.users_invited_to_channel",
invited_users: added_users.map { |u| "@#{u.username}" }.join(", "),
inviting_user: "@#{guardian.user.username}",
count: added_users.count,
),
},
) { on_failure { fail!(failure: "Failed to notice the channel") } }
end
end

View File

@ -6,9 +6,11 @@ module Chat
#
# @example
# Chat::AutoJoinChannelBatch.call(
# channel_id: 1,
# start_user_id: 27,
# end_user_id: 58,
# params: {
# channel_id: 1,
# start_user_id: 27,
# end_user_id: 58,
# }
# )
#
class AutoJoinChannelBatch

View File

@ -6,25 +6,27 @@ module Chat
# @example
# Service::Chat::CreateCategoryChannel.call(
# guardian: guardian,
# name: "SuperChannel",
# description: "This is the best channel",
# slug: "super-channel",
# category_id: category.id,
# threading_enabled: true,
# params: {
# name: "SuperChannel",
# description: "This is the best channel",
# slug: "super-channel",
# category_id: category.id,
# threading_enabled: true,
# }
# )
#
class CreateCategoryChannel
include Service::Base
# @!method call(guardian:, **params_to_create)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params_to_create
# @option params_to_create [String] name
# @option params_to_create [String] description
# @option params_to_create [String] slug
# @option params_to_create [Boolean] auto_join_users
# @option params_to_create [Integer] category_id
# @option params_to_create [Boolean] threading_enabled
# @param [Hash] params
# @option params [String] :name
# @option params [String] :description
# @option params [String] :slug
# @option params [Boolean] :auto_join_users
# @option params [Integer] :category_id
# @option params [Boolean] :threading_enabled
# @return [Service::Base::Context]
policy :public_channels_enabled

View File

@ -9,18 +9,20 @@ module Chat
# @example
# ::Chat::CreateDirectMessageChannel.call(
# guardian: guardian,
# target_usernames: ["bob", "alice"]
# params: {
# target_usernames: ["bob", "alice"],
# },
# )
#
class CreateDirectMessageChannel
include Service::Base
# @!method call(guardian:, **params_to_create)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params_to_create
# @option params_to_create [Array<String>] target_usernames
# @option params_to_create [Array<String>] target_groups
# @option params_to_create [Boolean] upsert
# @param [Hash] params
# @option params [Array<String>] :target_usernames
# @option params [Array<String>] :target_groups
# @option params [Boolean] :upsert
# @return [Service::Base::Context]
contract do

View File

@ -4,22 +4,34 @@ module Chat
# Service responsible for creating a new message.
#
# @example
# Chat::CreateMessage.call(chat_channel_id: 2, guardian: guardian, message: "A new message")
# Chat::CreateMessage.call(params: { chat_channel_id: 2, message: "A new message" }, guardian: guardian)
#
class CreateMessage
include Service::Base
# @!method call(chat_channel_id:, guardian:, in_reply_to_id:, message:, staged_id:, upload_ids:, thread_id:, incoming_chat_webhook:)
# @!method self.call(guardian:, params:, options:)
# @param guardian [Guardian]
# @param chat_channel_id [Integer]
# @param message [String]
# @param in_reply_to_id [Integer] ID of a message to reply to
# @param thread_id [Integer] ID of a thread to reply to
# @param upload_ids [Array<Integer>] IDs of uploaded documents
# @param context_topic_id [Integer] ID of the currently visible topic in drawer mode
# @param context_post_ids [Array<Integer>] IDs of the currently visible posts in drawer mode
# @param staged_id [String] arbitrary string that will be sent back to the client
# @param incoming_chat_webhook [Chat::IncomingWebhook]
# @param [Hash] params
# @option params [Integer] :chat_channel_id
# @option params [String] :message
# @option params [Integer] :in_reply_to_id ID of a message to reply to
# @option params [Integer] :thread_id ID of a thread to reply to
# @option params [Array<Integer>] :upload_ids IDs of uploaded documents
# @option params [Integer] :context_topic_id ID of the currently visible topic in drawer mode
# @option params [Array<Integer>] :context_post_ids IDs of the currently visible posts in drawer mode
# @option params [String] :staged_id arbitrary string that will be sent back to the client
# @param [Hash] options
# @option options [Chat::IncomingWebhook] :incoming_chat_webhook
# @return [Service::Base::Context]
options do
attribute :streaming, :boolean, default: false
attribute :enforce_membership, :boolean, default: false
attribute :process_inline, :boolean, default: -> { Rails.env.test? }
attribute :force_thread, :boolean, default: false
attribute :strip_whitespaces, :boolean, default: true
attribute :created_by_sdk, :boolean, default: false
end
policy :no_silenced_user
contract do
@ -31,13 +43,6 @@ module Chat
attribute :staged_id, :string
attribute :upload_ids, :array
attribute :thread_id, :string
attribute :streaming, :boolean, default: false
attribute :enforce_membership, :boolean, default: false
attribute :incoming_chat_webhook
attribute :process_inline, :boolean, default: Rails.env.test?
attribute :force_thread, :boolean, default: false
attribute :strip_whitespaces, :boolean, default: true
attribute :created_by_sdk, :boolean, default: false
validates :chat_channel_id, presence: true
validates :message, presence: true, if: -> { upload_ids.blank? }
@ -79,8 +84,8 @@ module Chat
Chat::Channel.find_by_id_or_slug(contract.chat_channel_id)
end
def enforce_membership(guardian:, channel:, contract:)
if guardian.user.bot? || contract.enforce_membership
def enforce_membership(guardian:, channel:, options:)
if guardian.user.bot? || options.enforce_membership
channel.add(guardian.user)
if channel.direct_message_channel?
@ -102,7 +107,7 @@ module Chat
reply&.chat_channel == channel
end
def fetch_thread(contract:, reply:, channel:)
def fetch_thread(contract:, reply:, channel:, options:)
return Chat::Thread.find_by(id: contract.thread_id) if contract.thread_id.present?
return unless reply
reply.thread ||
@ -110,7 +115,7 @@ module Chat
original_message: reply,
original_message_user: reply.user,
channel: channel,
force: contract.force_thread,
force: options.force_thread,
)
end
@ -129,16 +134,16 @@ module Chat
guardian.user.uploads.where(id: contract.upload_ids)
end
def clean_message(contract:)
def clean_message(contract:, options:)
contract.message =
TextCleaner.clean(
contract.message,
strip_whitespaces: contract.strip_whitespaces,
strip_whitespaces: options.strip_whitespaces,
strip_zero_width_spaces: true,
)
end
def instantiate_message(channel:, guardian:, contract:, uploads:, thread:, reply:)
def instantiate_message(channel:, guardian:, contract:, uploads:, thread:, reply:, options:)
channel.chat_messages.new(
user: guardian.user,
last_editor: guardian.user,
@ -148,7 +153,7 @@ module Chat
thread: thread,
cooked: ::Chat::Message.cook(contract.message, user_id: guardian.user.id),
cooked_version: ::Chat::Message::BAKED_VERSION,
streaming: contract.streaming,
streaming: options.streaming,
)
end
@ -169,10 +174,10 @@ module Chat
thread.add(thread.original_message_user)
end
def create_webhook_event(contract:, message_instance:)
return if contract.incoming_chat_webhook.blank?
def create_webhook_event(message_instance:)
return if context[:incoming_chat_webhook].blank?
message_instance.create_chat_webhook_event(
incoming_chat_webhook: contract.incoming_chat_webhook,
incoming_chat_webhook: context[:incoming_chat_webhook],
)
end
@ -186,8 +191,8 @@ module Chat
membership.update!(last_read_message: message_instance)
end
def update_created_by_sdk(message_instance:, contract:)
message_instance.created_by_sdk = contract.created_by_sdk
def update_created_by_sdk(message_instance:, options:)
message_instance.created_by_sdk = options.created_by_sdk
end
def process_direct_message_channel(membership:)
@ -200,7 +205,7 @@ module Chat
Chat::Publisher.publish_thread_created!(channel, reply, thread.id)
end
def process(channel:, message_instance:, contract:, thread:)
def process(channel:, message_instance:, contract:, thread:, options:)
::Chat::Publisher.publish_new!(channel, message_instance, contract.staged_id)
DiscourseEvent.trigger(
@ -218,7 +223,7 @@ module Chat
},
)
if contract.process_inline
if options.process_inline
Jobs::Chat::ProcessMessage.new.execute(
{ chat_message_id: message_instance.id, staged_id: contract.staged_id },
)

View File

@ -4,16 +4,17 @@ module Chat
# Creates a thread.
#
# @example
# Chat::CreateThread.call(channel_id: 2, original_message_id: 3, guardian: guardian, title: "Restaurant for Saturday")
# Chat::CreateThread.call(guardian: guardian, params: { channel_id: 2, original_message_id: 3, title: "Restaurant for Saturday" })
#
class CreateThread
include Service::Base
# @!method call(thread_id:, channel_id:, guardian:, **params_to_create)
# @param [Integer] original_message_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @option params_to_create [String,nil] title
# @param [Hash] params
# @option params [Integer] :original_message_id
# @option params [Integer] :channel_id
# @option params [String,nil] :title
# @return [Service::Base::Context]
contract do

View File

@ -6,26 +6,27 @@ module Chat
# @example
# ::Chat::FlagMessage.call(
# guardian: guardian,
# channel_id: 1,
# message_id: 43,
# params: {
# channel_id: 1,
# message_id: 43,
# }
# )
#
class FlagMessage
include Service::Base
# @!method call(guardian:, channel_id:, data:)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] channel_id of the channel
# @param [Integer] message_id of the message
# @param [Integer] flag_type_id - Type of flag to create
# @param [String] optional message - Used when the flag type is notify_user or notify_moderators and we have to create
# a separate PM.
# @param [Boolean] optional is_warning - Staff can send warnings when using the notify_user flag.
# @param [Boolean] optional take_action - Automatically approves the created reviewable and deletes the chat message.
# @param [Boolean] optional queue_for_review - Adds a special reason to the reviewable score and creates the reviewable using
# the force_review option.
# @param [Hash] params
# @option params [Integer] :channel_id of the channel
# @option params [Integer] :message_id of the message
# @option params [Integer] :flag_type_id Type of flag to create
# @option params [String] :message (optional) Used when the flag type is notify_user or notify_moderators and we have to create a separate PM.
# @option params [Boolean] :is_warning (optional) Staff can send warnings when using the notify_user flag.
# @option params [Boolean] :take_action (optional) Automatically approves the created reviewable and deletes the chat message.
# @option params [Boolean] :queue_for_review (optional) Adds a special reason to the reviewable score and creates the reviewable using the force_review option.
# @return [Service::Base::Context]
contract do
attribute :message_id, :integer
attribute :channel_id, :integer

View File

@ -4,16 +4,17 @@ module Chat
# Invites users to a channel.
#
# @example
# Chat::InviteUsersToChannel.call(channel_id: 2, user_ids: [2, 43], guardian: guardian, **optional_params)
# Chat::InviteUsersToChannel.call(params: { channel_id: 2, user_ids: [2, 43] }, guardian: guardian)
#
class InviteUsersToChannel
include Service::Base
# @!method call(user_ids:, channel_id:, guardian:)
# @param [Array<Integer>] user_ids
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @option optional_params [Integer, nil] message_id
# @param [Hash] params
# @option params [Array<Integer>] :user_ids
# @option params [Integer] :channel_id
# @option params [Integer, nil] :message_id
# @return [Service::Base::Context]
contract do

View File

@ -6,17 +6,20 @@ module Chat
# @example
# ::Chat::LeaveChannel.call(
# guardian: guardian,
# channel_id: 1,
# params: {
# channel_id: 1,
# }
# )
#
class LeaveChannel
include Service::Base
# @!method call(guardian:, channel_id:,)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] channel_id of the channel
# @param [Hash] params
# @option params [Integer] :channel_id ID of the channel
# @return [Service::Base::Context]
contract do
attribute :channel_id, :integer

View File

@ -5,14 +5,15 @@ module Chat
# or fetching paginated messages from last read.
#
# @example
# Chat::ListChannelMessages.call(channel_id: 2, guardian: guardian, **optional_params)
# Chat::ListChannelMessages.call(params: { channel_id: 2, **optional_params }, guardian: guardian)
#
class ListChannelMessages
include Service::Base
# @!method call(guardian:)
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -5,14 +5,15 @@ module Chat
# or fetching paginated messages from last read.
#
# @example
# Chat::ListThreadMessages.call(thread_id: 2, guardian: guardian, **optional_params)
# Chat::ListThreadMessages.call(params: { thread_id: 2, **optional_params }, guardian: guardian)
#
class ListChannelThreadMessages
include Service::Base
# @!method call(guardian:)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @option optional_params [Integer] thread_id
# @param [Hash] params
# @option params [Integer] :thread_id
# @return [Service::Base::Context]
contract do

View File

@ -4,12 +4,12 @@ module Chat
# List of the channels a user is tracking
#
# @example
# Chat::ListUserChannels.call(guardian: guardian, **optional_params)
# Chat::ListUserChannels.call(guardian:)
#
class ListUserChannels
include Service::Base
# @!method call(guardian:)
# @!method self.call(guardian:)
# @param [Guardian] guardian
# @return [Service::Base::Context]

View File

@ -10,18 +10,19 @@ module Chat
# of normal or tracking will be returned.
#
# @example
# Chat::LookupChannelThreads.call(channel_id: 2, guardian: guardian, limit: 5, offset: 2)
# Chat::LookupChannelThreads.call(params: { channel_id: 2, limit: 5, offset: 2 }, guardian: guardian)
#
class LookupChannelThreads
include Service::Base
THREADS_LIMIT = 10
# @!method call(channel_id:, guardian:, limit: nil, offset: nil)
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] limit
# @param [Integer] offset
# @param [Hash] params
# @option params [Integer] :channel_id
# @option params [Integer] :limit
# @option params [Integer] :offset
# @return [Service::Base::Context]
contract do

View File

@ -5,15 +5,16 @@ module Chat
# match, and the channel must specifically have threading enabled.
#
# @example
# Chat::LookupThread.call(thread_id: 88, channel_id: 2, guardian: guardian)
# Chat::LookupThread.call(params: { thread_id: 88, channel_id: 2 }, guardian: guardian)
#
class LookupThread
include Service::Base
# @!method call(thread_id:, channel_id:, guardian:)
# @param [Integer] thread_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :thread_id
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -7,17 +7,18 @@ module Chat
# of normal or tracking will be returned.
#
# @example
# Chat::LookupUserThreads.call(guardian: guardian, limit: 5, offset: 2)
# Chat::LookupUserThreads.call(guardian: guardian, params: { limit: 5, offset: 2 })
#
class LookupUserThreads
include Service::Base
THREADS_LIMIT = 10
# @!method call(guardian:, limit: nil, offset: nil)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] limit
# @param [Integer] offset
# @param [Hash] params
# @option params [Integer] :limit
# @option params [Integer] :offset
# @return [Service::Base::Context]
contract do

View File

@ -10,7 +10,7 @@ module Chat
class MarkAllUserChannelsRead
include ::Service::Base
# @!method call(guardian:)
# @!method self.call(guardian:)
# @param [Guardian] guardian
# @return [Service::Base::Context]

View File

@ -7,18 +7,21 @@ module Chat
#
# @example
# Chat::MarkThreadTitlePromptSeen.call(
# thread_id: 88,
# channel_id: 2,
# params: {
# thread_id: 88,
# channel_id: 2,
# },
# guardian: guardian,
# )
#
class MarkThreadTitlePromptSeen
include Service::Base
# @!method call(thread_id:, channel_id:, guardian:)
# @param [Integer] thread_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :thread_id
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -322,8 +322,10 @@ module Chat
tracking_data =
Chat::TrackingState.call(
guardian: Guardian.new(user),
channel_ids: channel_last_read_map.keys,
include_missing_memberships: true,
params: {
channel_ids: channel_last_read_map.keys,
include_missing_memberships: true,
},
)
if tracking_data.failure?
raise StandardError,

View File

@ -6,15 +6,16 @@ module Chat
# updated.
#
# @example
# Chat::RestoreMessage.call(message_id: 2, channel_id: 1, guardian: guardian)
# Chat::RestoreMessage.call(params: { message_id: 2, channel_id: 1 }, guardian: guardian)
#
class RestoreMessage
include Service::Base
# @!method call(message_id:, channel_id:, guardian:)
# @param [Integer] message_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :message_id
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -4,16 +4,17 @@ module Chat
# Returns a list of chatables (users, groups ,category channels, direct message channels) that can be chatted with.
#
# @example
# Chat::SearchChatable.call(term: "@bob", guardian: guardian)
# Chat::SearchChatable.call(params: { term: "@bob" }, guardian: guardian)
#
class SearchChatable
include Service::Base
SEARCH_RESULT_LIMIT ||= 10
# @!method call(term:, guardian:)
# @param [String] term
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [String] :term
# @return [Service::Base::Context]
contract do

View File

@ -4,14 +4,15 @@ module Chat
# Service responsible for stopping streaming of a message.
#
# @example
# Chat::StopMessageStreaming.call(message_id: 3, guardian: guardian)
# Chat::StopMessageStreaming.call(params: { message_id: 3 }, guardian: guardian)
#
class StopMessageStreaming
include ::Service::Base
# @!method call(message_id:, guardian:)
# @param [Integer] message_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :message_id
# @return [Service::Base::Context]
contract do
attribute :message_id, :integer

View File

@ -22,15 +22,16 @@ module Chat
# Only channels with threads enabled will return thread tracking state.
#
# @example
# Chat::TrackingState.call(channel_ids: [2, 3], thread_ids: [6, 7], guardian: guardian)
# Chat::TrackingState.call(params: { channel_ids: [2, 3], thread_ids: [6, 7] }, guardian: guardian)
#
class TrackingState
include Service::Base
# @!method call(thread_ids:, channel_ids:, guardian:)
# @param [Integer] thread_ids
# @param [Integer] channel_ids
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :thread_ids
# @option params [Integer] :channel_ids
# @return [Service::Base::Context]
contract do

View File

@ -5,14 +5,15 @@ module Chat
# Note the slug is modified to prevent collisions.
#
# @example
# Chat::TrashChannel.call(channel_id: 2, guardian: guardian)
# Chat::TrashChannel.call(params: { channel_id: 2 }, guardian: guardian)
#
class TrashChannel
include Service::Base
# @!method call(channel_id:, guardian:)
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
DELETE_CHANNEL_LOG_KEY = "chat_channel_delete"
@ -28,8 +29,8 @@ module Chat
private
def fetch_channel(channel_id:)
Chat::Channel.find_by(id: channel_id)
def fetch_channel(params:)
Chat::Channel.find_by(id: params[:channel_id])
end
def invalid_access(guardian:, channel:)

View File

@ -6,15 +6,16 @@ module Chat
# updated.
#
# @example
# Chat::TrashMessage.call(message_id: 2, channel_id: 1, guardian: guardian)
# Chat::TrashMessage.call(params: { message_id: 2, channel_id: 1 }, guardian: guardian)
#
class TrashMessage
include Service::Base
# @!method call(message_id:, channel_id:, guardian:)
# @param [Integer] message_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :message_id
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -6,15 +6,16 @@ module Chat
# is updated.
#
# @example
# Chat::TrashMessages.call(message_ids: [2, 3], channel_id: 1, guardian: guardian)
# Chat::TrashMessages.call(params: { message_ids: [2, 3], channel_id: 1 }, guardian: guardian)
#
class TrashMessages
include Service::Base
# @!method call(message_ids:, channel_id:, guardian:)
# @param [Array<Integer>] message_ids
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Array<Integer>] :message_ids
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -6,17 +6,20 @@ module Chat
# @example
# ::Chat::UnfollowChannel.call(
# guardian: guardian,
# channel_id: 1,
# params: {
# channel_id: 1,
# }
# )
#
class UnfollowChannel
include Service::Base
# @!method call(guardian:, channel_id:,)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] channel_id of the channel
# @param [Hash] params
# @option params [Integer] :channel_id ID of the channel
# @return [Service::Base::Context]
contract do
attribute :channel_id, :integer

View File

@ -8,28 +8,30 @@ module Chat
#
# @example
# ::Chat::UpdateChannel.call(
# channel_id: 2,
# guardian: guardian,
# name: "SuperChannel",
# description: "This is the best channel",
# slug: "super-channel",
# threading_enabled: true,
# params:{
# channel_id: 2,
# name: "SuperChannel",
# description: "This is the best channel",
# slug: "super-channel",
# threading_enabled: true
# },
# )
#
class UpdateChannel
include Service::Base
# @!method call(channel_id:, guardian:, **params_to_edit)
# @param [Integer] channel_id
# @!method self.call(params:, guardian:)
# @param [Guardian] guardian
# @param [Hash] params_to_edit
# @option params_to_edit [String,nil] name
# @option params_to_edit [String,nil] description
# @option params_to_edit [String,nil] slug
# @option params_to_edit [Boolean] threading_enabled
# @option params_to_edit [Boolean] auto_join_users Only valid for {CategoryChannel}. Whether active users
# with permission to see the category should automatically join the channel.
# @option params_to_edit [Boolean] allow_channel_wide_mentions Allow the use of @here and @all in the channel.
# @param [Hash] params
# @option params [Integer] :channel_id The channel ID
# @option params [String,nil] :name
# @option params [String,nil] :description
# @option params [String,nil] :slug
# @option params [Boolean] :threading_enabled
# @option params [Boolean] :auto_join_users Only valid for {CategoryChannel}. Whether active users with permission to see the category should automatically join the channel.
# @option params [Boolean] :allow_channel_wide_mentions Allow the use of @here and @all in the channel.
# @return [Service::Base::Context]
model :channel
@ -56,8 +58,8 @@ module Chat
private
def fetch_channel(channel_id:)
Chat::Channel.find_by(id: channel_id)
def fetch_channel(params:)
Chat::Channel.find_by(id: params[:channel_id])
end
def check_channel_permission(guardian:, channel:)

View File

@ -4,15 +4,16 @@ module Chat
# Service responsible for updating a chat channel status.
#
# @example
# Chat::UpdateChannelStatus.call(channel_id: 2, guardian: guardian, status: "open")
# Chat::UpdateChannelStatus.call(guardian: guardian, params: { status: "open", channel_id: 2 })
#
class UpdateChannelStatus
include Service::Base
# @!method call(channel_id:, guardian:, status:)
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [String] status
# @param [Hash] params
# @option params [Integer] :channel_id
# @option params [String] :status
# @return [Service::Base::Context]
model :channel, :fetch_channel
@ -26,8 +27,8 @@ module Chat
private
def fetch_channel(channel_id:)
Chat::Channel.find_by(id: channel_id)
def fetch_channel(params:)
Chat::Channel.find_by(id: params[:channel_id])
end
def check_channel_permission(guardian:, channel:, contract:)

View File

@ -4,24 +4,32 @@ module Chat
# Service responsible for updating a message.
#
# @example
# Chat::UpdateMessage.call(message_id: 2, guardian: guardian, message: "A new message")
# Chat::UpdateMessage.call(guardian: guardian, params: { message: "A new message", message_id: 2 })
#
class UpdateMessage
include Service::Base
# @!method call(message_id:, guardian:, message:, upload_ids:)
# @!method self.call(guardian:, params:, options:)
# @param guardian [Guardian]
# @param message_id [Integer]
# @param message [String]
# @param upload_ids [Array<Integer>] IDs of uploaded documents
# @param [Hash] params
# @option params [Integer] :message_id
# @option params [String] :message
# @option params [Array<Integer>] :upload_ids IDs of uploaded documents
# @param [Hash] options
# @option options [Boolean] (true) :strip_whitespaces
# @option options [Boolean] :process_inline
# @return [Service::Base::Context]
options do
attribute :strip_whitespaces, :boolean, default: true
attribute :process_inline, :boolean, default: -> { Rails.env.test? }
end
contract do
attribute :message_id, :string
attribute :message, :string
attribute :upload_ids, :array
attribute :streaming, :boolean, default: false
attribute :strip_whitespaces, :boolean, default: true
attribute :process_inline, :boolean, default: Rails.env.test?
validates :message_id, presence: true
validates :message, presence: true, if: -> { upload_ids.blank? }
@ -82,12 +90,12 @@ module Chat
guardian.can_edit_chat?(message)
end
def clean_message(contract:)
def clean_message(contract:, options:)
contract.message =
TextCleaner.clean(
contract.message,
strip_whitespaces: contract.strip_whitespaces,
strip_zero_width_spaces: true,
strip_whitespaces: options.strip_whitespaces,
)
end
@ -149,14 +157,14 @@ module Chat
chars_edited > max_edited_chars
end
def publish(message:, guardian:, contract:)
def publish(message:, guardian:, contract:, options:)
edit_timestamp = context[:revision]&.created_at&.iso8601(6) || Time.zone.now.iso8601(6)
::Chat::Publisher.publish_edit!(message.chat_channel, message)
DiscourseEvent.trigger(:chat_message_edited, message, message.chat_channel, message.user)
if contract.process_inline
if options.process_inline
Jobs::Chat::ProcessMessage.new.execute(
{ chat_message_id: message.id, edit_timestamp: edit_timestamp },
)

View File

@ -7,16 +7,16 @@ module Chat
# Only the thread title can be updated.
#
# @example
# Chat::UpdateThread.call(thread_id: 88, guardian: guardian, title: "Restaurant for Saturday")
# Chat::UpdateThread.call(guardian: guardian, params: { thread_id: 88, title: "Restaurant for Saturday" })
#
class UpdateThread
include Service::Base
# @!method call(thread_id:, channel_id:, guardian:, **params_to_edit)
# @param [Integer] thread_id
# @param [Integer] channel_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @option params_to_edit [String,nil] title
# @param [Hash] params
# @option params [Integer] :thread_id
# @option params [Integer] :channel_id
# @return [Service::Base::Context]
contract do

View File

@ -7,20 +7,23 @@ module Chat
#
# @example
# Chat::UpdateThreadNotificationSettings.call(
# thread_id: 88,
# channel_id: 2,
# params: {
# thread_id: 88,
# channel_id: 2,
# notification_level: notification_level,
# },
# guardian: guardian,
# notification_level: notification_level,
# )
#
class UpdateThreadNotificationSettings
include Service::Base
# @!method call(thread_id:, channel_id:, guardian:, notification_level:)
# @param [Integer] thread_id
# @param [Integer] channel_id
# @param [Integer] notification_level
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :thread_id
# @option params [Integer] :channel_id
# @option params [Integer] :notification_level
# @return [Service::Base::Context]
contract do

View File

@ -4,15 +4,16 @@ module Chat
# Service responsible for updating the last read message id of a membership.
#
# @example
# Chat::UpdateUserChannelLastRead.call(channel_id: 2, message_id: 3, guardian: guardian)
# Chat::UpdateUserChannelLastRead.call(params: { channel_id: 2, message_id: 3 }, guardian: guardian)
#
class UpdateUserChannelLastRead
include ::Service::Base
# @!method call(channel_id:, message_id:, guardian:)
# @param [Integer] channel_id
# @param [Integer] message_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :channel_id
# @option params [Integer] :message_id
# @return [Service::Base::Context]
contract do

View File

@ -5,16 +5,17 @@ module Chat
# as read.
#
# @example
# Chat::UpdateUserThreadLastRead.call(channel_id: 2, thread_id: 3, message_id: 4, guardian: guardian)
# Chat::UpdateUserThreadLastRead.call(params: { channel_id: 2, thread_id: 3, message_id: 4 }, guardian: guardian)
#
class UpdateUserThreadLastRead
include ::Service::Base
# @!method call(channel_id:, thread_id:, guardian:)
# @param [Integer] channel_id
# @param [Integer] thread_id
# @param [Integer] message_id
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Hash] params
# @option params [Integer] :channel_id
# @option params [Integer] :thread_id
# @option params [Integer] :message_id
# @return [Service::Base::Context]
contract do

View File

@ -6,20 +6,24 @@ module Chat
# @example
# ::Chat::UpsertDraft.call(
# guardian: guardian,
# channel_id: 1,
# thread_id: 1,
# data: { message: "foo" }
# params: {
# channel_id: 1,
# thread_id: 1,
# data: { message: "foo" }
# }
# )
#
class UpsertDraft
include Service::Base
# @!method call(guardian:, channel_id:, thread_id:, data:)
# @!method self.call(guardian:, params:)
# @param [Guardian] guardian
# @param [Integer] channel_id of the channel
# @param [String] json object as string containing the data of the draft (message, uploads, replyToMsg and editing keys)
# @option [Integer] thread_id of the channel
# @param [Hash] params
# @option params [Integer] :channel_id ID of the channel
# @option params [String] :data JSON object as string containing the data of the draft (message, uploads, replyToMsg and editing keys)
# @option params [Integer] :thread_id ID of the thread
# @return [Service::Base::Context]
contract do
attribute :channel_id, :integer
validates :channel_id, presence: true