DEV: properly namespace chat (#20690)

This commit main goal was to comply with Zeitwerk and properly rely on autoloading. To achieve this, most resources have been namespaced under the `Chat` module.

- Given all models are now namespaced with `Chat::` and would change the stored types in DB when using polymorphism or STI (single table inheritance), this commit uses various Rails methods to ensure proper class is loaded and the stored name in DB is unchanged, eg: `Chat::Message` model will be stored as `"ChatMessage"`, and `"ChatMessage"` will correctly load `Chat::Message` model.
- Jobs are now using constants only, eg: `Jobs::Chat::Foo` and should only be enqueued this way

Notes:
- This commit also used this opportunity to limit the number of registered css files in plugin.rb
- `discourse_dev` support has been removed within this commit and will be reintroduced later

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Joffrey JAFFEUX
2023-03-17 14:24:38 +01:00
committed by GitHub
parent 74349e17c9
commit 12a18d4d55
343 changed files with 9077 additions and 8745 deletions

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
class Chat::AdminIncomingChatWebhooksController < Admin::AdminController
requires_plugin Chat::PLUGIN_NAME
def index
render_serialized(
{
chat_channels: ChatChannel.public_channels,
incoming_chat_webhooks: IncomingChatWebhook.includes(:chat_channel).all,
},
AdminChatIndexSerializer,
root: false,
)
end
def create
params.require(%i[name chat_channel_id])
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
webhook = IncomingChatWebhook.new(name: params[:name], chat_channel: chat_channel)
if webhook.save
render_serialized(webhook, IncomingChatWebhookSerializer, root: false)
else
render_json_error(webhook)
end
end
def update
params.require(%i[incoming_chat_webhook_id name chat_channel_id])
webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
raise Discourse::NotFound unless webhook
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
if webhook.update(
name: params[:name],
description: params[:description],
emoji: params[:emoji],
username: params[:username],
chat_channel: chat_channel,
)
render json: success_json
else
render_json_error(webhook)
end
end
def destroy
params.require(:incoming_chat_webhook_id)
webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
webhook.destroy if webhook
render json: success_json
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelsStatusController < Chat::Api::ChatChannelsController
def update
with_service(Chat::Service::UpdateChannelStatus) do
on_success { render_serialized(result.channel, ChatChannelSerializer, root: "channel") }
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
on_failed_policy(:check_channel_permission) { raise Discourse::InvalidAccess }
end
end
end

View File

@ -1,8 +0,0 @@
# frozen_string_literal: true
class Chat::Api::ChatCurrentUserChannelsController < Chat::Api
def index
structured = Chat::ChatChannelFetcher.structured(guardian)
render_serialized(structured, ChatChannelIndexSerializer, root: false)
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
class Chat::Api < Chat::ChatBaseController
before_action :ensure_logged_in
before_action :ensure_can_chat
include Chat::WithServiceHelper
private
def ensure_can_chat
raise Discourse::NotFound unless SiteSetting.chat_enabled
guardian.ensure_can_chat!
end
def default_actions_for_service
proc do
on_success { render(json: success_json) }
on_failure { render(json: failed_json, status: 422) }
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
on_failed_contract do
render(
json: failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages),
status: 400,
)
end
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Chat
module Admin
class IncomingWebhooksController < ::Admin::AdminController
requires_plugin Chat::PLUGIN_NAME
def index
render_serialized(
{
chat_channels: Chat::Channel.public_channels,
incoming_chat_webhooks: Chat::IncomingWebhook.includes(:chat_channel).all,
},
Chat::AdminChatIndexSerializer,
root: false,
)
end
def create
params.require(%i[name chat_channel_id])
chat_channel = Chat::Channel.find_by(id: params[:chat_channel_id])
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
webhook = Chat::IncomingWebhook.new(name: params[:name], chat_channel: chat_channel)
if webhook.save
render_serialized(webhook, Chat::IncomingWebhookSerializer, root: false)
else
render_json_error(webhook)
end
end
def update
params.require(%i[incoming_chat_webhook_id name chat_channel_id])
webhook = Chat::IncomingWebhook.find_by(id: params[:incoming_chat_webhook_id])
raise Discourse::NotFound unless webhook
chat_channel = Chat::Channel.find_by(id: params[:chat_channel_id])
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
if webhook.update(
name: params[:name],
description: params[:description],
emoji: params[:emoji],
username: params[:username],
chat_channel: chat_channel,
)
render json: success_json
else
render_json_error(webhook)
end
end
def destroy
params.require(:incoming_chat_webhook_id)
webhook = Chat::IncomingWebhook.find_by(id: params[:incoming_chat_webhook_id])
webhook.destroy if webhook
render json: success_json
end
end
end
end

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelThreadsController < Chat::Api
class Chat::Api::ChannelThreadsController < Chat::ApiController
def show
with_service(Chat::Service::LookupThread) do
on_success { render_serialized(result.thread, ChatThreadSerializer, root: "thread") }
with_service(::Chat::LookupThread) do
on_success { render_serialized(result.thread, ::Chat::ThreadSerializer, root: "thread") }
on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound }
on_failed_policy(:threading_enabled_for_channel) { raise Discourse::NotFound }
on_model_not_found(:thread) { raise Discourse::NotFound }

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsController
class Chat::Api::ChannelsArchivesController < Chat::Api::ChannelsController
def create
existing_archive = channel_from_params.chat_channel_archive
if existing_archive.present?
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
raise Discourse::InvalidAccess if !existing_archive.failed?
Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: channel_from_params)
Chat::ChannelArchiveService.retry_archive_process(chat_channel: channel_from_params)
return render json: success_json
end
@ -20,12 +20,12 @@ class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsControl
end
begin
Chat::ChatChannelArchiveService.create_archive_process(
Chat::ChannelArchiveService.create_archive_process(
chat_channel: channel_from_params,
acting_user: current_user,
topic_params: topic_params,
)
rescue Chat::ChatChannelArchiveService::ArchiveValidationError => err
rescue Chat::ChannelArchiveService::ArchiveValidationError => err
return render json: failed_json.merge(errors: err.errors), status: 400
end

View File

@ -3,19 +3,19 @@
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
class Chat::Api::ChatChannelsController < Chat::Api
class Chat::Api::ChannelsController < Chat::ApiController
def index
permitted = params.permit(:filter, :limit, :offset, :status)
options = { filter: permitted[:filter], limit: (permitted[:limit] || 25).to_i }
options[:offset] = permitted[:offset].to_i
options[:status] = ChatChannel.statuses[permitted[:status]] ? permitted[:status] : nil
options[:status] = Chat::Channel.statuses[permitted[:status]] ? permitted[:status] : nil
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options)
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
channels = Chat::ChannelFetcher.secured_public_channels(guardian, memberships, options)
serialized_channels =
channels.map do |channel|
ChatChannelSerializer.new(
Chat::ChannelSerializer.new(
channel,
scope: Guardian.new(current_user),
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
@ -29,7 +29,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
end
def destroy
with_service Chat::Service::TrashChannel do
with_service Chat::TrashChannel do
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
end
end
@ -43,7 +43,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
raise Discourse::InvalidParameters.new(:name)
end
if ChatChannel.exists?(
if Chat::Channel.exists?(
chatable_type: "Category",
chatable_id: channel_params[:chatable_id],
name: channel_params[:name],
@ -69,12 +69,12 @@ class Chat::Api::ChatChannelsController < Chat::Api
channel.user_chat_channel_memberships.create!(user: current_user, following: true)
if channel.auto_join_users
Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
end
render_serialized(
channel,
ChatChannelSerializer,
Chat::ChannelSerializer,
membership: channel.membership_for(current_user),
root: "channel",
)
@ -83,7 +83,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
def show
render_serialized(
channel_from_params,
ChatChannelSerializer,
Chat::ChannelSerializer,
membership: channel_from_params.membership_for(current_user),
root: "channel",
)
@ -96,11 +96,11 @@ class Chat::Api::ChatChannelsController < Chat::Api
auto_join_limiter(channel_from_params).performed!
end
with_service(Chat::Service::UpdateChannel, **params_to_edit) do
with_service(Chat::UpdateChannel, **params_to_edit) do
on_success do
render_serialized(
result.channel,
ChatChannelSerializer,
Chat::ChannelSerializer,
root: "channel",
membership: result.channel.membership_for(current_user),
)
@ -116,7 +116,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
def channel_from_params
@channel ||=
begin
channel = ChatChannel.find(params.require(:channel_id))
channel = Chat::Channel.find(params.require(:channel_id))
guardian.ensure_can_preview_chat_channel!(channel)
channel
end
@ -126,7 +126,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
@membership ||=
begin
membership =
Chat::ChatChannelMembershipManager.new(channel_from_params).find_for_user(current_user)
Chat::ChannelMembershipManager.new(channel_from_params).find_for_user(current_user)
raise Discourse::NotFound if membership.blank?
membership
end

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatChannelsController
class Chat::Api::ChannelsCurrentUserMembershipController < Chat::Api::ChannelsController
def create
guardian.ensure_can_join_chat_channel!(channel_from_params)
render_serialized(
channel_from_params.add(current_user),
UserChatChannelMembershipSerializer,
Chat::UserChannelMembershipSerializer,
root: "membership",
)
end
@ -14,7 +14,7 @@ class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatCh
def destroy
render_serialized(
channel_from_params.remove(current_user),
UserChatChannelMembershipSerializer,
Chat::UserChannelMembershipSerializer,
root: "membership",
)
end

View File

@ -2,13 +2,13 @@
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
class Chat::Api::ChatChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChatChannelsController
class Chat::Api::ChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChannelsController
def update
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
membership_from_params.update!(settings_params.to_h)
render_serialized(
membership_from_params,
UserChatChannelMembershipSerializer,
Chat::UserChannelMembershipSerializer,
root: "membership",
)
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsController
class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
def index
params.permit(:username, :offset, :limit)
@ -8,7 +8,7 @@ class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsCont
limit = (params[:limit] || 50).to_i.clamp(1, 50)
memberships =
ChatChannelMembershipsQuery.call(
Chat::ChannelMembershipsQuery.call(
channel: channel_from_params,
offset: offset,
limit: limit,
@ -17,7 +17,7 @@ class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsCont
render_serialized(
memberships,
UserChatChannelMembershipSerializer,
Chat::UserChannelMembershipSerializer,
root: "memberships",
meta: {
total_rows: channel_from_params.user_count,

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsController
class Chat::Api::ChannelsMessagesMovesController < Chat::Api::ChannelsController
def create
move_params = params.require(:move)
move_params.require(:message_ids)
@ -8,10 +8,7 @@ class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsCo
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params)
destination_channel =
Chat::ChatChannelFetcher.find_with_access_check(
move_params[:destination_channel_id],
guardian,
)
Chat::ChannelFetcher.find_with_access_check(move_params[:destination_channel_id], guardian)
begin
message_ids = move_params[:message_ids].map(&:to_i)

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class Chat::Api::ChannelsStatusController < Chat::Api::ChannelsController
def update
with_service(Chat::UpdateChannelStatus) do
on_success { render_serialized(result.channel, Chat::ChannelSerializer, root: "channel") }
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
on_failed_policy(:check_channel_permission) { raise Discourse::InvalidAccess }
end
end
end

View File

@ -1,13 +1,14 @@
# frozen_string_literal: true
class Chat::Api::ChatChatablesController < Chat::Api
class Chat::Api::ChatablesController < Chat::ApiController
def index
params.require(:filter)
filter = params[:filter].downcase
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
public_channels =
Chat::ChatChannelFetcher.secured_public_channels(
Chat::ChannelFetcher.secured_public_channels(
guardian,
memberships,
filter: filter,
@ -41,7 +42,7 @@ class Chat::Api::ChatChatablesController < Chat::Api
direct_message_channels =
if users.count > 0
# FIXME: investigate the cost of this query
ChatChannel
Chat::Channel
.includes(chatable: :users)
.joins(direct_message: :direct_message_users)
.group(1)
@ -75,7 +76,7 @@ class Chat::Api::ChatChatablesController < Chat::Api
users: users_without_channel,
memberships: memberships,
},
ChatChannelSearchSerializer,
Chat::ChannelSearchSerializer,
root: false,
)
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Chat::Api::CurrentUserChannelsController < Chat::ApiController
def index
structured = Chat::ChannelFetcher.structured(guardian)
render_serialized(structured, Chat::ChannelIndexSerializer, root: false)
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Chat
class ApiController < ::Chat::BaseController
before_action :ensure_logged_in
before_action :ensure_can_chat
include Chat::WithServiceHelper
private
def ensure_can_chat
raise Discourse::NotFound unless SiteSetting.chat_enabled
guardian.ensure_can_chat!
end
def default_actions_for_service
proc do
on_success { render(json: success_json) }
on_failure { render(json: failed_json, status: 422) }
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
on_failed_contract do
render(
json:
failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages),
status: 400,
)
end
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Chat
class BaseController < ::ApplicationController
before_action :ensure_logged_in
before_action :ensure_can_chat
private
def ensure_can_chat
raise Discourse::NotFound unless SiteSetting.chat_enabled
guardian.ensure_can_chat!
end
def set_channel_and_chatable_with_access_check(chat_channel_id: nil)
params.require(:chat_channel_id) if chat_channel_id.blank?
id_or_name = chat_channel_id || params[:chat_channel_id]
@chat_channel = Chat::ChannelFetcher.find_with_access_check(id_or_name, guardian)
@chatable = @chat_channel.chatable
end
end
end

View File

@ -0,0 +1,481 @@
# frozen_string_literal: true
module Chat
class ChatController < ::Chat::BaseController
PAST_MESSAGE_LIMIT = 40
FUTURE_MESSAGE_LIMIT = 40
PAST = "past"
FUTURE = "future"
CHAT_DIRECTIONS = [PAST, FUTURE]
# Other endpoints use set_channel_and_chatable_with_access_check, but
# these endpoints require a standalone find because they need to be
# able to get deleted channels and recover them.
before_action :find_chatable, only: %i[enable_chat disable_chat]
before_action :find_chat_message,
only: %i[delete restore lookup_message edit_message rebake message_link]
before_action :set_channel_and_chatable_with_access_check,
except: %i[
respond
enable_chat
disable_chat
message_link
lookup_message
set_user_chat_status
dismiss_retention_reminder
flag
]
def respond
render
end
def enable_chat
chat_channel = Chat::Channel.with_deleted.find_by(chatable_id: @chatable)
guardian.ensure_can_join_chat_channel!(chat_channel) if chat_channel
if chat_channel && chat_channel.trashed?
chat_channel.recover!
elsif chat_channel
return render_json_error I18n.t("chat.already_enabled")
else
chat_channel = @chatable.chat_channel
guardian.ensure_can_join_chat_channel!(chat_channel)
end
success = chat_channel.save
if success && chat_channel.chatable_has_custom_fields?
@chatable.custom_fields[Chat::HAS_CHAT_ENABLED] = true
@chatable.save!
end
if success
membership = Chat::ChannelMembershipManager.new(channel).follow(user)
render_serialized(chat_channel, Chat::ChannelSerializer, membership: membership)
else
render_json_error(chat_channel)
end
Chat::ChannelMembershipManager.new(channel).follow(user)
end
def disable_chat
chat_channel = Chat::Channel.with_deleted.find_by(chatable_id: @chatable)
guardian.ensure_can_join_chat_channel!(chat_channel)
return render json: success_json if chat_channel.trashed?
chat_channel.trash!(current_user)
success = chat_channel.save
if success
if chat_channel.chatable_has_custom_fields?
@chatable.custom_fields.delete(Chat::HAS_CHAT_ENABLED)
@chatable.save!
end
render json: success_json
else
render_json_error(chat_channel)
end
end
def create_message
raise Discourse::InvalidAccess if current_user.silenced?
Chat::MessageRateLimiter.run!(current_user)
@user_chat_channel_membership =
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
current_user,
following: true,
)
raise Discourse::InvalidAccess unless @user_chat_channel_membership
reply_to_msg_id = params[:in_reply_to_id]
if reply_to_msg_id
rm = Chat::Message.find(reply_to_msg_id)
raise Discourse::NotFound if rm.chat_channel_id != @chat_channel.id
end
content = params[:message]
chat_message_creator =
Chat::MessageCreator.create(
chat_channel: @chat_channel,
user: current_user,
in_reply_to_id: reply_to_msg_id,
content: content,
staged_id: params[:staged_id],
upload_ids: params[:upload_ids],
)
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
@user_chat_channel_membership.update!(
last_read_message_id: chat_message_creator.chat_message.id,
)
if @chat_channel.direct_message_channel?
# If any of the channel users is ignoring, muting, or preventing DMs from
# the current user then we shold not auto-follow the channel once again or
# publish the new channel.
user_ids_allowing_communication =
UserCommScreener.new(
acting_user: current_user,
target_user_ids: @chat_channel.user_chat_channel_memberships.pluck(:user_id),
).allowing_actor_communication
if user_ids_allowing_communication.any?
Chat::Publisher.publish_new_channel(
@chat_channel,
@chat_channel.chatable.users.where(id: user_ids_allowing_communication),
)
@chat_channel
.user_chat_channel_memberships
.where(user_id: user_ids_allowing_communication)
.update_all(following: true)
end
end
Chat::Publisher.publish_user_tracking_state(
current_user,
@chat_channel.id,
chat_message_creator.chat_message.id,
)
render json: success_json
end
def edit_message
chat_message_updater =
Chat::MessageUpdater.update(
guardian: guardian,
chat_message: @message,
new_content: params[:new_message],
upload_ids: params[:upload_ids] || [],
)
return render_json_error(chat_message_updater.error) if chat_message_updater.failed?
render json: success_json
end
def update_user_last_read
membership =
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
current_user,
following: true,
)
raise Discourse::NotFound if membership.nil?
if membership.last_read_message_id &&
params[:message_id].to_i < membership.last_read_message_id
raise Discourse::InvalidParameters.new(:message_id)
end
unless Chat::Message.with_deleted.exists?(
chat_channel_id: @chat_channel.id,
id: params[:message_id],
)
raise Discourse::NotFound
end
membership.update!(last_read_message_id: params[:message_id])
Notification
.where(notification_type: Notification.types[:chat_mention])
.where(user: current_user)
.where(read: false)
.joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id")
.joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id")
.where("chat_messages.id <= ?", params[:message_id].to_i)
.where("chat_messages.chat_channel_id = ?", @chat_channel.id)
.update_all(read: true)
Chat::Publisher.publish_user_tracking_state(
current_user,
@chat_channel.id,
params[:message_id],
)
render json: success_json
end
def messages
page_size = params[:page_size]&.to_i || 1000
direction = params[:direction].to_s
message_id = params[:message_id]
if page_size > 50 ||
(
message_id.blank? ^ direction.blank? &&
(direction.present? && !CHAT_DIRECTIONS.include?(direction))
)
raise Discourse::InvalidParameters
end
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
if message_id.present?
condition = direction == PAST ? "<" : ">"
messages = messages.where("id #{condition} ?", message_id.to_i)
end
# NOTE: This order is reversed when we return the Chat::View below if the direction
# is not FUTURE.
order = direction == FUTURE ? "ASC" : "DESC"
messages = messages.order("created_at #{order}, id #{order}").limit(page_size).to_a
can_load_more_past = nil
can_load_more_future = nil
if direction == FUTURE
can_load_more_future = messages.size == page_size
elsif direction == PAST
can_load_more_past = messages.size == page_size
else
# When direction is blank, we'll return the latest messages.
can_load_more_future = false
can_load_more_past = messages.size == page_size
end
chat_view =
Chat::View.new(
chat_channel: @chat_channel,
chat_messages: direction == FUTURE ? messages : messages.reverse,
user: current_user,
can_load_more_past: can_load_more_past,
can_load_more_future: can_load_more_future,
)
render_serialized(chat_view, Chat::ViewSerializer, root: false)
end
def react
params.require(%i[message_id emoji react_action])
guardian.ensure_can_react!
Chat::MessageReactor.new(current_user, @chat_channel).react!(
message_id: params[:message_id],
react_action: params[:react_action].to_sym,
emoji: params[:emoji],
)
render json: success_json
end
def delete
guardian.ensure_can_delete_chat!(@message, @chatable)
Chat::MessageDestroyer.new.trash_message(@message, current_user)
head :ok
end
def restore
chat_channel = @message.chat_channel
guardian.ensure_can_restore_chat!(@message, chat_channel.chatable)
updated = @message.recover!
if updated
Chat::Publisher.publish_restore!(chat_channel, @message)
render json: success_json
else
render_json_error(@message)
end
end
def rebake
guardian.ensure_can_rebake_chat_message!(@message)
@message.rebake!(invalidate_oneboxes: true)
render json: success_json
end
def message_link
raise Discourse::NotFound if @message.blank? || @message.deleted_at.present?
raise Discourse::NotFound if @message.chat_channel.blank?
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
render json:
success_json.merge(
chat_channel_id: @chat_channel.id,
chat_channel_title: @chat_channel.title(current_user),
)
end
def lookup_message
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
past_messages =
messages
.where("created_at < ?", @message.created_at)
.order(created_at: :desc)
.limit(PAST_MESSAGE_LIMIT)
future_messages =
messages
.where("created_at > ?", @message.created_at)
.order(created_at: :asc)
.limit(FUTURE_MESSAGE_LIMIT)
can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT
can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT
messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat)
chat_view =
Chat::View.new(
chat_channel: @chat_channel,
chat_messages: messages,
user: current_user,
can_load_more_past: can_load_more_past,
can_load_more_future: can_load_more_future,
)
render_serialized(chat_view, Chat::ViewSerializer, root: false)
end
def set_user_chat_status
params.require(:chat_enabled)
current_user.user_option.update(chat_enabled: params[:chat_enabled])
render json: { chat_enabled: current_user.user_option.chat_enabled }
end
def invite_users
params.require(:user_ids)
users =
User
.includes(:groups)
.joins(:user_option)
.where(user_options: { chat_enabled: true })
.not_suspended
.where(id: params[:user_ids])
users.each do |user|
guardian = Guardian.new(user)
if guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel)
data = {
message: "chat.invitation_notification",
chat_channel_id: @chat_channel.id,
chat_channel_title: @chat_channel.title(user),
chat_channel_slug: @chat_channel.slug,
invited_by_username: current_user.username,
}
data[:chat_message_id] = params[:chat_message_id] if params[:chat_message_id]
user.notifications.create(
notification_type: Notification.types[:chat_invitation],
high_priority: true,
data: data.to_json,
)
end
end
render json: success_json
end
def dismiss_retention_reminder
params.require(:chatable_type)
guardian.ensure_can_chat!
unless Chat::Channel.chatable_types.include?(params[:chatable_type])
raise Discourse::InvalidParameters
end
field =
(
if Chat::Channel.public_channel_chatable_types.include?(params[:chatable_type])
:dismissed_channel_retention_reminder
else
:dismissed_dm_retention_reminder
end
)
current_user.user_option.update(field => true)
render json: success_json
end
def quote_messages
params.require(:message_ids)
message_ids = params[:message_ids].map(&:to_i)
markdown =
Chat::TranscriptService.new(
@chat_channel,
current_user,
messages_or_ids: message_ids,
).generate_markdown
render json: success_json.merge(markdown: markdown)
end
def flag
RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed!
permitted_params =
params.permit(
%i[chat_message_id flag_type_id message is_warning take_action queue_for_review],
)
chat_message =
Chat::Message.includes(:chat_channel, :revisions).find(permitted_params[:chat_message_id])
flag_type_id = permitted_params[:flag_type_id].to_i
if !ReviewableScore.types.values.include?(flag_type_id)
raise Discourse::InvalidParameters.new(:flag_type_id)
end
set_channel_and_chatable_with_access_check(chat_channel_id: chat_message.chat_channel_id)
result =
Chat::ReviewQueue.new.flag_message(chat_message, guardian, flag_type_id, permitted_params)
if result[:success]
render json: success_json
else
render_json_error(result[:errors])
end
end
def set_draft
if params[:data].present?
Chat::Draft.find_or_initialize_by(
user: current_user,
chat_channel_id: @chat_channel.id,
).update!(data: params[:data])
else
Chat::Draft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all
end
render json: success_json
end
private
def preloaded_chat_message_query
query =
Chat::Message
.includes(in_reply_to: [:user, chat_webhook_event: [:incoming_chat_webhook]])
.includes(:revisions)
.includes(user: :primary_group)
.includes(chat_webhook_event: :incoming_chat_webhook)
.includes(reactions: :user)
.includes(:bookmarks)
.includes(:uploads)
.includes(chat_channel: :chatable)
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
query
end
def find_chatable
@chatable = Category.find_by(id: params[:chatable_id])
guardian.ensure_can_moderate_chat!(@chatable)
end
def find_chat_message
@message = preloaded_chat_message_query.with_deleted
@message = @message.where(chat_channel_id: params[:chat_channel_id]) if params[
:chat_channel_id
]
@message = @message.find_by(id: params[:message_id])
raise Discourse::NotFound unless @message
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Chat
class DirectMessagesController < ::Chat::BaseController
# NOTE: For V1 of chat channel archiving and deleting we are not doing
# anything for DM channels, their behaviour will stay as is.
def create
guardian.ensure_can_chat!
users = users_from_usernames(current_user, params)
begin
chat_channel =
Chat::DirectMessageChannelCreator.create!(acting_user: current_user, target_users: users)
render_serialized(
chat_channel,
Chat::ChannelSerializer,
root: "channel",
membership: chat_channel.membership_for(current_user),
)
rescue Chat::DirectMessageChannelCreator::NotAllowed => err
render_json_error(err.message)
end
end
def index
guardian.ensure_can_chat!
users = users_from_usernames(current_user, params)
direct_message = Chat::DirectMessage.for_user_ids(users.map(&:id).uniq)
if direct_message
chat_channel = Chat::Channel.find_by(chatable_id: direct_message)
render_serialized(
chat_channel,
Chat::ChannelSerializer,
root: "channel",
membership: chat_channel.membership_for(current_user),
)
else
render body: nil, status: 404
end
end
private
def users_from_usernames(current_user, params)
params.require(:usernames)
usernames =
(params[:usernames].is_a?(String) ? params[:usernames].split(",") : params[:usernames])
users = [current_user]
other_usernames = usernames - [current_user.username]
users.concat(User.where(username: other_usernames).to_a) if other_usernames.any?
users
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Chat
class EmojisController < ::Chat::BaseController
def index
emojis = Emoji.all.group_by(&:group)
render json: MultiJson.dump(emojis)
end
end
end

View File

@ -0,0 +1,113 @@
# frozen_string_literal: true
module Chat
class IncomingWebhooksController < ::ApplicationController
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT = 10
skip_before_action :verify_authenticity_token, :redirect_to_login_if_required
before_action :validate_payload
def create_message
debug_payload
process_webhook_payload(text: params[:text], key: params[:key])
end
# See https://api.slack.com/reference/messaging/payload for the
# slack message payload format. For now we only support the
# text param, which we preprocess lightly to remove the slack-isms
# in the formatting.
def create_message_slack_compatible
debug_payload
# See note in validate_payload on why this is needed
attachments =
if params[:payload].present?
payload = params[:payload]
if String === payload
payload = JSON.parse(payload)
payload.deep_symbolize_keys!
end
payload[:attachments]
else
params[:attachments]
end
if params[:text].present?
text = Chat::SlackCompatibility.process_text(params[:text])
else
text = Chat::SlackCompatibility.process_legacy_attachments(attachments)
end
process_webhook_payload(text: text, key: params[:key])
rescue JSON::ParserError
raise Discourse::InvalidParameters
end
private
def process_webhook_payload(text:, key:)
validate_message_length(text)
webhook = find_and_rate_limit_webhook(key)
chat_message_creator =
Chat::MessageCreator.create(
chat_channel: webhook.chat_channel,
user: Discourse.system_user,
content: text,
incoming_chat_webhook: webhook,
)
if chat_message_creator.failed?
render_json_error(chat_message_creator.error)
else
render json: success_json
end
end
def find_and_rate_limit_webhook(key)
webhook = Chat::IncomingWebhook.includes(:chat_channel).find_by(key: key)
raise Discourse::NotFound unless webhook
# Rate limit to 10 messages per-minute. We can move to a site setting in the future if needed.
RateLimiter.new(
nil,
"incoming_chat_webhook_#{webhook.id}",
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT,
1.minute,
).performed!
webhook
end
def validate_message_length(message)
return if message.length <= SiteSetting.chat_maximum_message_length
raise Discourse::InvalidParameters.new(
"Body cannot be over #{SiteSetting.chat_maximum_message_length} characters",
)
end
# The webhook POST body can be in 3 different formats:
#
# * { text: "message text" }, which is the most basic method, and also mirrors Slack payloads
# * { attachments: [ text: "message text" ] }, which is a variant of Slack payloads using legacy attachments
# * { payload: "<JSON STRING>", attachments: null, text: null }, where JSON STRING can look
# like the `attachments` example above (along with other attributes), which is fired by OpsGenie
def validate_payload
params.require(:key)
if !params[:text] && !params[:payload] && !params[:attachments]
raise Discourse::InvalidParameters
end
end
def debug_payload
return if !SiteSetting.chat_debug_webhook_payloads
Rails.logger.warn(
"Debugging chat webhook payload for endpoint #{params[:key]}: " +
JSON.dump(
{ payload: params[:payload], attachments: params[:attachments], text: params[:text] },
),
)
end
end
end

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
class Chat::ChatBaseController < ::ApplicationController
before_action :ensure_logged_in
before_action :ensure_can_chat
private
def ensure_can_chat
raise Discourse::NotFound unless SiteSetting.chat_enabled
guardian.ensure_can_chat!
end
def set_channel_and_chatable_with_access_check(chat_channel_id: nil)
params.require(:chat_channel_id) if chat_channel_id.blank?
id_or_name = chat_channel_id || params[:chat_channel_id]
@chat_channel = Chat::ChatChannelFetcher.find_with_access_check(id_or_name, guardian)
@chatable = @chat_channel.chatable
end
end

View File

@ -1,472 +0,0 @@
# frozen_string_literal: true
class Chat::ChatController < Chat::ChatBaseController
PAST_MESSAGE_LIMIT = 40
FUTURE_MESSAGE_LIMIT = 40
PAST = "past"
FUTURE = "future"
CHAT_DIRECTIONS = [PAST, FUTURE]
# Other endpoints use set_channel_and_chatable_with_access_check, but
# these endpoints require a standalone find because they need to be
# able to get deleted channels and recover them.
before_action :find_chatable, only: %i[enable_chat disable_chat]
before_action :find_chat_message,
only: %i[delete restore lookup_message edit_message rebake message_link]
before_action :set_channel_and_chatable_with_access_check,
except: %i[
respond
enable_chat
disable_chat
message_link
lookup_message
set_user_chat_status
dismiss_retention_reminder
flag
]
def respond
render
end
def enable_chat
chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable)
guardian.ensure_can_join_chat_channel!(chat_channel) if chat_channel
if chat_channel && chat_channel.trashed?
chat_channel.recover!
elsif chat_channel
return render_json_error I18n.t("chat.already_enabled")
else
chat_channel = @chatable.chat_channel
guardian.ensure_can_join_chat_channel!(chat_channel)
end
success = chat_channel.save
if success && chat_channel.chatable_has_custom_fields?
@chatable.custom_fields[Chat::HAS_CHAT_ENABLED] = true
@chatable.save!
end
if success
membership = Chat::ChatChannelMembershipManager.new(channel).follow(user)
render_serialized(chat_channel, ChatChannelSerializer, membership: membership)
else
render_json_error(chat_channel)
end
Chat::ChatChannelMembershipManager.new(channel).follow(user)
end
def disable_chat
chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable)
guardian.ensure_can_join_chat_channel!(chat_channel)
return render json: success_json if chat_channel.trashed?
chat_channel.trash!(current_user)
success = chat_channel.save
if success
if chat_channel.chatable_has_custom_fields?
@chatable.custom_fields.delete(Chat::HAS_CHAT_ENABLED)
@chatable.save!
end
render json: success_json
else
render_json_error(chat_channel)
end
end
def create_message
raise Discourse::InvalidAccess if current_user.silenced?
Chat::ChatMessageRateLimiter.run!(current_user)
@user_chat_channel_membership =
Chat::ChatChannelMembershipManager.new(@chat_channel).find_for_user(
current_user,
following: true,
)
raise Discourse::InvalidAccess unless @user_chat_channel_membership
reply_to_msg_id = params[:in_reply_to_id]
if reply_to_msg_id
rm = ChatMessage.find(reply_to_msg_id)
raise Discourse::NotFound if rm.chat_channel_id != @chat_channel.id
end
content = params[:message]
chat_message_creator =
Chat::ChatMessageCreator.create(
chat_channel: @chat_channel,
user: current_user,
in_reply_to_id: reply_to_msg_id,
content: content,
staged_id: params[:staged_id],
upload_ids: params[:upload_ids],
)
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
@user_chat_channel_membership.update!(
last_read_message_id: chat_message_creator.chat_message.id,
)
if @chat_channel.direct_message_channel?
# If any of the channel users is ignoring, muting, or preventing DMs from
# the current user then we shold not auto-follow the channel once again or
# publish the new channel.
user_ids_allowing_communication =
UserCommScreener.new(
acting_user: current_user,
target_user_ids: @chat_channel.user_chat_channel_memberships.pluck(:user_id),
).allowing_actor_communication
if user_ids_allowing_communication.any?
ChatPublisher.publish_new_channel(
@chat_channel,
@chat_channel.chatable.users.where(id: user_ids_allowing_communication),
)
@chat_channel
.user_chat_channel_memberships
.where(user_id: user_ids_allowing_communication)
.update_all(following: true)
end
end
ChatPublisher.publish_user_tracking_state(
current_user,
@chat_channel.id,
chat_message_creator.chat_message.id,
)
render json: success_json
end
def edit_message
chat_message_updater =
Chat::ChatMessageUpdater.update(
guardian: guardian,
chat_message: @message,
new_content: params[:new_message],
upload_ids: params[:upload_ids] || [],
)
return render_json_error(chat_message_updater.error) if chat_message_updater.failed?
render json: success_json
end
def update_user_last_read
membership =
Chat::ChatChannelMembershipManager.new(@chat_channel).find_for_user(
current_user,
following: true,
)
raise Discourse::NotFound if membership.nil?
if membership.last_read_message_id && params[:message_id].to_i < membership.last_read_message_id
raise Discourse::InvalidParameters.new(:message_id)
end
unless ChatMessage.with_deleted.exists?(
chat_channel_id: @chat_channel.id,
id: params[:message_id],
)
raise Discourse::NotFound
end
membership.update!(last_read_message_id: params[:message_id])
Notification
.where(notification_type: Notification.types[:chat_mention])
.where(user: current_user)
.where(read: false)
.joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id")
.joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id")
.where("chat_messages.id <= ?", params[:message_id].to_i)
.where("chat_messages.chat_channel_id = ?", @chat_channel.id)
.update_all(read: true)
ChatPublisher.publish_user_tracking_state(current_user, @chat_channel.id, params[:message_id])
render json: success_json
end
def messages
page_size = params[:page_size]&.to_i || 1000
direction = params[:direction].to_s
message_id = params[:message_id]
if page_size > 50 ||
(
message_id.blank? ^ direction.blank? &&
(direction.present? && !CHAT_DIRECTIONS.include?(direction))
)
raise Discourse::InvalidParameters
end
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
if message_id.present?
condition = direction == PAST ? "<" : ">"
messages = messages.where("id #{condition} ?", message_id.to_i)
end
# NOTE: This order is reversed when we return the ChatView below if the direction
# is not FUTURE.
order = direction == FUTURE ? "ASC" : "DESC"
messages = messages.order("created_at #{order}, id #{order}").limit(page_size).to_a
can_load_more_past = nil
can_load_more_future = nil
if direction == FUTURE
can_load_more_future = messages.size == page_size
elsif direction == PAST
can_load_more_past = messages.size == page_size
else
# When direction is blank, we'll return the latest messages.
can_load_more_future = false
can_load_more_past = messages.size == page_size
end
chat_view =
ChatView.new(
chat_channel: @chat_channel,
chat_messages: direction == FUTURE ? messages : messages.reverse,
user: current_user,
can_load_more_past: can_load_more_past,
can_load_more_future: can_load_more_future,
)
render_serialized(chat_view, ChatViewSerializer, root: false)
end
def react
params.require(%i[message_id emoji react_action])
guardian.ensure_can_react!
Chat::ChatMessageReactor.new(current_user, @chat_channel).react!(
message_id: params[:message_id],
react_action: params[:react_action].to_sym,
emoji: params[:emoji],
)
render json: success_json
end
def delete
guardian.ensure_can_delete_chat!(@message, @chatable)
ChatMessageDestroyer.new.trash_message(@message, current_user)
head :ok
end
def restore
chat_channel = @message.chat_channel
guardian.ensure_can_restore_chat!(@message, chat_channel.chatable)
updated = @message.recover!
if updated
ChatPublisher.publish_restore!(chat_channel, @message)
render json: success_json
else
render_json_error(@message)
end
end
def rebake
guardian.ensure_can_rebake_chat_message!(@message)
@message.rebake!(invalidate_oneboxes: true)
render json: success_json
end
def message_link
raise Discourse::NotFound if @message.blank? || @message.deleted_at.present?
raise Discourse::NotFound if @message.chat_channel.blank?
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
render json:
success_json.merge(
chat_channel_id: @chat_channel.id,
chat_channel_title: @chat_channel.title(current_user),
)
end
def lookup_message
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
past_messages =
messages
.where("created_at < ?", @message.created_at)
.order(created_at: :desc)
.limit(PAST_MESSAGE_LIMIT)
future_messages =
messages
.where("created_at > ?", @message.created_at)
.order(created_at: :asc)
.limit(FUTURE_MESSAGE_LIMIT)
can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT
can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT
messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat)
chat_view =
ChatView.new(
chat_channel: @chat_channel,
chat_messages: messages,
user: current_user,
can_load_more_past: can_load_more_past,
can_load_more_future: can_load_more_future,
)
render_serialized(chat_view, ChatViewSerializer, root: false)
end
def set_user_chat_status
params.require(:chat_enabled)
current_user.user_option.update(chat_enabled: params[:chat_enabled])
render json: { chat_enabled: current_user.user_option.chat_enabled }
end
def invite_users
params.require(:user_ids)
users =
User
.includes(:groups)
.joins(:user_option)
.where(user_options: { chat_enabled: true })
.not_suspended
.where(id: params[:user_ids])
users.each do |user|
guardian = Guardian.new(user)
if guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel)
data = {
message: "chat.invitation_notification",
chat_channel_id: @chat_channel.id,
chat_channel_title: @chat_channel.title(user),
chat_channel_slug: @chat_channel.slug,
invited_by_username: current_user.username,
}
data[:chat_message_id] = params[:chat_message_id] if params[:chat_message_id]
user.notifications.create(
notification_type: Notification.types[:chat_invitation],
high_priority: true,
data: data.to_json,
)
end
end
render json: success_json
end
def dismiss_retention_reminder
params.require(:chatable_type)
guardian.ensure_can_chat!
unless ChatChannel.chatable_types.include?(params[:chatable_type])
raise Discourse::InvalidParameters
end
field =
(
if ChatChannel.public_channel_chatable_types.include?(params[:chatable_type])
:dismissed_channel_retention_reminder
else
:dismissed_dm_retention_reminder
end
)
current_user.user_option.update(field => true)
render json: success_json
end
def quote_messages
params.require(:message_ids)
message_ids = params[:message_ids].map(&:to_i)
markdown =
ChatTranscriptService.new(
@chat_channel,
current_user,
messages_or_ids: message_ids,
).generate_markdown
render json: success_json.merge(markdown: markdown)
end
def flag
RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed!
permitted_params =
params.permit(
%i[chat_message_id flag_type_id message is_warning take_action queue_for_review],
)
chat_message =
ChatMessage.includes(:chat_channel, :revisions).find(permitted_params[:chat_message_id])
flag_type_id = permitted_params[:flag_type_id].to_i
if !ReviewableScore.types.values.include?(flag_type_id)
raise Discourse::InvalidParameters.new(:flag_type_id)
end
set_channel_and_chatable_with_access_check(chat_channel_id: chat_message.chat_channel_id)
result =
Chat::ChatReviewQueue.new.flag_message(chat_message, guardian, flag_type_id, permitted_params)
if result[:success]
render json: success_json
else
render_json_error(result[:errors])
end
end
def set_draft
if params[:data].present?
ChatDraft.find_or_initialize_by(
user: current_user,
chat_channel_id: @chat_channel.id,
).update!(data: params[:data])
else
ChatDraft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all
end
render json: success_json
end
private
def preloaded_chat_message_query
query =
ChatMessage
.includes(in_reply_to: [:user, chat_webhook_event: [:incoming_chat_webhook]])
.includes(:revisions)
.includes(user: :primary_group)
.includes(chat_webhook_event: :incoming_chat_webhook)
.includes(reactions: :user)
.includes(:bookmarks)
.includes(:uploads)
.includes(chat_channel: :chatable)
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
query
end
def find_chatable
@chatable = Category.find_by(id: params[:chatable_id])
guardian.ensure_can_moderate_chat!(@chatable)
end
def find_chat_message
@message = preloaded_chat_message_query.with_deleted
@message = @message.where(chat_channel_id: params[:chat_channel_id]) if params[:chat_channel_id]
@message = @message.find_by(id: params[:message_id])
raise Discourse::NotFound unless @message
end
end

