mirror of
https://github.com/discourse/discourse.git
synced 2026-08-14 13:58:53 +08:00
Discourse Workflows is a visual automation plugin for admins. A workflow is a versioned graph made of trigger, condition, action, and utility nodes. ## Architecture The plugin has three main responsibilities: - Store and version workflow graphs. - Render an admin editor from node metadata. - Execute published workflow versions and record their results. The graph is stored as workflow nodes plus connections. Each node has a stable type, a type version, editor position, configuration parameters, optional credential references, and direct runtime settings. Published workflows create immutable versions that executions can safely use even after the draft changes. ## Runtime Flow A normal execution follows this path: 1. A trigger produces initial data from a Discourse event, schedule, webhook, or manual run. 2. Trigger data is wrapped as workflow items. 3. The executor queues downstream nodes from the graph connections. 4. Each node receives input items and returns one array per output. 5. The executor records the step, stores node output for expressions, and routes each output array to connected downstream nodes. 6. The execution finishes, fails, or enters a waiting state. Waiting nodes persist enough execution context to resume later. Resume requests continue from the waiting node using the workflow snapshot saved with the execution, not the latest draft. Expressions are resolved at runtime against the current item, node parameters, workflow variables, previous node outputs, and execution context. The editor stores expression values in parameters; node code should read them through the execution context instead of parsing parameter values directly. ## Adding nodes - Workflow nodes, registered with `DiscoursePluginRegistry.register_discourse_workflows_node`. Core nodes live in `lib/discourse_workflows/nodes` and are registered during plugin initialization. Other plugins can add their own nodes when `DiscourseWorkflows` is loaded. ## Node API Nodes inherit from `DiscourseWorkflows::NodeType`. A node class is responsible for two things: - Declaring its contract with `description(...)`. - Implementing the runtime entry point for its node kind. The description is the public contract between the node, editor, validator, and runtime. It should include: - `name`: stable identifier, usually prefixed by `trigger:`, `action:`, `condition:`, or `flow:`. - `version`: implementation version used by stored workflow nodes. - `defaults`: editor metadata such as icon and color. - `group`: palette category. - `inputs` and `outputs`: graph ports. - `properties`: configuration fields rendered by the property engine. - `credentials`: credential slots the node can use. - `webhooks`: public or resume webhook declarations. - `events`: Discourse events that should activate a trigger node. - `capabilities`: feature flags such as manual triggering, waiting, or current user access. - `available`: optional availability gate for nodes backed by another plugin or site setting. Description data should stay declarative. Put business logic in the runtime entry point or helper classes. ### Action, Condition, And Flow Nodes Action-like nodes implement `execute(exec_ctx)`. They receive input items through the execution context and return positional output arrays. A one-output node returns one outer array; a branching node returns one inner array per branch. ### Trigger Nodes Trigger nodes start executions. Event triggers declare `events` and usually implement: - `valid?` to ignore events that should not run workflows. - `matches?(trigger_ctx)` to compare event data with node configuration. - `output` to produce the initial workflow item data. ### Webhook And Waiting Nodes Webhook trigger nodes declare webhook metadata and produce initial data from the incoming request. Waiting nodes pause an execution and resume it through a later interaction or webhook. ### Dynamic Options Nodes can provide dynamic property options with `self.load_options_context(context)`. Use the context object to access current parameters, filtered credentials, the search filter, the current user, guardian, and shared helpers. ### Errors And Logs Raise `DiscourseWorkflows::NodeError` for failures admins can act on, such as invalid configuration or missing Discourse records. Unexpected exceptions fail the current execution. --------- --------- Co-authored-by: Renato Atilio <3530+renato@users.noreply.github.com> Co-authored-by: Martin Brennan <martin@discourse.org> Co-authored-by: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com>
581 lines
20 KiB
Ruby
Vendored
581 lines
20 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
# name: chat
|
|
# about: Adds chat functionality to your site so it can natively support both long-form and short-form communication needs of your online community
|
|
# meta_topic_id: 230881
|
|
# version: 0.4
|
|
# authors: Kane York, Mark VanLandingham, Martin Brennan, Joffrey Jaffeux
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/chat
|
|
# meta_topic_id: 230881
|
|
|
|
enabled_site_setting :chat_enabled
|
|
|
|
register_asset "stylesheets/colors.scss", :color_definitions
|
|
register_asset "stylesheets/mixins/index.scss"
|
|
register_asset "stylesheets/common/index.scss"
|
|
register_asset "stylesheets/desktop/index.scss", :desktop
|
|
register_asset "stylesheets/mobile/index.scss", :mobile
|
|
|
|
register_service_worker "service-worker/chat-service-worker-extensions.js"
|
|
|
|
register_svg_icon "comment-slash"
|
|
register_svg_icon "comment-dots"
|
|
register_svg_icon "lock"
|
|
register_svg_icon "clipboard"
|
|
register_svg_icon "file-audio"
|
|
register_svg_icon "file-video"
|
|
register_svg_icon "file-image"
|
|
register_svg_icon "circle-stop"
|
|
register_svg_icon "filter"
|
|
register_svg_icon "filter-circle-xmark"
|
|
register_svg_icon "sort"
|
|
|
|
# route: /admin/plugins/chat
|
|
add_admin_route "chat.admin.title", "chat", use_new_show_route: true
|
|
|
|
GlobalSetting.add_default(:allow_unsecure_chat_uploads, false)
|
|
|
|
module ::Chat
|
|
PLUGIN_NAME = "chat"
|
|
RETENTION_SETTINGS_TO_USER_OPTION_FIELDS = {
|
|
chat_channel_retention_days: :dismissed_channel_retention_reminder,
|
|
chat_dm_retention_days: :dismissed_dm_retention_reminder,
|
|
}
|
|
PRESENCE_REGEXP = %r{^/chat-reply/(\d+)(?:/thread/(\d+))?$}
|
|
end
|
|
|
|
require_relative "lib/chat/engine"
|
|
after_initialize do
|
|
register_seedfu_fixtures(Rails.root.join("plugins/chat/db/fixtures"))
|
|
|
|
UserNotifications.append_view_path(File.expand_path("../app/views", __FILE__))
|
|
|
|
register_category_custom_field_type(Chat::HAS_CHAT_ENABLED, :boolean)
|
|
|
|
register_search_index(
|
|
enabled: -> { SiteSetting.chat_search_enabled },
|
|
model_class: Chat::Message,
|
|
search_data_class: Chat::MessageSearchData,
|
|
index_version: 1,
|
|
search_data:
|
|
proc do |message, indexer_helper|
|
|
{
|
|
a_weight: message.message,
|
|
d_weight: indexer_helper.scrub_html(message.cooked)[0..600_000],
|
|
}
|
|
end,
|
|
load_unindexed_record_ids:
|
|
proc do |limit:, index_version:|
|
|
Chat::Message
|
|
.joins("LEFT JOIN chat_message_search_data ON chat_message_id = chat_messages.id")
|
|
.where(
|
|
"chat_message_search_data.locale IS NULL OR chat_message_search_data.locale != ? OR chat_message_search_data.version != ?",
|
|
SiteSetting.default_locale,
|
|
index_version,
|
|
)
|
|
.order("chat_messages.id ASC")
|
|
.limit(limit)
|
|
.pluck(:id)
|
|
end,
|
|
)
|
|
|
|
register_user_custom_field_type(Chat::LAST_CHAT_CHANNEL_ID, :integer)
|
|
DiscoursePluginRegistry.serialized_current_user_fields << Chat::LAST_CHAT_CHANNEL_ID
|
|
DiscoursePluginRegistry.register_flag_applies_to_type("Chat::Message", self)
|
|
|
|
UserUpdater::OPTION_ATTR.push(:chat_enabled)
|
|
UserUpdater::OPTION_ATTR.push(:chat_quick_reaction_type)
|
|
UserUpdater::OPTION_ATTR.push(:chat_quick_reactions_custom)
|
|
UserUpdater::OPTION_ATTR.push(:only_chat_push_notifications)
|
|
UserUpdater::OPTION_ATTR.push(:chat_sound)
|
|
UserUpdater::OPTION_ATTR.push(:ignore_channel_wide_mention)
|
|
UserUpdater::OPTION_ATTR.push(:show_thread_title_prompts)
|
|
UserUpdater::OPTION_ATTR.push(:chat_email_frequency)
|
|
UserUpdater::OPTION_ATTR.push(:chat_header_indicator_preference)
|
|
UserUpdater::OPTION_ATTR.push(:chat_separate_sidebar_mode)
|
|
UserUpdater::OPTION_ATTR.push(:chat_send_shortcut)
|
|
|
|
register_reviewable_type Chat::ReviewableMessage
|
|
|
|
if respond_to?(:register_discourse_workflows_node)
|
|
register_discourse_workflows_node do
|
|
require_relative "lib/discourse_workflows/nodes/chat_channel_selection"
|
|
require_relative "lib/discourse_workflows/nodes/send_chat_message/v1"
|
|
require_relative "lib/discourse_workflows/nodes/chat_approval/v1"
|
|
|
|
[DiscourseWorkflows::Nodes::SendChatMessage::V1, DiscourseWorkflows::Nodes::ChatApproval::V1]
|
|
end
|
|
|
|
on(:chat_message_interaction) do |interaction|
|
|
next unless SiteSetting.discourse_workflows_enabled
|
|
|
|
action_id = interaction.action&.dig("action_id").to_s
|
|
next if action_id.blank?
|
|
unless DiscourseWorkflows::InteractiveResume.action_id?(
|
|
action_id,
|
|
expected_node_type: "action:chat_approval",
|
|
allowed_actions: %w[approve deny],
|
|
)
|
|
next
|
|
end
|
|
|
|
Jobs.enqueue(
|
|
Jobs::Chat::ResumeWorkflowApproval,
|
|
action_id: action_id,
|
|
channel_id: interaction.message.chat_channel_id,
|
|
)
|
|
end
|
|
end
|
|
|
|
reloadable_patch do |plugin|
|
|
Site.preloaded_category_custom_fields << Chat::HAS_CHAT_ENABLED
|
|
|
|
Guardian.prepend Chat::GuardianExtensions
|
|
UserNotifications.prepend Chat::UserNotificationsExtension
|
|
Notifications::ConsolidationPlan.prepend Chat::NotificationConsolidationExtension
|
|
UserOption.prepend Chat::UserOptionExtension
|
|
Category.prepend Chat::CategoryExtension
|
|
Reviewable.prepend Chat::ReviewableExtension
|
|
Bookmark.prepend Chat::BookmarkExtension
|
|
User.prepend Chat::UserExtension
|
|
Group.prepend Chat::GroupExtension
|
|
Plugin::Instance.prepend Chat::PluginInstanceExtension
|
|
Jobs::ExportCsvFile.prepend Chat::MessagesExporter
|
|
WebHook.prepend Chat::OutgoingWebHookExtension
|
|
end
|
|
|
|
Oneboxer.register_local_handler("chat/chat") do |url, route|
|
|
Chat::OneboxHandler.handle(url, route)
|
|
end
|
|
|
|
InlineOneboxer.register_local_handler("chat/chat") do |url, route|
|
|
Chat::InlineOneboxHandler.handle(url, route)
|
|
end
|
|
|
|
register_upload_in_use do |upload|
|
|
Chat::Message.where(
|
|
"message LIKE ? OR message LIKE ?",
|
|
"%#{upload.sha1}%",
|
|
"%#{upload.base62_sha1}%",
|
|
).exists? ||
|
|
Chat::Draft.where(
|
|
"data LIKE ? OR data LIKE ?",
|
|
"%#{upload.sha1}%",
|
|
"%#{upload.base62_sha1}%",
|
|
).exists?
|
|
end
|
|
|
|
add_to_serializer(:user_card, :can_chat_user) do
|
|
return false if !SiteSetting.chat_enabled
|
|
return false if scope.user.blank?
|
|
return false if !scope.user.user_option.chat_enabled || !object.user_option.chat_enabled
|
|
|
|
scope.can_direct_message? && Guardian.new(object).can_chat? &&
|
|
scope.recipient_allows_direct_messages?(object)
|
|
end
|
|
|
|
add_to_serializer(:hidden_profile, :can_chat_user) do
|
|
return false if !SiteSetting.chat_enabled
|
|
return false if scope.user.blank?
|
|
return false if !scope.user.user_option.chat_enabled || !object.user_option.chat_enabled
|
|
|
|
scope.can_direct_message? && Guardian.new(object).can_chat? &&
|
|
scope.recipient_allows_direct_messages?(object)
|
|
end
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:can_chat,
|
|
include_condition: -> do
|
|
return @can_chat if defined?(@can_chat)
|
|
@can_chat = SiteSetting.chat_enabled && scope.can_chat?
|
|
end,
|
|
) { true }
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:can_direct_message,
|
|
include_condition: -> do
|
|
return @can_direct_message if defined?(@can_direct_message)
|
|
@can_direct_message = include_has_chat_enabled? && scope.can_direct_message?
|
|
end,
|
|
) { true }
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:has_chat_enabled,
|
|
include_condition: -> do
|
|
return @has_chat_enabled if defined?(@has_chat_enabled)
|
|
@has_chat_enabled = include_can_chat? && object.user_option.chat_enabled
|
|
end,
|
|
) { true }
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:chat_sound,
|
|
include_condition: -> { include_has_chat_enabled? && object.user_option.chat_sound },
|
|
) { object.user_option.chat_sound }
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:needs_channel_retention_reminder,
|
|
include_condition: -> do
|
|
include_has_chat_enabled? && object.staff? &&
|
|
!object.user_option.dismissed_channel_retention_reminder &&
|
|
!SiteSetting.chat_channel_retention_days.zero?
|
|
end,
|
|
) { true }
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:needs_dm_retention_reminder,
|
|
include_condition: -> do
|
|
include_has_chat_enabled? && !object.user_option.dismissed_dm_retention_reminder &&
|
|
!SiteSetting.chat_dm_retention_days.zero?
|
|
end,
|
|
) { true }
|
|
|
|
add_to_serializer(:current_user, :has_joinable_public_channels) do
|
|
Chat::ChannelFetcher.secured_public_channel_search(
|
|
scope,
|
|
following: false,
|
|
limit: 1,
|
|
status: :open,
|
|
).exists?
|
|
end
|
|
|
|
add_to_serializer(
|
|
:current_user,
|
|
:chat_drafts,
|
|
include_condition: -> { include_has_chat_enabled? },
|
|
) do
|
|
Chat::Draft
|
|
.where(user_id: object.id)
|
|
.order(updated_at: :desc)
|
|
.limit(20)
|
|
.pluck(:chat_channel_id, :data, :thread_id)
|
|
.map { |row| { channel_id: row[0], data: row[1], thread_id: row[2] } }
|
|
end
|
|
|
|
add_to_serializer(
|
|
:user_notification_total,
|
|
:chat_notifications,
|
|
include_condition: -> do
|
|
return @has_chat_enabled if defined?(@has_chat_enabled)
|
|
@has_chat_enabled =
|
|
SiteSetting.chat_enabled && scope.can_chat? && object.user_option.chat_enabled
|
|
end,
|
|
) { Chat::ChannelFetcher.unreads_total(scope) }
|
|
|
|
add_to_serializer(:user_option, :chat_enabled) { object.chat_enabled }
|
|
|
|
add_to_serializer(
|
|
:user_option,
|
|
:chat_sound,
|
|
include_condition: -> { object.chat_sound.present? },
|
|
) { object.chat_sound }
|
|
|
|
add_to_serializer(:user_option, :only_chat_push_notifications) do
|
|
object.only_chat_push_notifications
|
|
end
|
|
|
|
add_to_serializer(:user_option, :ignore_channel_wide_mention) do
|
|
object.ignore_channel_wide_mention
|
|
end
|
|
|
|
add_to_serializer(:user_option, :show_thread_title_prompts) { object.show_thread_title_prompts }
|
|
|
|
add_to_serializer(:current_user_option, :show_thread_title_prompts) do
|
|
object.show_thread_title_prompts
|
|
end
|
|
|
|
add_to_serializer(:user_option, :chat_email_frequency) { object.chat_email_frequency }
|
|
|
|
add_to_serializer(:user_option, :chat_header_indicator_preference) do
|
|
object.chat_header_indicator_preference
|
|
end
|
|
|
|
add_to_serializer(:current_user_option, :chat_header_indicator_preference) do
|
|
object.chat_header_indicator_preference
|
|
end
|
|
|
|
add_to_serializer(:user_option, :chat_separate_sidebar_mode) { object.chat_separate_sidebar_mode }
|
|
|
|
add_to_serializer(:current_user_option, :chat_separate_sidebar_mode) do
|
|
object.chat_separate_sidebar_mode
|
|
end
|
|
|
|
add_to_serializer(:user_option, :chat_send_shortcut) { object.chat_send_shortcut }
|
|
|
|
add_to_serializer(:current_user_option, :chat_send_shortcut) { object.chat_send_shortcut }
|
|
|
|
add_to_serializer(:user_option, :chat_quick_reaction_type) { object.chat_quick_reaction_type }
|
|
add_to_serializer(:current_user_option, :chat_quick_reaction_type) do
|
|
object.chat_quick_reaction_type
|
|
end
|
|
|
|
add_to_serializer(:user_option, :chat_quick_reactions_custom) do
|
|
object.chat_quick_reactions_custom
|
|
end
|
|
add_to_serializer(:current_user_option, :chat_quick_reactions_custom) do
|
|
object.chat_quick_reactions_custom
|
|
end
|
|
|
|
add_to_serializer(
|
|
:category,
|
|
:has_chat_channels,
|
|
include_condition: -> { SiteSetting.chat_enabled },
|
|
) { object.category_channels.exists? }
|
|
|
|
on(:site_setting_changed) do |name, old_value, new_value|
|
|
user_option_field = Chat::RETENTION_SETTINGS_TO_USER_OPTION_FIELDS[name.to_sym]
|
|
begin
|
|
if user_option_field && old_value != new_value && !new_value.zero?
|
|
UserOption.where(user_option_field => true).update_all(user_option_field => false)
|
|
end
|
|
rescue => e
|
|
Rails.logger.warn(
|
|
"Error updating user_options fields after chat retention settings changed: #{e}",
|
|
)
|
|
end
|
|
|
|
if name == :secure_uploads && old_value == false && new_value == true
|
|
Chat::SecureUploadsCompatibility.update_settings
|
|
end
|
|
|
|
if name == :chat_allowed_groups
|
|
Jobs.enqueue(Jobs::Chat::AutoJoinUsers, event: "chat_allowed_groups_changed")
|
|
end
|
|
end
|
|
|
|
on(:post_alerter_after_save_post) do |post, new_record, notified|
|
|
next if !new_record
|
|
Chat::PostNotificationHandler.new(post, notified).handle
|
|
end
|
|
|
|
on(:group_destroyed) do |group, _user_ids|
|
|
Chat::AutoLeaveChannels.call(params: { group_id: group.id, event: :group_destroyed })
|
|
end
|
|
|
|
register_presence_channel_prefix("chat") do |channel_name|
|
|
next if channel_name != "/chat/online"
|
|
PresenceChannel::Config.new.tap { |config| config.allowed_group_ids = Chat.allowed_group_ids }
|
|
end
|
|
|
|
register_presence_channel_prefix("chat-reply") do |channel_name|
|
|
channel_id, thread_id = Chat::PRESENCE_REGEXP.match(channel_name)&.captures
|
|
|
|
next if channel_id.blank?
|
|
|
|
chat_channel =
|
|
if thread_id.present?
|
|
Chat::Thread.find_by(id: thread_id, channel_id:)&.channel
|
|
else
|
|
Chat::Channel.find_by(id: channel_id)
|
|
end
|
|
|
|
next if chat_channel.nil?
|
|
|
|
PresenceChannel::Config.new.tap do |config|
|
|
config.allowed_group_ids = chat_channel.allowed_group_ids
|
|
config.allowed_user_ids = chat_channel.allowed_user_ids
|
|
config.public = !chat_channel.read_restricted?
|
|
end
|
|
end
|
|
|
|
register_push_notification_filter do |user, payload|
|
|
if user.user_option.only_chat_push_notifications && user.user_option.chat_enabled
|
|
payload[:notification_type].in?(::Notification.types.values_at(:chat_mention, :chat_message))
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
on(:user_seen) do |user|
|
|
if user.last_seen_at == user.first_seen_at
|
|
Chat::AutoJoinChannels.call(params: { user_id: user.id })
|
|
end
|
|
end
|
|
|
|
on(:user_confirmed_email) do |user|
|
|
Chat::AutoJoinChannels.call(params: { user_id: user.id }) if user.active?
|
|
end
|
|
|
|
on(:user_added_to_group) do |user, group|
|
|
Chat::AutoJoinChannels.call(params: { user_id: user.id }) do |result|
|
|
on_exceptions do |exception|
|
|
Rails.logger.warn(
|
|
"[chat] Error auto-joining user #{user.id} to channels after being added to group #{group.id}: #{exception.message}\n\n#{result.inspect_steps}",
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
on(:user_removed_from_group) do |user, _group|
|
|
Chat::AutoLeaveChannels.call(params: { user_id: user.id, event: :user_removed_from_group })
|
|
end
|
|
|
|
on(:category_updated) do |category|
|
|
# There's a bug on core where this event is triggered with an `#update` result (true/false)
|
|
next unless category.is_a?(Category)
|
|
next unless category_channel = Chat::Channel.find_by(chatable: category)
|
|
|
|
if category_channel.auto_join_users
|
|
Chat::AutoJoinChannels.call(params: { category_id: category.id })
|
|
end
|
|
Chat::AutoLeaveChannels.call(params: { category_id: category.id, event: :category_updated })
|
|
end
|
|
|
|
# outgoing webhook events
|
|
%i[
|
|
chat_message_created
|
|
chat_message_edited
|
|
chat_message_trashed
|
|
chat_message_restored
|
|
].each do |chat_message_event|
|
|
on(chat_message_event) do |message, channel, user|
|
|
guardian = Guardian.new(user)
|
|
|
|
payload = {
|
|
message: Chat::MessageSerializer.new(message, { scope: guardian, root: false }).as_json,
|
|
channel:
|
|
Chat::ChannelSerializer.new(
|
|
channel,
|
|
{ scope: guardian, membership: channel.membership_for(user), root: false },
|
|
).as_json,
|
|
}
|
|
|
|
category_id = channel.chatable_type == "Category" ? channel.chatable_id : nil
|
|
|
|
WebHook.enqueue_chat_message_hooks(
|
|
chat_message_event,
|
|
payload.to_json,
|
|
category_id: category_id,
|
|
)
|
|
end
|
|
end
|
|
|
|
Discourse::Application.routes.append do
|
|
mount ::Chat::Engine, at: "/chat"
|
|
|
|
get "/admin/plugins/chat/hooks" => "chat/admin/incoming_webhooks#index",
|
|
:constraints => StaffConstraint.new
|
|
post "/admin/plugins/chat/hooks" => "chat/admin/incoming_webhooks#create",
|
|
:constraints => StaffConstraint.new
|
|
put "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
|
"chat/admin/incoming_webhooks#update",
|
|
:constraints => StaffConstraint.new
|
|
get "/admin/plugins/chat/hooks/new" => "chat/admin/incoming_webhooks#new",
|
|
:constraints => StaffConstraint.new
|
|
get "/admin/plugins/chat/hooks/:incoming_chat_webhook_id/edit" =>
|
|
"chat/admin/incoming_webhooks#edit",
|
|
:constraints => StaffConstraint.new
|
|
delete "/admin/plugins/chat/hooks/:incoming_chat_webhook_id" =>
|
|
"chat/admin/incoming_webhooks#destroy",
|
|
:constraints => StaffConstraint.new
|
|
get "u/:username/preferences/chat" => "users#preferences",
|
|
:constraints => {
|
|
username: RouteFormat.username,
|
|
}
|
|
end
|
|
|
|
add_automation_scriptable("send_chat_message") do
|
|
field :chat_channel_id, component: :text, required: true
|
|
field :message, component: :message, required: true, accepts_placeholders: true
|
|
field :sender, component: :user
|
|
|
|
placeholder :channel_name
|
|
placeholder :post_quote, triggerable: :post_created_edited
|
|
|
|
triggerables %i[recurring topic_tags_changed post_created_edited]
|
|
|
|
script do |context, fields, automation|
|
|
sender = User.find_by(username: fields.dig("sender", "value")) || Discourse.system_user
|
|
channel = Chat::Channel.find_by(id: fields.dig("chat_channel_id", "value"))
|
|
placeholders = { channel_name: channel.title(sender) }.merge(context["placeholders"] || {})
|
|
|
|
if context["kind"] == "post_created_edited"
|
|
placeholders[:post_quote] = utils.build_quote(context["post"])
|
|
end
|
|
|
|
creator =
|
|
::Chat::CreateMessage.call(
|
|
guardian: sender.guardian,
|
|
params: {
|
|
chat_channel_id: channel.id,
|
|
message: utils.apply_placeholders(fields.dig("message", "value"), placeholders),
|
|
},
|
|
)
|
|
|
|
if creator.failure?
|
|
Rails.logger.warn "[discourse-automation] Chat message failed to send:\n#{creator.inspect_steps}"
|
|
end
|
|
end
|
|
end
|
|
|
|
add_api_key_scope(
|
|
:chat,
|
|
{
|
|
create_message: {
|
|
actions: %w[chat/api/channel_messages#create],
|
|
params: %i[chat_channel_id],
|
|
},
|
|
},
|
|
)
|
|
|
|
# Dark mode email styles
|
|
Email::Styles.register_plugin_style do |fragment|
|
|
fragment.css(".chat-summary-header").each { |element| element[:dm] = "header" }
|
|
fragment.css(".chat-summary-content").each { |element| element[:dm] = "body" }
|
|
end
|
|
|
|
register_email_unsubscriber("chat_summary", EmailControllerHelper::ChatSummaryUnsubscriber)
|
|
|
|
register_stat("chat_messages", expose_via_api: true) { Chat::Statistics.about_messages }
|
|
register_stat("chat_users", expose_via_api: true) { Chat::Statistics.about_users }
|
|
register_stat("chat_channels", expose_via_api: true) { Chat::Statistics.about_channels }
|
|
|
|
register_stat("chat_channel_messages") { Chat::Statistics.channel_messages }
|
|
register_stat("chat_direct_messages") { Chat::Statistics.direct_messages }
|
|
register_stat("chat_open_channels_with_threads_enabled") do
|
|
Chat::Statistics.open_channels_with_threads_enabled
|
|
end
|
|
register_stat("chat_threaded_messages") { Chat::Statistics.threaded_messages }
|
|
|
|
# Make sure to update spec/system/hashtag_autocomplete_spec.rb when changing this.
|
|
register_hashtag_data_source(Chat::ChannelHashtagDataSource)
|
|
register_hashtag_type_priority_for_context("channel", "chat-composer", 200)
|
|
register_hashtag_type_priority_for_context("category", "chat-composer", 100)
|
|
register_hashtag_type_priority_for_context("tag", "chat-composer", 50)
|
|
register_hashtag_type_priority_for_context("channel", "topic-composer", 10)
|
|
|
|
register_post_stripper do |nokogiri_fragment|
|
|
nokogiri_fragment.css(".chat-transcript .mention").remove
|
|
end
|
|
|
|
Site.markdown_additional_options["chat"] = {
|
|
limited_pretty_text_features: Chat::Message::MARKDOWN_FEATURES,
|
|
limited_pretty_text_markdown_rules: Chat::Message::MARKDOWN_IT_RULES,
|
|
hashtag_configurations: HashtagAutocompleteService.contexts_with_ordered_types,
|
|
}
|
|
|
|
register_user_destroyer_on_content_deletion_callback(
|
|
Proc.new { |user| Jobs.enqueue(Jobs::Chat::DeleteUserMessages, user_id: user.id) },
|
|
)
|
|
|
|
register_notification_consolidation_plan(
|
|
Chat::NotificationConsolidationExtension.watched_thread_message_plan,
|
|
)
|
|
|
|
register_bookmarkable(Chat::MessageBookmarkable)
|
|
|
|
# When we eventually allow secure_uploads in chat, this will need to be
|
|
# removed. Depending on the channel, uploads may end up being secure.
|
|
UploadSecurity.register_custom_public_type("chat-composer")
|
|
|
|
if Rails.env.local?
|
|
DiscoursePluginRegistry.discourse_dev_populate_reviewable_types.add DiscourseDev::ReviewableMessage
|
|
end
|
|
end
|
|
|
|
Dir[Rails.root.join("plugins/chat/spec/support/**/*.rb")].each { |f| require f } if Rails.env.test?
|