View File

@ -1,55 +0,0 @@
# frozen_string_literal: true
class Chat::DirectMessagesController < Chat::ChatBaseController
# NOTE: For V1 of chat channel archiving and deleting we are not doing
# anything for DM channels, their behaviour will stay as is.
def create
guardian.ensure_can_chat!
users = users_from_usernames(current_user, params)
begin
chat_channel =
Chat::DirectMessageChannelCreator.create!(acting_user: current_user, target_users: users)
render_serialized(
chat_channel,
ChatChannelSerializer,
root: "channel",
membership: chat_channel.membership_for(current_user),
)
rescue Chat::DirectMessageChannelCreator::NotAllowed => err
render_json_error(err.message)
end
end
def index
guardian.ensure_can_chat!
users = users_from_usernames(current_user, params)
direct_message = DirectMessage.for_user_ids(users.map(&:id).uniq)
if direct_message
chat_channel = ChatChannel.find_by(chatable: direct_message)
render_serialized(
chat_channel,
ChatChannelSerializer,
root: "channel",
membership: chat_channel.membership_for(current_user),
)
else
render body: nil, status: 404
end
end
private
def users_from_usernames(current_user, params)
params.require(:usernames)
usernames =
(params[:usernames].is_a?(String) ? params[:usernames].split(",") : params[:usernames])
users = [current_user]
other_usernames = usernames - [current_user.username]
users.concat(User.where(username: other_usernames).to_a) if other_usernames.any?
users
end
end

View File

@ -1,8 +0,0 @@
# frozen_string_literal: true
class Chat::EmojisController < Chat::ChatBaseController
def index
emojis = Emoji.all.group_by(&:group)
render json: MultiJson.dump(emojis)
end
end

View File

@ -1,111 +0,0 @@
# frozen_string_literal: true
class Chat::IncomingChatWebhooksController < ApplicationController
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT = 10
skip_before_action :verify_authenticity_token, :redirect_to_login_if_required
before_action :validate_payload
def create_message
debug_payload
process_webhook_payload(text: params[:text], key: params[:key])
end
# See https://api.slack.com/reference/messaging/payload for the
# slack message payload format. For now we only support the
# text param, which we preprocess lightly to remove the slack-isms
# in the formatting.
def create_message_slack_compatible
debug_payload
# See note in validate_payload on why this is needed
attachments =
if params[:payload].present?
payload = params[:payload]
if String === payload
payload = JSON.parse(payload)
payload.deep_symbolize_keys!
end
payload[:attachments]
else
params[:attachments]
end
if params[:text].present?
text = Chat::SlackCompatibility.process_text(params[:text])
else
text = Chat::SlackCompatibility.process_legacy_attachments(attachments)
end
process_webhook_payload(text: text, key: params[:key])
rescue JSON::ParserError
raise Discourse::InvalidParameters
end
private
def process_webhook_payload(text:, key:)
validate_message_length(text)
webhook = find_and_rate_limit_webhook(key)
chat_message_creator =
Chat::ChatMessageCreator.create(
chat_channel: webhook.chat_channel,
user: Discourse.system_user,
content: text,
incoming_chat_webhook: webhook,
)
if chat_message_creator.failed?
render_json_error(chat_message_creator.error)
else
render json: success_json
end
end
def find_and_rate_limit_webhook(key)
webhook = IncomingChatWebhook.includes(:chat_channel).find_by(key: key)
raise Discourse::NotFound unless webhook
# Rate limit to 10 messages per-minute. We can move to a site setting in the future if needed.
RateLimiter.new(
nil,
"incoming_chat_webhook_#{webhook.id}",
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT,
1.minute,
).performed!
webhook
end
def validate_message_length(message)
return if message.length <= SiteSetting.chat_maximum_message_length
raise Discourse::InvalidParameters.new(
"Body cannot be over #{SiteSetting.chat_maximum_message_length} characters",
)
end
# The webhook POST body can be in 3 different formats:
#
# * { text: "message text" }, which is the most basic method, and also mirrors Slack payloads
# * { attachments: [ text: "message text" ] }, which is a variant of Slack payloads using legacy attachments
# * { payload: "<JSON STRING>", attachments: null, text: null }, where JSON STRING can look
# like the `attachments` example above (along with other attributes), which is fired by OpsGenie
def validate_payload
params.require(:key)
if !params[:text] && !params[:payload] && !params[:attachments]
raise Discourse::InvalidParameters
end
end
def debug_payload
return if !SiteSetting.chat_debug_webhook_payloads
Rails.logger.warn(
"Debugging chat webhook payload for endpoint #{params[:key]}: " +
JSON.dump(
{ payload: params[:payload], attachments: params[:attachments], text: params[:text] },
),
)
end
end