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:
parent
74349e17c9
commit
12a18d4d55
|
@ -129,7 +129,7 @@ class Reviewable < ActiveRecord::Base
|
|||
update_args = {
|
||||
status: statuses[:pending],
|
||||
id: target.id,
|
||||
type: target.class.name,
|
||||
type: target.class.sti_name,
|
||||
potential_spam: potential_spam == true ? true : nil,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
DiscoursePluginRegistry.define_register(:chat_markdown_features, Set)
|
||||
|
||||
class Plugin::Instance
|
||||
def chat
|
||||
ChatPluginApiExtensions
|
||||
end
|
||||
|
||||
module ChatPluginApiExtensions
|
||||
def self.enable_markdown_feature(name)
|
||||
DiscoursePluginRegistry.chat_markdown_features << name
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@ module Chat
|
|||
instance_exec(&object.method(:default_actions_for_service).call) if default_actions
|
||||
instance_exec(&(block || proc {}))
|
||||
end
|
||||
Chat::ServiceRunner.call(service, object, **dependencies, &merged_block)
|
||||
ServiceRunner.call(service, object, **dependencies, &merged_block)
|
||||
end
|
||||
|
||||
def run_service(service, dependencies)
|
|
@ -1,81 +0,0 @@
|
|||
# NOTE: When changing auto-join logic, make sure to update the `settings.auto_join_users_info` translation as well.
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class AutoJoinChannelBatch < ::Jobs::Base
|
||||
def execute(args)
|
||||
return "starts_at or ends_at missing" if args[:starts_at].blank? || args[:ends_at].blank?
|
||||
start_user_id = args[:starts_at].to_i
|
||||
end_user_id = args[:ends_at].to_i
|
||||
|
||||
return "End is higher than start" if end_user_id < start_user_id
|
||||
|
||||
channel =
|
||||
ChatChannel.find_by(
|
||||
id: args[:chat_channel_id],
|
||||
auto_join_users: true,
|
||||
chatable_type: "Category",
|
||||
)
|
||||
|
||||
return if !channel
|
||||
|
||||
category = channel.chatable
|
||||
return if !category
|
||||
|
||||
query_args = {
|
||||
chat_channel_id: channel.id,
|
||||
start: start_user_id,
|
||||
end: end_user_id,
|
||||
suspended_until: Time.zone.now,
|
||||
last_seen_at: 3.months.ago,
|
||||
channel_category: channel.chatable_id,
|
||||
mode: UserChatChannelMembership.join_modes[:automatic],
|
||||
}
|
||||
|
||||
new_member_ids = DB.query_single(create_memberships_query(category), query_args)
|
||||
|
||||
# Only do this if we are running auto-join for a single user, if we
|
||||
# are doing it for many then we should do it after all batches are
|
||||
# complete for the channel in Jobs::AutoManageChannelMemberships
|
||||
if start_user_id == end_user_id
|
||||
Chat::ChatChannelMembershipManager.new(channel).recalculate_user_count
|
||||
end
|
||||
|
||||
ChatPublisher.publish_new_channel(channel.reload, User.where(id: new_member_ids))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_memberships_query(category)
|
||||
query = <<~SQL
|
||||
INSERT INTO user_chat_channel_memberships (user_id, chat_channel_id, following, created_at, updated_at, join_mode)
|
||||
SELECT DISTINCT(users.id), :chat_channel_id, TRUE, NOW(), NOW(), :mode
|
||||
FROM users
|
||||
INNER JOIN user_options uo ON uo.user_id = users.id
|
||||
LEFT OUTER JOIN user_chat_channel_memberships uccm ON
|
||||
uccm.chat_channel_id = :chat_channel_id AND uccm.user_id = users.id
|
||||
SQL
|
||||
|
||||
query += <<~SQL if category.read_restricted?
|
||||
INNER JOIN group_users gu ON gu.user_id = users.id
|
||||
LEFT OUTER JOIN category_groups cg ON cg.group_id = gu.group_id
|
||||
SQL
|
||||
|
||||
query += <<~SQL
|
||||
WHERE (users.id >= :start AND users.id <= :end) AND
|
||||
users.staged IS FALSE AND users.active AND
|
||||
NOT EXISTS(SELECT 1 FROM anonymous_users a WHERE a.user_id = users.id) AND
|
||||
(suspended_till IS NULL OR suspended_till <= :suspended_until) AND
|
||||
(last_seen_at > :last_seen_at) AND
|
||||
uo.chat_enabled AND
|
||||
uccm.id IS NULL
|
||||
SQL
|
||||
|
||||
query += <<~SQL if category.read_restricted?
|
||||
AND cg.category_id = :channel_category
|
||||
SQL
|
||||
|
||||
query += "RETURNING user_chat_channel_memberships.user_id"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,79 +0,0 @@
|
|||
# NOTE: When changing auto-join logic, make sure to update the `settings.auto_join_users_info` translation as well.
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class AutoManageChannelMemberships < ::Jobs::Base
|
||||
def execute(args)
|
||||
channel =
|
||||
ChatChannel.includes(:chatable).find_by(
|
||||
id: args[:chat_channel_id],
|
||||
auto_join_users: true,
|
||||
chatable_type: "Category",
|
||||
)
|
||||
|
||||
return if !channel&.chatable
|
||||
|
||||
processed =
|
||||
UserChatChannelMembership.where(
|
||||
chat_channel: channel,
|
||||
following: true,
|
||||
join_mode: UserChatChannelMembership.join_modes[:automatic],
|
||||
).count
|
||||
|
||||
auto_join_query(channel).find_in_batches do |batch|
|
||||
break if processed >= SiteSetting.max_chat_auto_joined_users
|
||||
|
||||
starts_at = batch.first.query_user_id
|
||||
ends_at = batch.last.query_user_id
|
||||
|
||||
Jobs.enqueue(
|
||||
:auto_join_channel_batch,
|
||||
chat_channel_id: channel.id,
|
||||
starts_at: starts_at,
|
||||
ends_at: ends_at,
|
||||
)
|
||||
|
||||
processed += batch.size
|
||||
end
|
||||
|
||||
# The Jobs::AutoJoinChannelBatch job will only do this recalculation
|
||||
# if it's operating on one user, so we need to make sure we do it for
|
||||
# the channel here once this job is complete.
|
||||
Chat::ChatChannelMembershipManager.new(channel).recalculate_user_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auto_join_query(channel)
|
||||
category = channel.chatable
|
||||
|
||||
users =
|
||||
User
|
||||
.real
|
||||
.activated
|
||||
.not_suspended
|
||||
.not_staged
|
||||
.distinct
|
||||
.select(:id, "users.id AS query_user_id")
|
||||
.where("last_seen_at > ?", 3.months.ago)
|
||||
.joins(:user_option)
|
||||
.where(user_options: { chat_enabled: true })
|
||||
.joins(<<~SQL)
|
||||
LEFT OUTER JOIN user_chat_channel_memberships uccm
|
||||
ON uccm.chat_channel_id = #{channel.id} AND
|
||||
uccm.user_id = users.id
|
||||
SQL
|
||||
.where("uccm.id IS NULL")
|
||||
|
||||
if category.read_restricted?
|
||||
users =
|
||||
users
|
||||
.joins(:group_users)
|
||||
.joins("INNER JOIN category_groups cg ON cg.group_id = group_users.group_id")
|
||||
.where("cg.category_id = ?", channel.chatable_id)
|
||||
end
|
||||
|
||||
users
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,83 @@
|
|||
# NOTE: When changing auto-join logic, make sure to update the `settings.auto_join_users_info` translation as well.
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoJoinChannelBatch < ::Jobs::Base
|
||||
def execute(args)
|
||||
return "starts_at or ends_at missing" if args[:starts_at].blank? || args[:ends_at].blank?
|
||||
start_user_id = args[:starts_at].to_i
|
||||
end_user_id = args[:ends_at].to_i
|
||||
|
||||
return "End is higher than start" if end_user_id < start_user_id
|
||||
|
||||
channel =
|
||||
::Chat::Channel.find_by(
|
||||
id: args[:chat_channel_id],
|
||||
auto_join_users: true,
|
||||
chatable_type: "Category",
|
||||
)
|
||||
|
||||
return if !channel
|
||||
|
||||
category = channel.chatable
|
||||
return if !category
|
||||
|
||||
query_args = {
|
||||
chat_channel_id: channel.id,
|
||||
start: start_user_id,
|
||||
end: end_user_id,
|
||||
suspended_until: Time.zone.now,
|
||||
last_seen_at: 3.months.ago,
|
||||
channel_category: channel.chatable_id,
|
||||
mode: ::Chat::UserChatChannelMembership.join_modes[:automatic],
|
||||
}
|
||||
|
||||
new_member_ids = DB.query_single(create_memberships_query(category), query_args)
|
||||
|
||||
# Only do this if we are running auto-join for a single user, if we
|
||||
# are doing it for many then we should do it after all batches are
|
||||
# complete for the channel in Jobs::Chat::AutoManageChannelMemberships
|
||||
if start_user_id == end_user_id
|
||||
::Chat::ChannelMembershipManager.new(channel).recalculate_user_count
|
||||
end
|
||||
|
||||
::Chat::Publisher.publish_new_channel(channel.reload, User.where(id: new_member_ids))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_memberships_query(category)
|
||||
query = <<~SQL
|
||||
INSERT INTO user_chat_channel_memberships (user_id, chat_channel_id, following, created_at, updated_at, join_mode)
|
||||
SELECT DISTINCT(users.id), :chat_channel_id, TRUE, NOW(), NOW(), :mode
|
||||
FROM users
|
||||
INNER JOIN user_options uo ON uo.user_id = users.id
|
||||
LEFT OUTER JOIN user_chat_channel_memberships uccm ON
|
||||
uccm.chat_channel_id = :chat_channel_id AND uccm.user_id = users.id
|
||||
SQL
|
||||
|
||||
query += <<~SQL if category.read_restricted?
|
||||
INNER JOIN group_users gu ON gu.user_id = users.id
|
||||
LEFT OUTER JOIN category_groups cg ON cg.group_id = gu.group_id
|
||||
SQL
|
||||
|
||||
query += <<~SQL
|
||||
WHERE (users.id >= :start AND users.id <= :end) AND
|
||||
users.staged IS FALSE AND users.active AND
|
||||
NOT EXISTS(SELECT 1 FROM anonymous_users a WHERE a.user_id = users.id) AND
|
||||
(suspended_till IS NULL OR suspended_till <= :suspended_until) AND
|
||||
(last_seen_at > :last_seen_at) AND
|
||||
uo.chat_enabled AND
|
||||
uccm.id IS NULL
|
||||
SQL
|
||||
|
||||
query += <<~SQL if category.read_restricted?
|
||||
AND cg.category_id = :channel_category
|
||||
SQL
|
||||
|
||||
query += "RETURNING user_chat_channel_memberships.user_id"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,81 @@
|
|||
# NOTE: When changing auto-join logic, make sure to update the `settings.auto_join_users_info` translation as well.
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoManageChannelMemberships < ::Jobs::Base
|
||||
def execute(args)
|
||||
channel =
|
||||
::Chat::Channel.includes(:chatable).find_by(
|
||||
id: args[:chat_channel_id],
|
||||
auto_join_users: true,
|
||||
chatable_type: "Category",
|
||||
)
|
||||
|
||||
return if !channel&.chatable
|
||||
|
||||
processed =
|
||||
::Chat::UserChatChannelMembership.where(
|
||||
chat_channel: channel,
|
||||
following: true,
|
||||
join_mode: ::Chat::UserChatChannelMembership.join_modes[:automatic],
|
||||
).count
|
||||
|
||||
auto_join_query(channel).find_in_batches do |batch|
|
||||
break if processed >= ::SiteSetting.max_chat_auto_joined_users
|
||||
|
||||
starts_at = batch.first.query_user_id
|
||||
ends_at = batch.last.query_user_id
|
||||
|
||||
::Jobs.enqueue(
|
||||
::Jobs::Chat::AutoJoinChannelBatch,
|
||||
chat_channel_id: channel.id,
|
||||
starts_at: starts_at,
|
||||
ends_at: ends_at,
|
||||
)
|
||||
|
||||
processed += batch.size
|
||||
end
|
||||
|
||||
# The Jobs::Chat::AutoJoinChannelBatch job will only do this recalculation
|
||||
# if it's operating on one user, so we need to make sure we do it for
|
||||
# the channel here once this job is complete.
|
||||
::Chat::ChannelMembershipManager.new(channel).recalculate_user_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auto_join_query(channel)
|
||||
category = channel.chatable
|
||||
|
||||
users =
|
||||
::User
|
||||
.real
|
||||
.activated
|
||||
.not_suspended
|
||||
.not_staged
|
||||
.distinct
|
||||
.select(:id, "users.id AS query_user_id")
|
||||
.where("last_seen_at > ?", 3.months.ago)
|
||||
.joins(:user_option)
|
||||
.where(user_options: { chat_enabled: true })
|
||||
.joins(<<~SQL)
|
||||
LEFT OUTER JOIN user_chat_channel_memberships uccm
|
||||
ON uccm.chat_channel_id = #{channel.id} AND
|
||||
uccm.user_id = users.id
|
||||
SQL
|
||||
.where("uccm.id IS NULL")
|
||||
|
||||
if category.read_restricted?
|
||||
users =
|
||||
users
|
||||
.joins(:group_users)
|
||||
.joins("INNER JOIN category_groups cg ON cg.group_id = group_users.group_id")
|
||||
.where("cg.category_id = ?", channel.chatable_id)
|
||||
end
|
||||
|
||||
users
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class ChannelArchive < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args = {})
|
||||
channel_archive = ::Chat::ChannelArchive.find_by(id: args[:chat_channel_archive_id])
|
||||
|
||||
# this should not really happen, but better to do this than throw an error
|
||||
if channel_archive.blank?
|
||||
::Rails.logger.warn(
|
||||
"Chat channel archive #{args[:chat_channel_archive_id]} could not be found, aborting archive job.",
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if channel_archive.complete?
|
||||
channel_archive.chat_channel.update!(status: :archived)
|
||||
|
||||
::Chat::Publisher.publish_archive_status(
|
||||
channel_archive.chat_channel,
|
||||
archive_status: :success,
|
||||
archived_messages: channel_archive.archived_messages,
|
||||
archive_topic_id: channel_archive.destination_topic_id,
|
||||
total_messages: channel_archive.total_messages,
|
||||
)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
::DistributedMutex.synchronize(
|
||||
"archive_chat_channel_#{channel_archive.chat_channel_id}",
|
||||
validity: 20.minutes,
|
||||
) { ::Chat::ChannelArchiveService.new(channel_archive).execute }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class ChannelDelete < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
chat_channel = ::Chat::Channel.with_deleted.find_by(id: args[:chat_channel_id])
|
||||
|
||||
# this should not really happen, but better to do this than throw an error
|
||||
if chat_channel.blank?
|
||||
::Rails.logger.warn(
|
||||
"Chat channel #{args[:chat_channel_id]} could not be found, aborting delete job.",
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
::DistributedMutex.synchronize("delete_chat_channel_#{chat_channel.id}") do
|
||||
::Rails.logger.debug("Deleting webhooks and events for channel #{chat_channel.id}")
|
||||
::Chat::Message.transaction do
|
||||
webhooks = ::Chat::IncomingWebhook.where(chat_channel: chat_channel)
|
||||
::Chat::WebhookEvent.where(incoming_chat_webhook_id: webhooks.select(:id)).delete_all
|
||||
webhooks.delete_all
|
||||
end
|
||||
|
||||
::Rails.logger.debug("Deleting drafts and memberships for channel #{chat_channel.id}")
|
||||
::Chat::Draft.where(chat_channel: chat_channel).delete_all
|
||||
::Chat::UserChatChannelMembership.where(chat_channel: chat_channel).delete_all
|
||||
|
||||
::Rails.logger.debug(
|
||||
"Deleting chat messages, mentions, revisions, and uploads for channel #{chat_channel.id}",
|
||||
)
|
||||
chat_messages = ::Chat::Message.where(chat_channel: chat_channel)
|
||||
delete_messages_and_related_records(chat_channel, chat_messages) if chat_messages.any?
|
||||
end
|
||||
end
|
||||
|
||||
def delete_messages_and_related_records(chat_channel, chat_messages)
|
||||
message_ids = chat_messages.pluck(:id)
|
||||
|
||||
::Chat::Message.transaction do
|
||||
::Chat::Mention.where(chat_message_id: message_ids).delete_all
|
||||
::Chat::MessageRevision.where(chat_message_id: message_ids).delete_all
|
||||
::Chat::MessageReaction.where(chat_message_id: message_ids).delete_all
|
||||
|
||||
# if the uploads are not used anywhere else they will be deleted
|
||||
# by the CleanUpUploads job in core
|
||||
::DB.exec("DELETE FROM chat_uploads WHERE chat_message_id IN (#{message_ids.join(",")})")
|
||||
::UploadReference.where(
|
||||
target_id: message_ids,
|
||||
target_type: ::Chat::Message.sti_name,
|
||||
).delete_all
|
||||
|
||||
# only the messages and the channel are Trashable, everything else gets
|
||||
# permanently destroyed
|
||||
chat_messages.update_all(
|
||||
deleted_by_id: chat_channel.deleted_by_id,
|
||||
deleted_at: Time.zone.now,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class DeleteUserMessages < ::Jobs::Base
|
||||
def execute(args)
|
||||
return if args[:user_id].nil?
|
||||
|
||||
::Chat::MessageDestroyer.new.destroy_in_batches(
|
||||
::Chat::Message.with_deleted.where(user_id: args[:user_id]),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,148 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class NotifyMentioned < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
@chat_message =
|
||||
::Chat::Message.includes(:user, :revisions, chat_channel: :chatable).find_by(
|
||||
id: args[:chat_message_id],
|
||||
)
|
||||
if @chat_message.nil? ||
|
||||
@chat_message.revisions.where("created_at > ?", args[:timestamp]).any?
|
||||
return
|
||||
end
|
||||
|
||||
@creator = @chat_message.user
|
||||
@chat_channel = @chat_message.chat_channel
|
||||
@already_notified_user_ids = args[:already_notified_user_ids] || []
|
||||
user_ids_to_notify = args[:to_notify_ids_map] || {}
|
||||
user_ids_to_notify.each { |mention_type, ids| process_mentions(ids, mention_type.to_sym) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_memberships(user_ids)
|
||||
query =
|
||||
::Chat::UserChatChannelMembership.includes(:user).where(
|
||||
user_id: (user_ids - @already_notified_user_ids),
|
||||
chat_channel_id: @chat_message.chat_channel_id,
|
||||
)
|
||||
query = query.where(following: true) if @chat_channel.public_channel?
|
||||
query
|
||||
end
|
||||
|
||||
def build_data_for(membership, identifier_type:)
|
||||
data = {
|
||||
chat_message_id: @chat_message.id,
|
||||
chat_channel_id: @chat_channel.id,
|
||||
mentioned_by_username: @creator.username,
|
||||
is_direct_message_channel: @chat_channel.direct_message_channel?,
|
||||
}
|
||||
|
||||
if !@is_direct_message_channel
|
||||
data[:chat_channel_title] = @chat_channel.title(membership.user)
|
||||
data[:chat_channel_slug] = @chat_channel.slug
|
||||
end
|
||||
|
||||
return data if identifier_type == :direct_mentions
|
||||
|
||||
case identifier_type
|
||||
when :here_mentions
|
||||
data[:identifier] = "here"
|
||||
when :global_mentions
|
||||
data[:identifier] = "all"
|
||||
else
|
||||
data[:identifier] = identifier_type if identifier_type
|
||||
data[:is_group_mention] = true
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def build_payload_for(membership, identifier_type:)
|
||||
payload = {
|
||||
notification_type: ::Notification.types[:chat_mention],
|
||||
username: @creator.username,
|
||||
tag: ::Chat::Notifier.push_notification_tag(:mention, @chat_channel.id),
|
||||
excerpt: @chat_message.push_notification_excerpt,
|
||||
post_url: "#{@chat_channel.relative_url}/#{@chat_message.id}",
|
||||
}
|
||||
|
||||
translation_prefix =
|
||||
(
|
||||
if @chat_channel.direct_message_channel?
|
||||
"discourse_push_notifications.popup.direct_message_chat_mention"
|
||||
else
|
||||
"discourse_push_notifications.popup.chat_mention"
|
||||
end
|
||||
)
|
||||
|
||||
translation_suffix = identifier_type == :direct_mentions ? "direct" : "other_type"
|
||||
identifier_text =
|
||||
case identifier_type
|
||||
when :here_mentions
|
||||
"@here"
|
||||
when :global_mentions
|
||||
"@all"
|
||||
when :direct_mentions
|
||||
""
|
||||
else
|
||||
"@#{identifier_type}"
|
||||
end
|
||||
|
||||
payload[:translated_title] = ::I18n.t(
|
||||
"#{translation_prefix}.#{translation_suffix}",
|
||||
username: @creator.username,
|
||||
identifier: identifier_text,
|
||||
channel: @chat_channel.title(membership.user),
|
||||
)
|
||||
|
||||
payload
|
||||
end
|
||||
|
||||
def create_notification!(membership, mention, mention_type)
|
||||
notification_data = build_data_for(membership, identifier_type: mention_type)
|
||||
is_read = ::Chat::Notifier.user_has_seen_message?(membership, @chat_message.id)
|
||||
notification =
|
||||
::Notification.create!(
|
||||
notification_type: ::Notification.types[:chat_mention],
|
||||
user_id: membership.user_id,
|
||||
high_priority: true,
|
||||
data: notification_data.to_json,
|
||||
read: is_read,
|
||||
)
|
||||
|
||||
mention.update!(notification: notification)
|
||||
end
|
||||
|
||||
def send_notifications(membership, mention_type)
|
||||
payload = build_payload_for(membership, identifier_type: mention_type)
|
||||
|
||||
if !membership.desktop_notifications_never? && !membership.muted?
|
||||
::MessageBus.publish(
|
||||
"/chat/notification-alert/#{membership.user_id}",
|
||||
payload,
|
||||
user_ids: [membership.user_id],
|
||||
)
|
||||
end
|
||||
|
||||
if !membership.mobile_notifications_never? && !membership.muted?
|
||||
::PostAlerter.push_notification(membership.user, payload)
|
||||
end
|
||||
end
|
||||
|
||||
def process_mentions(user_ids, mention_type)
|
||||
memberships = get_memberships(user_ids)
|
||||
|
||||
memberships.each do |membership|
|
||||
mention = ::Chat::Mention.find_by(user: membership.user, chat_message: @chat_message)
|
||||
if mention.present?
|
||||
create_notification!(membership, mention, mention_type)
|
||||
send_notifications(membership, mention_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class NotifyWatching < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
@chat_message =
|
||||
::Chat::Message.includes(:user, chat_channel: :chatable).find_by(
|
||||
id: args[:chat_message_id],
|
||||
)
|
||||
return if @chat_message.nil?
|
||||
|
||||
@creator = @chat_message.user
|
||||
@chat_channel = @chat_message.chat_channel
|
||||
@is_direct_message_channel = @chat_channel.direct_message_channel?
|
||||
|
||||
always_notification_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
members =
|
||||
::Chat::UserChatChannelMembership
|
||||
.includes(user: :groups)
|
||||
.joins(user: :user_option)
|
||||
.where(user_option: { chat_enabled: true })
|
||||
.where.not(user_id: args[:except_user_ids])
|
||||
.where(chat_channel_id: @chat_channel.id)
|
||||
.where(following: true)
|
||||
.where(
|
||||
"desktop_notification_level = ? OR mobile_notification_level = ?",
|
||||
always_notification_level,
|
||||
always_notification_level,
|
||||
)
|
||||
.merge(User.not_suspended)
|
||||
|
||||
if @is_direct_message_channel
|
||||
::UserCommScreener
|
||||
.new(acting_user: @creator, target_user_ids: members.map(&:user_id))
|
||||
.allowing_actor_communication
|
||||
.each do |user_id|
|
||||
send_notifications(members.find { |member| member.user_id == user_id })
|
||||
end
|
||||
else
|
||||
members.each { |member| send_notifications(member) }
|
||||
end
|
||||
end
|
||||
|
||||
def send_notifications(membership)
|
||||
user = membership.user
|
||||
guardian = ::Guardian.new(user)
|
||||
return unless guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel)
|
||||
return if ::Chat::Notifier.user_has_seen_message?(membership, @chat_message.id)
|
||||
return if online_user_ids.include?(user.id)
|
||||
|
||||
translation_key =
|
||||
(
|
||||
if @is_direct_message_channel
|
||||
"discourse_push_notifications.popup.new_direct_chat_message"
|
||||
else
|
||||
"discourse_push_notifications.popup.new_chat_message"
|
||||
end
|
||||
)
|
||||
|
||||
translation_args = { username: @creator.username }
|
||||
translation_args[:channel] = @chat_channel.title(user) unless @is_direct_message_channel
|
||||
|
||||
payload = {
|
||||
username: @creator.username,
|
||||
notification_type: ::Notification.types[:chat_message],
|
||||
post_url: @chat_channel.relative_url,
|
||||
translated_title: ::I18n.t(translation_key, translation_args),
|
||||
tag: ::Chat::Notifier.push_notification_tag(:message, @chat_channel.id),
|
||||
excerpt: @chat_message.push_notification_excerpt,
|
||||
}
|
||||
|
||||
if membership.desktop_notifications_always? && !membership.muted?
|
||||
::MessageBus.publish("/chat/notification-alert/#{user.id}", payload, user_ids: [user.id])
|
||||
end
|
||||
|
||||
if membership.mobile_notifications_always? && !membership.muted?
|
||||
::PostAlerter.push_notification(user, payload)
|
||||
end
|
||||
end
|
||||
|
||||
def online_user_ids
|
||||
@online_user_ids ||= ::PresenceChannel.new("/chat/online").user_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class ProcessMessage < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
::DistributedMutex.synchronize(
|
||||
"jobs_chat_process_message_#{args[:chat_message_id]}",
|
||||
validity: 10.minutes,
|
||||
) do
|
||||
chat_message = ::Chat::Message.find_by(id: args[:chat_message_id])
|
||||
return if !chat_message
|
||||
processor = ::Chat::MessageProcessor.new(chat_message)
|
||||
processor.run!
|
||||
|
||||
if args[:is_dirty] || processor.dirty?
|
||||
chat_message.update(
|
||||
cooked: processor.html,
|
||||
cooked_version: ::Chat::Message::BAKED_VERSION,
|
||||
)
|
||||
::Chat::Publisher.publish_processed!(chat_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class SendMessageNotifications < ::Jobs::Base
|
||||
def execute(args)
|
||||
reason = args[:reason]
|
||||
valid_reasons = %w[new edit]
|
||||
return unless valid_reasons.include?(reason)
|
||||
|
||||
return if (timestamp = args[:timestamp]).blank?
|
||||
|
||||
return if (message = ::Chat::Message.find_by(id: args[:chat_message_id])).nil?
|
||||
|
||||
if reason == "new"
|
||||
::Chat::Notifier.new(message, timestamp).notify_new
|
||||
elsif reason == "edit"
|
||||
::Chat::Notifier.new(message, timestamp).notify_edit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class UpdateChannelUserCount < Jobs::Base
|
||||
def execute(args = {})
|
||||
channel = ::Chat::Channel.find_by(id: args[:chat_channel_id])
|
||||
return if channel.blank?
|
||||
return if !channel.user_count_stale
|
||||
|
||||
channel.update!(
|
||||
user_count: ::Chat::ChannelMembershipsQuery.count(channel),
|
||||
user_count_stale: false,
|
||||
)
|
||||
|
||||
::Chat::Publisher.publish_chat_channel_metadata(channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ChatChannelArchive < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args = {})
|
||||
channel_archive = ::ChatChannelArchive.find_by(id: args[:chat_channel_archive_id])
|
||||
|
||||
# this should not really happen, but better to do this than throw an error
|
||||
if channel_archive.blank?
|
||||
Rails.logger.warn(
|
||||
"Chat channel archive #{args[:chat_channel_archive_id]} could not be found, aborting archive job.",
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if channel_archive.complete?
|
||||
channel_archive.chat_channel.update!(status: :archived)
|
||||
|
||||
ChatPublisher.publish_archive_status(
|
||||
channel_archive.chat_channel,
|
||||
archive_status: :success,
|
||||
archived_messages: channel_archive.archived_messages,
|
||||
archive_topic_id: channel_archive.destination_topic_id,
|
||||
total_messages: channel_archive.total_messages,
|
||||
)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
DistributedMutex.synchronize(
|
||||
"archive_chat_channel_#{channel_archive.chat_channel_id}",
|
||||
validity: 20.minutes,
|
||||
) { Chat::ChatChannelArchiveService.new(channel_archive).execute }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ChatChannelDelete < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
chat_channel = ::ChatChannel.with_deleted.find_by(id: args[:chat_channel_id])
|
||||
|
||||
# this should not really happen, but better to do this than throw an error
|
||||
if chat_channel.blank?
|
||||
Rails.logger.warn(
|
||||
"Chat channel #{args[:chat_channel_id]} could not be found, aborting delete job.",
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
DistributedMutex.synchronize("delete_chat_channel_#{chat_channel.id}") do
|
||||
Rails.logger.debug("Deleting webhooks and events for channel #{chat_channel.id}")
|
||||
ChatMessage.transaction do
|
||||
webhooks = IncomingChatWebhook.where(chat_channel: chat_channel)
|
||||
ChatWebhookEvent.where(incoming_chat_webhook_id: webhooks.select(:id)).delete_all
|
||||
webhooks.delete_all
|
||||
end
|
||||
|
||||
Rails.logger.debug("Deleting drafts and memberships for channel #{chat_channel.id}")
|
||||
ChatDraft.where(chat_channel: chat_channel).delete_all
|
||||
UserChatChannelMembership.where(chat_channel: chat_channel).delete_all
|
||||
|
||||
Rails.logger.debug(
|
||||
"Deleting chat messages, mentions, revisions, and uploads for channel #{chat_channel.id}",
|
||||
)
|
||||
chat_messages = ChatMessage.where(chat_channel: chat_channel)
|
||||
delete_messages_and_related_records(chat_channel, chat_messages) if chat_messages.any?
|
||||
end
|
||||
end
|
||||
|
||||
def delete_messages_and_related_records(chat_channel, chat_messages)
|
||||
message_ids = chat_messages.pluck(:id)
|
||||
|
||||
ChatMessage.transaction do
|
||||
ChatMention.where(chat_message_id: message_ids).delete_all
|
||||
ChatMessageRevision.where(chat_message_id: message_ids).delete_all
|
||||
ChatMessageReaction.where(chat_message_id: message_ids).delete_all
|
||||
|
||||
# if the uploads are not used anywhere else they will be deleted
|
||||
# by the CleanUpUploads job in core
|
||||
DB.exec("DELETE FROM chat_uploads WHERE chat_message_id IN (#{message_ids.join(",")})")
|
||||
UploadReference.where(target_id: message_ids, target_type: "ChatMessage").delete_all
|
||||
|
||||
# only the messages and the channel are Trashable, everything else gets
|
||||
# permanently destroyed
|
||||
chat_messages.update_all(
|
||||
deleted_by_id: chat_channel.deleted_by_id,
|
||||
deleted_at: Time.zone.now,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,146 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ChatNotifyMentioned < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
@chat_message =
|
||||
ChatMessage.includes(:user, :revisions, chat_channel: :chatable).find_by(
|
||||
id: args[:chat_message_id],
|
||||
)
|
||||
if @chat_message.nil? ||
|
||||
@chat_message.revisions.where("created_at > ?", args[:timestamp]).any?
|
||||
return
|
||||
end
|
||||
|
||||
@creator = @chat_message.user
|
||||
@chat_channel = @chat_message.chat_channel
|
||||
@already_notified_user_ids = args[:already_notified_user_ids] || []
|
||||
user_ids_to_notify = args[:to_notify_ids_map] || {}
|
||||
user_ids_to_notify.each { |mention_type, ids| process_mentions(ids, mention_type.to_sym) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_memberships(user_ids)
|
||||
query =
|
||||
UserChatChannelMembership.includes(:user).where(
|
||||
user_id: (user_ids - @already_notified_user_ids),
|
||||
chat_channel_id: @chat_message.chat_channel_id,
|
||||
)
|
||||
query = query.where(following: true) if @chat_channel.public_channel?
|
||||
query
|
||||
end
|
||||
|
||||
def build_data_for(membership, identifier_type:)
|
||||
data = {
|
||||
chat_message_id: @chat_message.id,
|
||||
chat_channel_id: @chat_channel.id,
|
||||
mentioned_by_username: @creator.username,
|
||||
is_direct_message_channel: @chat_channel.direct_message_channel?,
|
||||
}
|
||||
|
||||
if !@is_direct_message_channel
|
||||
data[:chat_channel_title] = @chat_channel.title(membership.user)
|
||||
data[:chat_channel_slug] = @chat_channel.slug
|
||||
end
|
||||
|
||||
return data if identifier_type == :direct_mentions
|
||||
|
||||
case identifier_type
|
||||
when :here_mentions
|
||||
data[:identifier] = "here"
|
||||
when :global_mentions
|
||||
data[:identifier] = "all"
|
||||
else
|
||||
data[:identifier] = identifier_type if identifier_type
|
||||
data[:is_group_mention] = true
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def build_payload_for(membership, identifier_type:)
|
||||
payload = {
|
||||
notification_type: Notification.types[:chat_mention],
|
||||
username: @creator.username,
|
||||
tag: Chat::ChatNotifier.push_notification_tag(:mention, @chat_channel.id),
|
||||
excerpt: @chat_message.push_notification_excerpt,
|
||||
post_url: "#{@chat_channel.relative_url}/#{@chat_message.id}",
|
||||
}
|
||||
|
||||
translation_prefix =
|
||||
(
|
||||
if @chat_channel.direct_message_channel?
|
||||
"discourse_push_notifications.popup.direct_message_chat_mention"
|
||||
else
|
||||
"discourse_push_notifications.popup.chat_mention"
|
||||
end
|
||||
)
|
||||
|
||||
translation_suffix = identifier_type == :direct_mentions ? "direct" : "other_type"
|
||||
identifier_text =
|
||||
case identifier_type
|
||||
when :here_mentions
|
||||
"@here"
|
||||
when :global_mentions
|
||||
"@all"
|
||||
when :direct_mentions
|
||||
""
|
||||
else
|
||||
"@#{identifier_type}"
|
||||
end
|
||||
|
||||
payload[:translated_title] = I18n.t(
|
||||
"#{translation_prefix}.#{translation_suffix}",
|
||||
username: @creator.username,
|
||||
identifier: identifier_text,
|
||||
channel: @chat_channel.title(membership.user),
|
||||
)
|
||||
|
||||
payload
|
||||
end
|
||||
|
||||
def create_notification!(membership, mention, mention_type)
|
||||
notification_data = build_data_for(membership, identifier_type: mention_type)
|
||||
is_read = Chat::ChatNotifier.user_has_seen_message?(membership, @chat_message.id)
|
||||
notification =
|
||||
Notification.create!(
|
||||
notification_type: Notification.types[:chat_mention],
|
||||
user_id: membership.user_id,
|
||||
high_priority: true,
|
||||
data: notification_data.to_json,
|
||||
read: is_read,
|
||||
)
|
||||
|
||||
mention.update!(notification: notification)
|
||||
end
|
||||
|
||||
def send_notifications(membership, mention_type)
|
||||
payload = build_payload_for(membership, identifier_type: mention_type)
|
||||
|
||||
if !membership.desktop_notifications_never? && !membership.muted?
|
||||
MessageBus.publish(
|
||||
"/chat/notification-alert/#{membership.user_id}",
|
||||
payload,
|
||||
user_ids: [membership.user_id],
|
||||
)
|
||||
end
|
||||
|
||||
if !membership.mobile_notifications_never? && !membership.muted?
|
||||
PostAlerter.push_notification(membership.user, payload)
|
||||
end
|
||||
end
|
||||
|
||||
def process_mentions(user_ids, mention_type)
|
||||
memberships = get_memberships(user_ids)
|
||||
|
||||
memberships.each do |membership|
|
||||
mention = ChatMention.find_by(user: membership.user, chat_message: @chat_message)
|
||||
if mention.present?
|
||||
create_notification!(membership, mention, mention_type)
|
||||
send_notifications(membership, mention_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,84 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ChatNotifyWatching < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
@chat_message =
|
||||
ChatMessage.includes(:user, chat_channel: :chatable).find_by(id: args[:chat_message_id])
|
||||
return if @chat_message.nil?
|
||||
|
||||
@creator = @chat_message.user
|
||||
@chat_channel = @chat_message.chat_channel
|
||||
@is_direct_message_channel = @chat_channel.direct_message_channel?
|
||||
|
||||
always_notification_level = UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
members =
|
||||
UserChatChannelMembership
|
||||
.includes(user: :groups)
|
||||
.joins(user: :user_option)
|
||||
.where(user_option: { chat_enabled: true })
|
||||
.where.not(user_id: args[:except_user_ids])
|
||||
.where(chat_channel_id: @chat_channel.id)
|
||||
.where(following: true)
|
||||
.where(
|
||||
"desktop_notification_level = ? OR mobile_notification_level = ?",
|
||||
always_notification_level,
|
||||
always_notification_level,
|
||||
)
|
||||
.merge(User.not_suspended)
|
||||
|
||||
if @is_direct_message_channel
|
||||
UserCommScreener
|
||||
.new(acting_user: @creator, target_user_ids: members.map(&:user_id))
|
||||
.allowing_actor_communication
|
||||
.each do |user_id|
|
||||
send_notifications(members.find { |member| member.user_id == user_id })
|
||||
end
|
||||
else
|
||||
members.each { |member| send_notifications(member) }
|
||||
end
|
||||
end
|
||||
|
||||
def send_notifications(membership)
|
||||
user = membership.user
|
||||
guardian = Guardian.new(user)
|
||||
return unless guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel)
|
||||
return if Chat::ChatNotifier.user_has_seen_message?(membership, @chat_message.id)
|
||||
return if online_user_ids.include?(user.id)
|
||||
|
||||
translation_key =
|
||||
(
|
||||
if @is_direct_message_channel
|
||||
"discourse_push_notifications.popup.new_direct_chat_message"
|
||||
else
|
||||
"discourse_push_notifications.popup.new_chat_message"
|
||||
end
|
||||
)
|
||||
|
||||
translation_args = { username: @creator.username }
|
||||
translation_args[:channel] = @chat_channel.title(user) unless @is_direct_message_channel
|
||||
|
||||
payload = {
|
||||
username: @creator.username,
|
||||
notification_type: Notification.types[:chat_message],
|
||||
post_url: @chat_channel.relative_url,
|
||||
translated_title: I18n.t(translation_key, translation_args),
|
||||
tag: Chat::ChatNotifier.push_notification_tag(:message, @chat_channel.id),
|
||||
excerpt: @chat_message.push_notification_excerpt,
|
||||
}
|
||||
|
||||
if membership.desktop_notifications_always? && !membership.muted?
|
||||
MessageBus.publish("/chat/notification-alert/#{user.id}", payload, user_ids: [user.id])
|
||||
end
|
||||
|
||||
if membership.mobile_notifications_always? && !membership.muted?
|
||||
PostAlerter.push_notification(user, payload)
|
||||
end
|
||||
end
|
||||
|
||||
def online_user_ids
|
||||
@online_user_ids ||= PresenceChannel.new("/chat/online").user_ids
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DeleteUserMessages < ::Jobs::Base
|
||||
def execute(args)
|
||||
return if args[:user_id].nil?
|
||||
|
||||
ChatMessageDestroyer.new.destroy_in_batches(
|
||||
ChatMessage.with_deleted.where(user_id: args[:user_id]),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ProcessChatMessage < ::Jobs::Base
|
||||
def execute(args = {})
|
||||
DistributedMutex.synchronize(
|
||||
"process_chat_message_#{args[:chat_message_id]}",
|
||||
validity: 10.minutes,
|
||||
) do
|
||||
chat_message = ChatMessage.find_by(id: args[:chat_message_id])
|
||||
return if !chat_message
|
||||
processor = Chat::ChatMessageProcessor.new(chat_message)
|
||||
processor.run!
|
||||
|
||||
if args[:is_dirty] || processor.dirty?
|
||||
chat_message.update(cooked: processor.html, cooked_version: ChatMessage::BAKED_VERSION)
|
||||
ChatPublisher.publish_processed!(chat_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class SendMessageNotifications < ::Jobs::Base
|
||||
def execute(args)
|
||||
reason = args[:reason]
|
||||
valid_reasons = %w[new edit]
|
||||
return unless valid_reasons.include?(reason)
|
||||
|
||||
return if (timestamp = args[:timestamp]).blank?
|
||||
|
||||
return if (message = ChatMessage.find_by(id: args[:chat_message_id])).nil?
|
||||
|
||||
if reason == "new"
|
||||
Chat::ChatNotifier.new(message, timestamp).notify_new
|
||||
elsif reason == "edit"
|
||||
Chat::ChatNotifier.new(message, timestamp).notify_edit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class UpdateChannelUserCount < Jobs::Base
|
||||
def execute(args = {})
|
||||
channel = ChatChannel.find_by(id: args[:chat_channel_id])
|
||||
return if channel.blank?
|
||||
return if !channel.user_count_stale
|
||||
|
||||
channel.update!(
|
||||
user_count: ChatChannelMembershipsQuery.count(channel),
|
||||
user_count_stale: false,
|
||||
)
|
||||
|
||||
ChatPublisher.publish_chat_channel_metadata(channel)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class AutoJoinUsers < ::Jobs::Scheduled
|
||||
every 1.hour
|
||||
|
||||
def execute(_args)
|
||||
ChatChannel
|
||||
.where(auto_join_users: true)
|
||||
.each do |channel|
|
||||
Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoJoinUsers < ::Jobs::Scheduled
|
||||
every 1.hour
|
||||
|
||||
def execute(_args)
|
||||
::Chat::Channel
|
||||
.where(auto_join_users: true)
|
||||
.each do |channel|
|
||||
::Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class DeleteOldMessages < ::Jobs::Scheduled
|
||||
daily at: 0.hours
|
||||
|
||||
def execute(args = {})
|
||||
delete_public_channel_messages
|
||||
delete_dm_channel_messages
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_public_channel_messages
|
||||
return unless valid_day_value?(:chat_channel_retention_days)
|
||||
|
||||
::Chat::MessageDestroyer.new.destroy_in_batches(
|
||||
::Chat::Message.in_public_channel.with_deleted.created_before(
|
||||
::SiteSetting.chat_channel_retention_days.days.ago,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def delete_dm_channel_messages
|
||||
return unless valid_day_value?(:chat_dm_retention_days)
|
||||
|
||||
::Chat::MessageDestroyer.new.destroy_in_batches(
|
||||
::Chat::Message.in_dm_channel.with_deleted.created_before(
|
||||
::SiteSetting.chat_dm_retention_days.days.ago,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def valid_day_value?(setting_name)
|
||||
(::SiteSetting.public_send(setting_name) || 0).positive?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class EmailNotifications < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
|
||||
def execute(args = {})
|
||||
return unless ::SiteSetting.chat_enabled
|
||||
|
||||
::Chat::Mailer.send_unread_mentions_summary
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class PeriodicalUpdates < ::Jobs::Scheduled
|
||||
every 15.minutes
|
||||
|
||||
def execute(args = nil)
|
||||
# TODO: Add rebaking of old messages (baked_version <
|
||||
# Chat::Message::BAKED_VERSION or baked_version IS NULL)
|
||||
::Chat::Channel.ensure_consistency!
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
# TODO (martin) Move into Chat::Channel.ensure_consistency! so it
|
||||
# is run with Jobs::Chat::PeriodicalUpdates
|
||||
module Chat
|
||||
class UpdateUserCountsForChannels < ::Jobs::Scheduled
|
||||
every 1.hour
|
||||
|
||||
# FIXME: This could become huge as the amount of channels grows, we
|
||||
# need a different approach here. Perhaps we should only bother for
|
||||
# channels updated or with new messages in the past N days? Perhaps
|
||||
# we could update all the counts in a single query as well?
|
||||
def execute(args = {})
|
||||
::Chat::Channel
|
||||
.where(status: %i[open closed])
|
||||
.find_each { |chat_channel| set_user_count(chat_channel) }
|
||||
end
|
||||
|
||||
def set_user_count(chat_channel)
|
||||
current_count = chat_channel.user_count || 0
|
||||
new_count = ::Chat::ChannelMembershipsQuery.count(chat_channel)
|
||||
return if current_count == new_count
|
||||
|
||||
chat_channel.update(user_count: new_count, user_count_stale: false)
|
||||
::Chat::Publisher.publish_chat_channel_metadata(chat_channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class ChatPeriodicalUpdates < ::Jobs::Scheduled
|
||||
every 15.minutes
|
||||
|
||||
def execute(args = nil)
|
||||
# TODO: Add rebaking of old messages (baked_version <
|
||||
# ChatMessage::BAKED_VERSION or baked_version IS NULL)
|
||||
ChatChannel.ensure_consistency!
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DeleteOldChatMessages < ::Jobs::Scheduled
|
||||
daily at: 0.hours
|
||||
|
||||
def execute(args = {})
|
||||
delete_public_channel_messages
|
||||
delete_dm_channel_messages
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_public_channel_messages
|
||||
return unless valid_day_value?(:chat_channel_retention_days)
|
||||
|
||||
ChatMessageDestroyer.new.destroy_in_batches(
|
||||
ChatMessage.in_public_channel.with_deleted.created_before(
|
||||
SiteSetting.chat_channel_retention_days.days.ago,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def delete_dm_channel_messages
|
||||
return unless valid_day_value?(:chat_dm_retention_days)
|
||||
|
||||
ChatMessageDestroyer.new.destroy_in_batches(
|
||||
ChatMessage.in_dm_channel.with_deleted.created_before(
|
||||
SiteSetting.chat_dm_retention_days.days.ago,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def valid_day_value?(setting_name)
|
||||
(SiteSetting.public_send(setting_name) || 0).positive?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class EmailChatNotifications < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
|
||||
def execute(args = {})
|
||||
return unless SiteSetting.chat_enabled
|
||||
|
||||
Chat::ChatMailer.send_unread_mentions_summary
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
# TODO (martin) Move into ChatChannel.ensure_consistency! so it
|
||||
# is run with ChatPeriodicalUpdates
|
||||
class UpdateUserCountsForChatChannels < ::Jobs::Scheduled
|
||||
every 1.hour
|
||||
|
||||
# FIXME: This could become huge as the amount of channels grows, we
|
||||
# need a different approach here. Perhaps we should only bother for
|
||||
# channels updated or with new messages in the past N days? Perhaps
|
||||
# we could update all the counts in a single query as well?
|
||||
def execute(args = {})
|
||||
ChatChannel
|
||||
.where(status: %i[open closed])
|
||||
.find_each { |chat_channel| set_user_count(chat_channel) }
|
||||
end
|
||||
|
||||
def set_user_count(chat_channel)
|
||||
current_count = chat_channel.user_count || 0
|
||||
new_count = ChatChannelMembershipsQuery.count(chat_channel)
|
||||
return if current_count == new_count
|
||||
|
||||
chat_channel.update(user_count: new_count, user_count_stale: false)
|
||||
ChatPublisher.publish_chat_channel_metadata(chat_channel)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CategoryChannel < ChatChannel
|
||||
alias_attribute :category, :chatable
|
||||
|
||||
delegate :read_restricted?, to: :category
|
||||
delegate :url, to: :chatable, prefix: true
|
||||
|
||||
%i[category_channel? public_channel? chatable_has_custom_fields?].each do |name|
|
||||
define_method(name) { true }
|
||||
end
|
||||
|
||||
def allowed_group_ids
|
||||
return if !read_restricted?
|
||||
|
||||
staff_groups = Group::AUTO_GROUPS.slice(:staff, :moderators, :admins).values
|
||||
category.secure_group_ids.to_a.concat(staff_groups)
|
||||
end
|
||||
|
||||
def title(_ = nil)
|
||||
name.presence || category.name
|
||||
end
|
||||
|
||||
def generate_auto_slug
|
||||
return if self.slug.present?
|
||||
self.slug = Slug.for(self.title.strip, "")
|
||||
self.slug = "" if duplicate_slug?
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
if self.slug.present?
|
||||
# if we don't unescape it first we strip the % from the encoded version
|
||||
slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug
|
||||
self.slug = Slug.for(slug, "", method: :encoded)
|
||||
|
||||
if self.slug.blank?
|
||||
errors.add(:slug, :invalid)
|
||||
elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.slug_contains_non_ascii_chars"))
|
||||
elsif duplicate_slug?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.is_already_in_use"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class CategoryChannel < Channel
|
||||
alias_attribute :category, :chatable
|
||||
|
||||
delegate :read_restricted?, to: :category
|
||||
delegate :url, to: :chatable, prefix: true
|
||||
|
||||
def self.polymorphic_class_for(name)
|
||||
Chat::Chatable.polymorphic_class_for(name) || super(name)
|
||||
end
|
||||
|
||||
%i[category_channel? public_channel? chatable_has_custom_fields?].each do |name|
|
||||
define_method(name) { true }
|
||||
end
|
||||
|
||||
def allowed_group_ids
|
||||
return if !read_restricted?
|
||||
|
||||
staff_groups = Group::AUTO_GROUPS.slice(:staff, :moderators, :admins).values
|
||||
category.secure_group_ids.to_a.concat(staff_groups)
|
||||
end
|
||||
|
||||
def title(_ = nil)
|
||||
name.presence || category.name
|
||||
end
|
||||
|
||||
def generate_auto_slug
|
||||
return if self.slug.present?
|
||||
self.slug = Slug.for(self.title.strip, "")
|
||||
self.slug = "" if duplicate_slug?
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
if self.slug.present?
|
||||
# if we don't unescape it first we strip the % from the encoded version
|
||||
slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug
|
||||
self.slug = Slug.for(slug, "", method: :encoded)
|
||||
|
||||
if self.slug.blank?
|
||||
errors.add(:slug, :invalid)
|
||||
elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.slug_contains_non_ascii_chars"))
|
||||
elsif duplicate_slug?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.is_already_in_use"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,196 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class Channel < ActiveRecord::Base
|
||||
include Trashable
|
||||
|
||||
self.table_name = "chat_channels"
|
||||
|
||||
belongs_to :chatable, polymorphic: true
|
||||
|
||||
def self.sti_class_for(type_name)
|
||||
Chat::Chatable.sti_class_for(type_name) || super(type_name)
|
||||
end
|
||||
|
||||
def self.sti_name
|
||||
Chat::Chatable.sti_name_for(self) || super
|
||||
end
|
||||
|
||||
belongs_to :direct_message,
|
||||
class_name: "Chat::DirectMessage",
|
||||
foreign_key: :chatable_id,
|
||||
inverse_of: :direct_message_channel,
|
||||
optional: true
|
||||
|
||||
has_many :chat_messages, class_name: "Chat::Message", foreign_key: :chat_channel_id
|
||||
has_many :user_chat_channel_memberships,
|
||||
class_name: "Chat::UserChatChannelMembership",
|
||||
foreign_key: :chat_channel_id
|
||||
has_one :chat_channel_archive, class_name: "Chat::ChannelArchive", foreign_key: :chat_channel_id
|
||||
|
||||
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
|
||||
|
||||
validates :name,
|
||||
length: {
|
||||
maximum: Proc.new { SiteSetting.max_topic_title_length },
|
||||
},
|
||||
presence: true,
|
||||
allow_nil: true
|
||||
validate :ensure_slug_ok, if: :slug_changed?
|
||||
before_validation :generate_auto_slug
|
||||
|
||||
scope :public_channels,
|
||||
-> {
|
||||
where(chatable_type: public_channel_chatable_types).where(
|
||||
"categories.id IS NOT NULL",
|
||||
).joins(
|
||||
"LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
|
||||
)
|
||||
}
|
||||
|
||||
delegate :empty?, to: :chat_messages, prefix: true
|
||||
|
||||
class << self
|
||||
def editable_statuses
|
||||
statuses.filter { |k, _| !%w[read_only archived].include?(k) }
|
||||
end
|
||||
|
||||
def public_channel_chatable_types
|
||||
%w[Category]
|
||||
end
|
||||
|
||||
def direct_channel_chatable_types
|
||||
%w[DirectMessage]
|
||||
end
|
||||
|
||||
def chatable_types
|
||||
public_channel_chatable_types + direct_channel_chatable_types
|
||||
end
|
||||
end
|
||||
|
||||
statuses.keys.each do |status|
|
||||
define_method("#{status}!") { |acting_user| change_status(acting_user, status.to_sym) }
|
||||
end
|
||||
|
||||
%i[
|
||||
category_channel?
|
||||
direct_message_channel?
|
||||
public_channel?
|
||||
chatable_has_custom_fields?
|
||||
read_restricted?
|
||||
].each { |name| define_method(name) { false } }
|
||||
|
||||
%i[allowed_user_ids allowed_group_ids chatable_url].each { |name| define_method(name) { nil } }
|
||||
|
||||
def membership_for(user)
|
||||
user_chat_channel_memberships.find_by(user: user)
|
||||
end
|
||||
|
||||
def add(user)
|
||||
Chat::ChannelMembershipManager.new(self).follow(user)
|
||||
end
|
||||
|
||||
def remove(user)
|
||||
Chat::ChannelMembershipManager.new(self).unfollow(user)
|
||||
end
|
||||
|
||||
def url
|
||||
"#{Discourse.base_url}/chat/c/#{self.slug || "-"}/#{self.id}"
|
||||
end
|
||||
|
||||
def relative_url
|
||||
"#{Discourse.base_path}/chat/c/#{self.slug || "-"}/#{self.id}"
|
||||
end
|
||||
|
||||
def self.ensure_consistency!
|
||||
update_counts
|
||||
end
|
||||
|
||||
# TODO (martin) Move Jobs::Chat::UpdateUserCountsForChannels into here
|
||||
def self.update_counts
|
||||
# NOTE: Chat::Channel#messages_count is not updated every time
|
||||
# a message is created or deleted in a channel, so it should not
|
||||
# be displayed in the UI. It is updated eventually via Jobs::Chat::PeriodicalUpdates
|
||||
DB.exec <<~SQL
|
||||
UPDATE chat_channels channels
|
||||
SET messages_count = subquery.messages_count
|
||||
FROM (
|
||||
SELECT COUNT(*) AS messages_count, chat_channel_id
|
||||
FROM chat_messages
|
||||
WHERE chat_messages.deleted_at IS NULL
|
||||
GROUP BY chat_channel_id
|
||||
) subquery
|
||||
WHERE channels.id = subquery.chat_channel_id
|
||||
AND channels.deleted_at IS NULL
|
||||
AND subquery.messages_count != channels.messages_count
|
||||
SQL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def change_status(acting_user, target_status)
|
||||
return if !Guardian.new(acting_user).can_change_channel_status?(self, target_status)
|
||||
self.update!(status: target_status)
|
||||
log_channel_status_change(acting_user: acting_user)
|
||||
end
|
||||
|
||||
def log_channel_status_change(acting_user:)
|
||||
DiscourseEvent.trigger(
|
||||
:chat_channel_status_change,
|
||||
channel: self,
|
||||
old_status: status_previously_was,
|
||||
new_status: status,
|
||||
)
|
||||
|
||||
StaffActionLogger.new(acting_user).log_custom(
|
||||
"chat_channel_status_change",
|
||||
{
|
||||
chat_channel_id: self.id,
|
||||
chat_channel_name: self.name,
|
||||
previous_value: status_previously_was,
|
||||
new_value: status,
|
||||
},
|
||||
)
|
||||
|
||||
Chat::Publisher.publish_channel_status(self)
|
||||
end
|
||||
|
||||
def duplicate_slug?
|
||||
Chat::Channel.where(slug: self.slug).where.not(id: self.id).any?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: chat_channels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# chatable_id :integer not null
|
||||
# deleted_at :datetime
|
||||
# deleted_by_id :integer
|
||||
# featured_in_category_id :integer
|
||||
# delete_after_seconds :integer
|
||||
# chatable_type :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# name :string
|
||||
# description :text
|
||||
# status :integer default("open"), not null
|
||||
# user_count :integer default(0), not null
|
||||
# last_message_sent_at :datetime not null
|
||||
# auto_join_users :boolean default(FALSE), not null
|
||||
# allow_channel_wide_mentions :boolean default(TRUE), not null
|
||||
# user_count_stale :boolean default(FALSE), not null
|
||||
# slug :string
|
||||
# type :string
|
||||
# threading_enabled :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_chat_channels_on_messages_count (messages_count)
|
||||
# index_chat_channels_on_chatable_id (chatable_id)
|
||||
# index_chat_channels_on_chatable_id_and_chatable_type (chatable_id,chatable_type)
|
||||
# index_chat_channels_on_slug (slug) UNIQUE
|
||||
# index_chat_channels_on_status (status)
|
||||
#
|
|
@ -1,21 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatChannelArchive < ActiveRecord::Base
|
||||
belongs_to :chat_channel
|
||||
belongs_to :archived_by, class_name: "User"
|
||||
module Chat
|
||||
class ChannelArchive < ActiveRecord::Base
|
||||
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||
belongs_to :archived_by, class_name: "User"
|
||||
belongs_to :destination_topic, class_name: "Topic"
|
||||
|
||||
belongs_to :destination_topic, class_name: "Topic"
|
||||
self.table_name = "chat_channel_archives"
|
||||
|
||||
def complete?
|
||||
self.archived_messages >= self.total_messages && self.chat_channel.chat_messages.count.zero?
|
||||
end
|
||||
def complete?
|
||||
self.archived_messages >= self.total_messages && self.chat_channel.chat_messages.count.zero?
|
||||
end
|
||||
|
||||
def failed?
|
||||
!complete? && self.archive_error.present?
|
||||
end
|
||||
def failed?
|
||||
!complete? && self.archive_error.present?
|
||||
end
|
||||
|
||||
def new_topic?
|
||||
self.destination_topic_title.present?
|
||||
def new_topic?
|
||||
self.destination_topic_title.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class DeletedUser < User
|
||||
def username
|
||||
I18n.t("chat.deleted_chat_username")
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
"/plugins/chat/images/deleted-chat-user-avatar.png"
|
||||
end
|
||||
|
||||
def bot?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class DirectMessage < ActiveRecord::Base
|
||||
self.table_name = "direct_message_channels"
|
||||
|
||||
include Chatable
|
||||
|
||||
def self.polymorphic_name
|
||||
Chat::Chatable.polymorphic_name_for(self) || super
|
||||
end
|
||||
|
||||
has_many :direct_message_users,
|
||||
class_name: "Chat::DirectMessageUser",
|
||||
foreign_key: :direct_message_channel_id
|
||||
has_many :users, through: :direct_message_users
|
||||
|
||||
has_one :direct_message_channel, as: :chatable, class_name: "Chat::DirectMessageChannel"
|
||||
|
||||
def self.for_user_ids(user_ids)
|
||||
joins(:users)
|
||||
.group("direct_message_channels.id")
|
||||
.having("ARRAY[?] = ARRAY_AGG(users.id ORDER BY users.id)", user_ids.sort)
|
||||
&.first
|
||||
end
|
||||
|
||||
def user_can_access?(user)
|
||||
users.include?(user)
|
||||
end
|
||||
|
||||
def chat_channel_title_for_user(chat_channel, acting_user)
|
||||
users =
|
||||
(direct_message_users.map(&:user) - [acting_user]).map do |user|
|
||||
user || Chat::DeletedUser.new
|
||||
end
|
||||
|
||||
# direct message to self
|
||||
if users.empty?
|
||||
return I18n.t("chat.channel.dm_title.single_user", username: "@#{acting_user.username}")
|
||||
end
|
||||
|
||||
# all users deleted
|
||||
return chat_channel.id if !users.first
|
||||
|
||||
usernames_formatted = users.sort_by(&:username).map { |u| "@#{u.username}" }
|
||||
if usernames_formatted.size > 5
|
||||
return(
|
||||
I18n.t(
|
||||
"chat.channel.dm_title.multi_user_truncated",
|
||||
comma_separated_usernames:
|
||||
usernames_formatted[0..4].join(I18n.t("word_connector.comma")),
|
||||
count: usernames_formatted.length - 5,
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
I18n.t(
|
||||
"chat.channel.dm_title.multi_user",
|
||||
comma_separated_usernames: usernames_formatted.join(I18n.t("word_connector.comma")),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: direct_message_channels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class DirectMessageChannel < Channel
|
||||
alias_attribute :direct_message, :chatable
|
||||
|
||||
def self.polymorphic_class_for(name)
|
||||
Chat::Chatable.polymorphic_class_for(name) || super(name)
|
||||
end
|
||||
|
||||
def direct_message_channel?
|
||||
true
|
||||
end
|
||||
|
||||
def allowed_user_ids
|
||||
direct_message.user_ids
|
||||
end
|
||||
|
||||
def read_restricted?
|
||||
true
|
||||
end
|
||||
|
||||
def title(user)
|
||||
direct_message.chat_channel_title_for_user(self, user)
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
true
|
||||
end
|
||||
|
||||
def generate_auto_slug
|
||||
self.slug = nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectMessageUser < ActiveRecord::Base
|
||||
belongs_to :direct_message, foreign_key: :direct_message_channel_id
|
||||
belongs_to :user
|
||||
module Chat
|
||||
class DirectMessageUser < ActiveRecord::Base
|
||||
self.table_name = "direct_message_users"
|
||||
|
||||
belongs_to :direct_message,
|
||||
class_name: "Chat::DirectMessage",
|
||||
foreign_key: :direct_message_channel_id
|
||||
belongs_to :user
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -1,13 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatDraft < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :chat_channel
|
||||
module Chat
|
||||
class Draft < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||
|
||||
validate :data_length
|
||||
def data_length
|
||||
if self.data && self.data.length > SiteSetting.max_chat_draft_length
|
||||
self.errors.add(:base, I18n.t("chat.errors.draft_too_long"))
|
||||
self.table_name = "chat_drafts"
|
||||
|
||||
validate :data_length
|
||||
def data_length
|
||||
if self.data && self.data.length > SiteSetting.max_chat_draft_length
|
||||
self.errors.add(:base, I18n.t("chat.errors.draft_too_long"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IncomingChatWebhook < ActiveRecord::Base
|
||||
belongs_to :chat_channel
|
||||
has_many :chat_webhook_events
|
||||
module Chat
|
||||
class IncomingWebhook < ActiveRecord::Base
|
||||
self.table_name = "incoming_chat_webhooks"
|
||||
|
||||
before_create { self.key = SecureRandom.hex(12) }
|
||||
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||
has_many :chat_webhook_events, class_name: "Chat::WebhookEvent"
|
||||
|
||||
def url
|
||||
"#{Discourse.base_url}/chat/hooks/#{key}.json"
|
||||
before_create { self.key = SecureRandom.hex(12) }
|
||||
|
||||
def url
|
||||
"#{Discourse.base_url}/chat/hooks/#{key}.json"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatMention < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :chat_message
|
||||
belongs_to :notification, dependent: :destroy
|
||||
module Chat
|
||||
class Mention < ActiveRecord::Base
|
||||
self.table_name = "chat_mentions"
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :chat_message, class_name: "Chat::Message"
|
||||
belongs_to :notification, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -0,0 +1,360 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class Message < ActiveRecord::Base
|
||||
include Trashable
|
||||
|
||||
self.table_name = "chat_messages"
|
||||
|
||||
attribute :has_oneboxes, default: false
|
||||
|
||||
BAKED_VERSION = 2
|
||||
|
||||
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||
belongs_to :user
|
||||
belongs_to :in_reply_to, class_name: "Chat::Message"
|
||||
belongs_to :last_editor, class_name: "User"
|
||||
belongs_to :thread, class_name: "Chat::Thread"
|
||||
|
||||
has_many :replies,
|
||||
class_name: "Chat::Message",
|
||||
foreign_key: "in_reply_to_id",
|
||||
dependent: :nullify
|
||||
has_many :revisions,
|
||||
class_name: "Chat::MessageRevision",
|
||||
dependent: :destroy,
|
||||
foreign_key: :chat_message_id
|
||||
has_many :reactions,
|
||||
class_name: "Chat::MessageReaction",
|
||||
dependent: :destroy,
|
||||
foreign_key: :chat_message_id
|
||||
has_many :bookmarks,
|
||||
-> {
|
||||
unscope(where: :bookmarkable_type).where(bookmarkable_type: Chat::Message.sti_name)
|
||||
},
|
||||
as: :bookmarkable,
|
||||
dependent: :destroy
|
||||
has_many :upload_references,
|
||||
-> { unscope(where: :target_type).where(target_type: Chat::Message.sti_name) },
|
||||
dependent: :destroy,
|
||||
foreign_key: :target_id
|
||||
has_many :uploads, through: :upload_references, class_name: "::Upload"
|
||||
|
||||
CLASS_MAPPING = { "ChatMessage" => Chat::Message }
|
||||
|
||||
# the model used when loading type column
|
||||
def self.sti_class_for(name)
|
||||
CLASS_MAPPING[name] if CLASS_MAPPING.key?(name)
|
||||
end
|
||||
# the type column value
|
||||
def self.sti_name
|
||||
CLASS_MAPPING.invert.fetch(self)
|
||||
end
|
||||
|
||||
# the model used when loading chatable_type column
|
||||
def self.polymorphic_class_for(name)
|
||||
CLASS_MAPPING[name] if CLASS_MAPPING.key?(name)
|
||||
end
|
||||
# the type stored in *_type column of polymorphic associations
|
||||
def self.polymorphic_name
|
||||
CLASS_MAPPING.invert.fetch(self) || super
|
||||
end
|
||||
|
||||
# TODO (martin) Remove this when we drop the ChatUpload table
|
||||
has_many :chat_uploads,
|
||||
dependent: :destroy,
|
||||
class_name: "Chat::Upload",
|
||||
foreign_key: :chat_message_id
|
||||
has_one :chat_webhook_event,
|
||||
dependent: :destroy,
|
||||
class_name: "Chat::WebhookEvent",
|
||||
foreign_key: :chat_message_id
|
||||
has_many :chat_mentions,
|
||||
dependent: :destroy,
|
||||
class_name: "Chat::Mention",
|
||||
foreign_key: :chat_message_id
|
||||
|
||||
scope :in_public_channel,
|
||||
-> {
|
||||
joins(:chat_channel).where(
|
||||
chat_channel: {
|
||||
chatable_type: Chat::Channel.public_channel_chatable_types,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
scope :in_dm_channel,
|
||||
-> {
|
||||
joins(:chat_channel).where(
|
||||
chat_channel: {
|
||||
chatable_type: Chat::Channel.direct_channel_chatable_types,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
scope :created_before, ->(date) { where("chat_messages.created_at < ?", date) }
|
||||
|
||||
before_save { ensure_last_editor_id }
|
||||
|
||||
def validate_message(has_uploads:)
|
||||
WatchedWordsValidator.new(attributes: [:message]).validate(self)
|
||||
|
||||
if self.new_record? || self.changed.include?("message")
|
||||
Chat::DuplicateMessageValidator.new(self).validate
|
||||
end
|
||||
|
||||
if !has_uploads && message_too_short?
|
||||
self.errors.add(
|
||||
:base,
|
||||
I18n.t(
|
||||
"chat.errors.minimum_length_not_met",
|
||||
count: SiteSetting.chat_minimum_message_length,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
if message_too_long?
|
||||
self.errors.add(
|
||||
:base,
|
||||
I18n.t("chat.errors.message_too_long", count: SiteSetting.chat_maximum_message_length),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def attach_uploads(uploads)
|
||||
return if uploads.blank? || self.new_record?
|
||||
|
||||
now = Time.now
|
||||
ref_record_attrs =
|
||||
uploads.map do |upload|
|
||||
{
|
||||
upload_id: upload.id,
|
||||
target_id: self.id,
|
||||
target_type: self.class.sti_name,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
end
|
||||
UploadReference.insert_all!(ref_record_attrs)
|
||||
end
|
||||
|
||||
def excerpt(max_length: 50)
|
||||
# just show the URL if the whole message is a URL, because we cannot excerpt oneboxes
|
||||
return message if UrlHelper.relaxed_parse(message).is_a?(URI)
|
||||
|
||||
# upload-only messages are better represented as the filename
|
||||
return uploads.first.original_filename if cooked.blank? && uploads.present?
|
||||
|
||||
# this may return blank for some complex things like quotes, that is acceptable
|
||||
PrettyText.excerpt(message, max_length, { text_entities: true })
|
||||
end
|
||||
|
||||
def cooked_for_excerpt
|
||||
(cooked.blank? && uploads.present?) ? "<p>#{uploads.first.original_filename}</p>" : cooked
|
||||
end
|
||||
|
||||
def push_notification_excerpt
|
||||
Emoji.gsub_emoji_to_unicode(message).truncate(400)
|
||||
end
|
||||
|
||||
def to_markdown
|
||||
upload_markdown =
|
||||
self
|
||||
.upload_references
|
||||
.includes(:upload)
|
||||
.order(:created_at)
|
||||
.map(&:to_markdown)
|
||||
.reject(&:empty?)
|
||||
|
||||
return self.message if upload_markdown.empty?
|
||||
|
||||
return ["#{self.message}\n"].concat(upload_markdown).join("\n") if self.message.present?
|
||||
|
||||
upload_markdown.join("\n")
|
||||
end
|
||||
|
||||
def cook
|
||||
ensure_last_editor_id
|
||||
|
||||
self.cooked = self.class.cook(self.message, user_id: self.last_editor_id)
|
||||
self.cooked_version = BAKED_VERSION
|
||||
end
|
||||
|
||||
def rebake!(invalidate_oneboxes: false, priority: nil)
|
||||
ensure_last_editor_id
|
||||
|
||||
previous_cooked = self.cooked
|
||||
new_cooked =
|
||||
self.class.cook(
|
||||
message,
|
||||
invalidate_oneboxes: invalidate_oneboxes,
|
||||
user_id: self.last_editor_id,
|
||||
)
|
||||
update_columns(cooked: new_cooked, cooked_version: BAKED_VERSION)
|
||||
args = { chat_message_id: self.id }
|
||||
args[:queue] = priority.to_s if priority && priority != :normal
|
||||
args[:is_dirty] = true if previous_cooked != new_cooked
|
||||
|
||||
Jobs.enqueue(Jobs::Chat::ProcessMessage, args)
|
||||
end
|
||||
|
||||
def self.uncooked
|
||||
where("cooked_version <> ? or cooked_version IS NULL", BAKED_VERSION)
|
||||
end
|
||||
|
||||
MARKDOWN_FEATURES = %w[
|
||||
anchor
|
||||
bbcode-block
|
||||
bbcode-inline
|
||||
code
|
||||
category-hashtag
|
||||
censored
|
||||
chat-transcript
|
||||
discourse-local-dates
|
||||
emoji
|
||||
emojiShortcuts
|
||||
inlineEmoji
|
||||
html-img
|
||||
hashtag-autocomplete
|
||||
mentions
|
||||
unicodeUsernames
|
||||
onebox
|
||||
quotes
|
||||
spoiler-alert
|
||||
table
|
||||
text-post-process
|
||||
upload-protocol
|
||||
watched-words
|
||||
]
|
||||
|
||||
MARKDOWN_IT_RULES = %w[
|
||||
autolink
|
||||
list
|
||||
backticks
|
||||
newline
|
||||
code
|
||||
fence
|
||||
image
|
||||
table
|
||||
linkify
|
||||
link
|
||||
strikethrough
|
||||
blockquote
|
||||
emphasis
|
||||
]
|
||||
|
||||
def self.cook(message, opts = {})
|
||||
# A rule in our Markdown pipeline may have Guardian checks that require a
|
||||
# user to be present. The last editing user of the message will be more
|
||||
# generally up to date than the creating user. For example, we use
|
||||
# this when cooking #hashtags to determine whether we should render
|
||||
# the found hashtag based on whether the user can access the channel it
|
||||
# is referencing.
|
||||
cooked =
|
||||
PrettyText.cook(
|
||||
message,
|
||||
features_override:
|
||||
MARKDOWN_FEATURES + DiscoursePluginRegistry.chat_markdown_features.to_a,
|
||||
markdown_it_rules: MARKDOWN_IT_RULES,
|
||||
force_quote_link: true,
|
||||
user_id: opts[:user_id],
|
||||
hashtag_context: "chat-composer",
|
||||
)
|
||||
|
||||
result =
|
||||
Oneboxer.apply(cooked) do |url|
|
||||
if opts[:invalidate_oneboxes]
|
||||
Oneboxer.invalidate(url)
|
||||
InlineOneboxer.invalidate(url)
|
||||
end
|
||||
onebox = Oneboxer.cached_onebox(url)
|
||||
onebox
|
||||
end
|
||||
|
||||
cooked = result.to_html if result.changed?
|
||||
cooked
|
||||
end
|
||||
|
||||
def full_url
|
||||
"#{Discourse.base_url}#{url}"
|
||||
end
|
||||
|
||||
def url
|
||||
"/chat/c/-/#{self.chat_channel_id}/#{self.id}"
|
||||
end
|
||||
|
||||
def create_mentions(user_ids)
|
||||
return if user_ids.empty?
|
||||
|
||||
now = Time.zone.now
|
||||
mentions = []
|
||||
User
|
||||
.where(id: user_ids)
|
||||
.find_each do |user|
|
||||
mentions << {
|
||||
chat_message_id: self.id,
|
||||
user_id: user.id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
end
|
||||
|
||||
Chat::Mention.insert_all(mentions)
|
||||
end
|
||||
|
||||
def update_mentions(mentioned_user_ids)
|
||||
old_mentions = chat_mentions.pluck(:user_id)
|
||||
updated_mentions = mentioned_user_ids
|
||||
mentioned_user_ids_to_drop = old_mentions - updated_mentions
|
||||
mentioned_user_ids_to_add = updated_mentions - old_mentions
|
||||
|
||||
delete_mentions(mentioned_user_ids_to_drop)
|
||||
create_mentions(mentioned_user_ids_to_add)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_mentions(user_ids)
|
||||
chat_mentions.where(user_id: user_ids).destroy_all
|
||||
end
|
||||
|
||||
def message_too_short?
|
||||
message.length < SiteSetting.chat_minimum_message_length
|
||||
end
|
||||
|
||||
def message_too_long?
|
||||
message.length > SiteSetting.chat_maximum_message_length
|
||||
end
|
||||
|
||||
def ensure_last_editor_id
|
||||
self.last_editor_id ||= self.user_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: chat_messages
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# chat_channel_id :integer not null
|
||||
# user_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# deleted_at :datetime
|
||||
# deleted_by_id :integer
|
||||
# in_reply_to_id :integer
|
||||
# message :text
|
||||
# cooked :text
|
||||
# cooked_version :integer
|
||||
# last_editor_id :integer not null
|
||||
# thread_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# idx_chat_messages_by_created_at_not_deleted (created_at) WHERE (deleted_at IS NULL)
|
||||
# index_chat_messages_on_chat_channel_id_and_created_at (chat_channel_id,created_at)
|
||||
# index_chat_messages_on_chat_channel_id_and_id (chat_channel_id,id) WHERE (deleted_at IS NULL)
|
||||
# index_chat_messages_on_last_editor_id (last_editor_id)
|
||||
# index_chat_messages_on_thread_id (thread_id)
|
||||
#
|
|
@ -1,8 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatMessageReaction < ActiveRecord::Base
|
||||
belongs_to :chat_message
|
||||
belongs_to :user
|
||||
module Chat
|
||||
class MessageReaction < ActiveRecord::Base
|
||||
self.table_name = "chat_message_reactions"
|
||||
|
||||
belongs_to :chat_message, class_name: "Chat::Message"
|
||||
belongs_to :user
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -1,8 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatMessageRevision < ActiveRecord::Base
|
||||
belongs_to :chat_message
|
||||
belongs_to :user
|
||||
module Chat
|
||||
class MessageRevision < ActiveRecord::Base
|
||||
self.table_name = "chat_message_revisions"
|
||||
|
||||
belongs_to :chat_message, class_name: "Chat::Message"
|
||||
belongs_to :user
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -0,0 +1,159 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ReviewableMessage < Reviewable
|
||||
def serializer
|
||||
Chat::ReviewableMessageSerializer
|
||||
end
|
||||
|
||||
def self.action_aliases
|
||||
{
|
||||
agree_and_keep_hidden: :agree_and_delete,
|
||||
agree_and_silence: :agree_and_delete,
|
||||
agree_and_suspend: :agree_and_delete,
|
||||
delete_and_agree: :agree_and_delete,
|
||||
}
|
||||
end
|
||||
|
||||
def self.score_to_silence_user
|
||||
sensitivity_score(SiteSetting.chat_silence_user_sensitivity, scale: 0.6)
|
||||
end
|
||||
|
||||
def chat_message
|
||||
@chat_message ||= (target || Chat::Message.with_deleted.find_by(id: target_id))
|
||||
end
|
||||
|
||||
def chat_message_creator
|
||||
@chat_message_creator ||= chat_message.user
|
||||
end
|
||||
|
||||
def flagged_by_user_ids
|
||||
@flagged_by_user_ids ||= reviewable_scores.map(&:user_id)
|
||||
end
|
||||
|
||||
def post
|
||||
nil
|
||||
end
|
||||
|
||||
def build_actions(actions, guardian, args)
|
||||
return unless pending?
|
||||
return if chat_message.blank?
|
||||
|
||||
agree =
|
||||
actions.add_bundle(
|
||||
"#{id}-agree",
|
||||
icon: "thumbs-up",
|
||||
label: "reviewables.actions.agree.title",
|
||||
)
|
||||
|
||||
if chat_message.deleted_at?
|
||||
build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree)
|
||||
build_action(actions, :agree_and_keep_deleted, icon: "thumbs-up", bundle: agree)
|
||||
build_action(actions, :disagree_and_restore, icon: "thumbs-down")
|
||||
else
|
||||
build_action(actions, :agree_and_delete, icon: "far-eye-slash", bundle: agree)
|
||||
build_action(actions, :agree_and_keep_message, icon: "thumbs-up", bundle: agree)
|
||||
build_action(actions, :disagree, icon: "thumbs-down")
|
||||
end
|
||||
|
||||
if guardian.can_suspend?(chat_message_creator)
|
||||
build_action(
|
||||
actions,
|
||||
:agree_and_suspend,
|
||||
icon: "ban",
|
||||
bundle: agree,
|
||||
client_action: "suspend",
|
||||
)
|
||||
build_action(
|
||||
actions,
|
||||
:agree_and_silence,
|
||||
icon: "microphone-slash",
|
||||
bundle: agree,
|
||||
client_action: "silence",
|
||||
)
|
||||
end
|
||||
|
||||
build_action(actions, :ignore, icon: "external-link-alt")
|
||||
|
||||
unless chat_message.deleted_at?
|
||||
build_action(actions, :delete_and_agree, icon: "far-trash-alt")
|
||||
end
|
||||
end
|
||||
|
||||
def perform_agree_and_keep_message(performed_by, args)
|
||||
agree
|
||||
end
|
||||
|
||||
def perform_agree_and_restore(performed_by, args)
|
||||
agree { chat_message.recover! }
|
||||
end
|
||||
|
||||
def perform_agree_and_delete(performed_by, args)
|
||||
agree { chat_message.trash!(performed_by) }
|
||||
end
|
||||
|
||||
def perform_disagree_and_restore(performed_by, args)
|
||||
disagree { chat_message.recover! }
|
||||
end
|
||||
|
||||
def perform_disagree(performed_by, args)
|
||||
disagree
|
||||
end
|
||||
|
||||
def perform_ignore(performed_by, args)
|
||||
ignore
|
||||
end
|
||||
|
||||
def perform_delete_and_ignore(performed_by, args)
|
||||
ignore { chat_message.trash!(performed_by) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def agree
|
||||
yield if block_given?
|
||||
create_result(:success, :approved) do |result|
|
||||
result.update_flag_stats = { status: :agreed, user_ids: flagged_by_user_ids }
|
||||
result.recalculate_score = true
|
||||
end
|
||||
end
|
||||
|
||||
def disagree
|
||||
yield if block_given?
|
||||
|
||||
UserSilencer.unsilence(chat_message_creator)
|
||||
|
||||
create_result(:success, :rejected) do |result|
|
||||
result.update_flag_stats = { status: :disagreed, user_ids: flagged_by_user_ids }
|
||||
result.recalculate_score = true
|
||||
end
|
||||
end
|
||||
|
||||
def ignore
|
||||
yield if block_given?
|
||||
create_result(:success, :ignored) do |result|
|
||||
result.update_flag_stats = { status: :ignored, user_ids: flagged_by_user_ids }
|
||||
end
|
||||
end
|
||||
|
||||
def build_action(
|
||||
actions,
|
||||
id,
|
||||
icon:,
|
||||
button_class: nil,
|
||||
bundle: nil,
|
||||
client_action: nil,
|
||||
confirm: false
|
||||
)
|
||||
actions.add(id, bundle: bundle) do |action|
|
||||
prefix = "reviewables.actions.#{id}"
|
||||
action.icon = icon
|
||||
action.button_class = button_class
|
||||
action.label = "chat.#{prefix}.title"
|
||||
action.description = "chat.#{prefix}.description"
|
||||
action.client_action = client_action
|
||||
action.confirm_message = "#{prefix}.confirm" if confirm
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,29 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatThread < ActiveRecord::Base
|
||||
EXCERPT_LENGTH = 150
|
||||
module Chat
|
||||
class Thread < ActiveRecord::Base
|
||||
EXCERPT_LENGTH = 150
|
||||
|
||||
belongs_to :channel, foreign_key: "channel_id", class_name: "ChatChannel"
|
||||
belongs_to :original_message_user, foreign_key: "original_message_user_id", class_name: "User"
|
||||
belongs_to :original_message, foreign_key: "original_message_id", class_name: "ChatMessage"
|
||||
self.table_name = "chat_threads"
|
||||
|
||||
has_many :chat_messages,
|
||||
-> { order("chat_messages.created_at ASC, chat_messages.id ASC") },
|
||||
foreign_key: :thread_id,
|
||||
primary_key: :id
|
||||
belongs_to :channel, foreign_key: "channel_id", class_name: "Chat::Channel"
|
||||
belongs_to :original_message_user, foreign_key: "original_message_user_id", class_name: "User"
|
||||
belongs_to :original_message, foreign_key: "original_message_id", class_name: "Chat::Message"
|
||||
|
||||
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
|
||||
has_many :chat_messages,
|
||||
-> { order("chat_messages.created_at ASC, chat_messages.id ASC") },
|
||||
foreign_key: :thread_id,
|
||||
primary_key: :id,
|
||||
class_name: "Chat::Message"
|
||||
|
||||
def url
|
||||
"#{channel.url}/t/#{self.id}"
|
||||
end
|
||||
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
|
||||
|
||||
def relative_url
|
||||
"#{channel.relative_url}/t/#{self.id}"
|
||||
end
|
||||
def url
|
||||
"#{channel.url}/t/#{self.id}"
|
||||
end
|
||||
|
||||
def excerpt
|
||||
original_message.excerpt(max_length: EXCERPT_LENGTH)
|
||||
def relative_url
|
||||
"#{channel.relative_url}/t/#{self.id}"
|
||||
end
|
||||
|
||||
def excerpt
|
||||
original_message.excerpt(max_length: EXCERPT_LENGTH)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,11 +5,15 @@
|
|||
#
|
||||
# NOTE: Do not use this model anymore, chat messages are linked to uploads via
|
||||
# the UploadReference table now, just like everything else.
|
||||
class ChatUpload < ActiveRecord::Base
|
||||
belongs_to :chat_message
|
||||
belongs_to :upload
|
||||
module Chat
|
||||
class Upload < ActiveRecord::Base
|
||||
self.table_name = "chat_uploads"
|
||||
|
||||
deprecate *public_instance_methods(false)
|
||||
belongs_to :chat_message, class_name: "Chat::Message"
|
||||
belongs_to :upload
|
||||
|
||||
deprecate *public_instance_methods(false)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -1,18 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserChatChannelMembership < ActiveRecord::Base
|
||||
NOTIFICATION_LEVELS = { never: 0, mention: 1, always: 2 }
|
||||
module Chat
|
||||
class UserChatChannelMembership < ActiveRecord::Base
|
||||
self.table_name = "user_chat_channel_memberships"
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :chat_channel
|
||||
belongs_to :last_read_message, class_name: "ChatMessage", optional: true
|
||||
NOTIFICATION_LEVELS = { never: 0, mention: 1, always: 2 }
|
||||
|
||||
enum :desktop_notification_level, NOTIFICATION_LEVELS, prefix: :desktop_notifications
|
||||
enum :mobile_notification_level, NOTIFICATION_LEVELS, prefix: :mobile_notifications
|
||||
enum :join_mode, { manual: 0, automatic: 1 }
|
||||
belongs_to :user
|
||||
belongs_to :last_read_message, class_name: "Chat::Message", optional: true
|
||||
belongs_to :chat_channel, class_name: "Chat::Channel", foreign_key: :chat_channel_id
|
||||
|
||||
attribute :unread_count, default: 0
|
||||
attribute :unread_mentions, default: 0
|
||||
enum :desktop_notification_level, NOTIFICATION_LEVELS, prefix: :desktop_notifications
|
||||
enum :mobile_notification_level, NOTIFICATION_LEVELS, prefix: :mobile_notifications
|
||||
enum :join_mode, { manual: 0, automatic: 1 }
|
||||
|
||||
attribute :unread_count, default: 0
|
||||
attribute :unread_mentions, default: 0
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class View
|
||||
attr_reader :user, :chat_channel, :chat_messages, :can_load_more_past, :can_load_more_future
|
||||
|
||||
def initialize(
|
||||
chat_channel:,
|
||||
chat_messages:,
|
||||
user:,
|
||||
can_load_more_past: nil,
|
||||
can_load_more_future: nil
|
||||
)
|
||||
@chat_channel = chat_channel
|
||||
@chat_messages = chat_messages
|
||||
@user = user
|
||||
@can_load_more_past = can_load_more_past
|
||||
@can_load_more_future = can_load_more_future
|
||||
end
|
||||
|
||||
def reviewable_ids
|
||||
return @reviewable_ids if defined?(@reviewable_ids)
|
||||
|
||||
@reviewable_ids = @user.staff? ? get_reviewable_ids : nil
|
||||
end
|
||||
|
||||
def user_flag_statuses
|
||||
return @user_flag_statuses if defined?(@user_flag_statuses)
|
||||
|
||||
@user_flag_statuses = get_user_flag_statuses
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_reviewable_ids
|
||||
sql = <<~SQL
|
||||
SELECT
|
||||
target_id,
|
||||
MAX(r.id) reviewable_id
|
||||
FROM
|
||||
reviewables r
|
||||
JOIN
|
||||
reviewable_scores s ON reviewable_id = r.id
|
||||
WHERE
|
||||
r.target_id IN (:message_ids) AND
|
||||
r.target_type = :target_type AND
|
||||
s.status = :pending
|
||||
GROUP BY
|
||||
target_id
|
||||
SQL
|
||||
|
||||
ids = {}
|
||||
|
||||
DB
|
||||
.query(
|
||||
sql,
|
||||
pending: ReviewableScore.statuses[:pending],
|
||||
message_ids: @chat_messages.map(&:id),
|
||||
target_type: Chat::Message.sti_name,
|
||||
)
|
||||
.each { |row| ids[row.target_id] = row.reviewable_id }
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
def get_user_flag_statuses
|
||||
sql = <<~SQL
|
||||
SELECT
|
||||
target_id,
|
||||
s.status
|
||||
FROM
|
||||
reviewables r
|
||||
JOIN
|
||||
reviewable_scores s ON reviewable_id = r.id
|
||||
WHERE
|
||||
s.user_id = :user_id AND
|
||||
r.target_id IN (:message_ids) AND
|
||||
r.target_type = :target_type
|
||||
SQL
|
||||
|
||||
statuses = {}
|
||||
|
||||
DB
|
||||
.query(
|
||||
sql,
|
||||
message_ids: @chat_messages.map(&:id),
|
||||
user_id: @user.id,
|
||||
target_type: Chat::Message.sti_name,
|
||||
)
|
||||
.each { |row| statuses[row.target_id] = row.status }
|
||||
|
||||
statuses
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatWebhookEvent < ActiveRecord::Base
|
||||
belongs_to :chat_message
|
||||
belongs_to :incoming_chat_webhook
|
||||
module Chat
|
||||
class WebhookEvent < ActiveRecord::Base
|
||||
self.table_name = "chat_webhook_events"
|
||||
|
||||
delegate :username, to: :incoming_chat_webhook
|
||||
delegate :emoji, to: :incoming_chat_webhook
|
||||
belongs_to :chat_message, class_name: "Chat::Message"
|
||||
belongs_to :incoming_chat_webhook, class_name: "Chat::IncomingWebhook"
|
||||
|
||||
delegate :username, to: :incoming_chat_webhook
|
||||
delegate :emoji, to: :incoming_chat_webhook
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
|
@ -1,176 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatChannel < ActiveRecord::Base
|
||||
include Trashable
|
||||
|
||||
belongs_to :chatable, polymorphic: true
|
||||
belongs_to :direct_message,
|
||||
-> { where(chat_channels: { chatable_type: "DirectMessage" }) },
|
||||
foreign_key: "chatable_id"
|
||||
|
||||
has_many :chat_messages
|
||||
has_many :user_chat_channel_memberships
|
||||
|
||||
has_one :chat_channel_archive
|
||||
|
||||
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
|
||||
|
||||
validates :name,
|
||||
length: {
|
||||
maximum: Proc.new { SiteSetting.max_topic_title_length },
|
||||
},
|
||||
presence: true,
|
||||
allow_nil: true
|
||||
validate :ensure_slug_ok, if: :slug_changed?
|
||||
before_validation :generate_auto_slug
|
||||
|
||||
scope :public_channels,
|
||||
-> {
|
||||
where(chatable_type: public_channel_chatable_types).where(
|
||||
"categories.id IS NOT NULL",
|
||||
).joins(
|
||||
"LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
|
||||
)
|
||||
}
|
||||
|
||||
delegate :empty?, to: :chat_messages, prefix: true
|
||||
|
||||
class << self
|
||||
def editable_statuses
|
||||
statuses.filter { |k, _| !%w[read_only archived].include?(k) }
|
||||
end
|
||||
|
||||
def public_channel_chatable_types
|
||||
["Category"]
|
||||
end
|
||||
|
||||
def chatable_types
|
||||
public_channel_chatable_types << "DirectMessage"
|
||||
end
|
||||
end
|
||||
|
||||
statuses.keys.each do |status|
|
||||
define_method("#{status}!") { |acting_user| change_status(acting_user, status.to_sym) }
|
||||
end
|
||||
|
||||
%i[
|
||||
category_channel?
|
||||
direct_message_channel?
|
||||
public_channel?
|
||||
chatable_has_custom_fields?
|
||||
read_restricted?
|
||||
].each { |name| define_method(name) { false } }
|
||||
|
||||
%i[allowed_user_ids allowed_group_ids chatable_url].each { |name| define_method(name) { nil } }
|
||||
|
||||
def membership_for(user)
|
||||
user_chat_channel_memberships.find_by(user: user)
|
||||
end
|
||||
|
||||
def add(user)
|
||||
Chat::ChatChannelMembershipManager.new(self).follow(user)
|
||||
end
|
||||
|
||||
def remove(user)
|
||||
Chat::ChatChannelMembershipManager.new(self).unfollow(user)
|
||||
end
|
||||
|
||||
def url
|
||||
"#{Discourse.base_url}/chat/c/#{self.slug || "-"}/#{self.id}"
|
||||
end
|
||||
|
||||
def relative_url
|
||||
"#{Discourse.base_path}/chat/c/#{self.slug || "-"}/#{self.id}"
|
||||
end
|
||||
|
||||
def self.ensure_consistency!
|
||||
update_counts
|
||||
end
|
||||
|
||||
# TODO (martin) Move UpdateUserCountsForChatChannels into here
|
||||
def self.update_counts
|
||||
# NOTE: ChatChannel#messages_count is not updated every time
|
||||
# a message is created or deleted in a channel, so it should not
|
||||
# be displayed in the UI. It is updated eventually via Jobs::ChatPeriodicalUpdates
|
||||
DB.exec <<~SQL
|
||||
UPDATE chat_channels channels
|
||||
SET messages_count = subquery.messages_count
|
||||
FROM (
|
||||
SELECT COUNT(*) AS messages_count, chat_channel_id
|
||||
FROM chat_messages
|
||||
WHERE chat_messages.deleted_at IS NULL
|
||||
GROUP BY chat_channel_id
|
||||
) subquery
|
||||
WHERE channels.id = subquery.chat_channel_id
|
||||
AND channels.deleted_at IS NULL
|
||||
AND subquery.messages_count != channels.messages_count
|
||||
SQL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def change_status(acting_user, target_status)
|
||||
return if !Guardian.new(acting_user).can_change_channel_status?(self, target_status)
|
||||
self.update!(status: target_status)
|
||||
log_channel_status_change(acting_user: acting_user)
|
||||
end
|
||||
|
||||
def log_channel_status_change(acting_user:)
|
||||
DiscourseEvent.trigger(
|
||||
:chat_channel_status_change,
|
||||
channel: self,
|
||||
old_status: status_previously_was,
|
||||
new_status: status,
|
||||
)
|
||||
|
||||
StaffActionLogger.new(acting_user).log_custom(
|
||||
"chat_channel_status_change",
|
||||
{
|
||||
chat_channel_id: self.id,
|
||||
chat_channel_name: self.name,
|
||||
previous_value: status_previously_was,
|
||||
new_value: status,
|
||||
},
|
||||
)
|
||||
|
||||
ChatPublisher.publish_channel_status(self)
|
||||
end
|
||||
|
||||
def duplicate_slug?
|
||||
ChatChannel.where(slug: self.slug).where.not(id: self.id).any?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: chat_channels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# chatable_id :integer not null
|
||||
# deleted_at :datetime
|
||||
# deleted_by_id :integer
|
||||
# featured_in_category_id :integer
|
||||
# delete_after_seconds :integer
|
||||
# chatable_type :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# name :string
|
||||
# description :text
|
||||
# status :integer default("open"), not null
|
||||
# user_count :integer default(0), not null
|
||||
# last_message_sent_at :datetime not null
|
||||
# auto_join_users :boolean default(FALSE), not null
|
||||
# allow_channel_wide_mentions :boolean default(TRUE), not null
|
||||
# user_count_stale :boolean default(FALSE), not null
|
||||
# slug :string
|
||||
# type :string
|
||||
# threading_enabled :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_chat_channels_on_messages_count (messages_count)
|
||||
# index_chat_channels_on_chatable_id (chatable_id)
|
||||
# index_chat_channels_on_chatable_id_and_chatable_type (chatable_id,chatable_type)
|
||||
# index_chat_channels_on_slug (slug) UNIQUE
|
||||
# index_chat_channels_on_status (status)
|
||||
#
|
|
@ -1,297 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatMessage < ActiveRecord::Base
|
||||
include Trashable
|
||||
attribute :has_oneboxes, default: false
|
||||
|
||||
BAKED_VERSION = 2
|
||||
|
||||
belongs_to :chat_channel
|
||||
belongs_to :user
|
||||
belongs_to :in_reply_to, class_name: "ChatMessage"
|
||||
belongs_to :last_editor, class_name: "User"
|
||||
belongs_to :thread, class_name: "ChatThread"
|
||||
|
||||
has_many :replies, class_name: "ChatMessage", foreign_key: "in_reply_to_id", dependent: :nullify
|
||||
has_many :revisions, class_name: "ChatMessageRevision", dependent: :destroy
|
||||
has_many :reactions, class_name: "ChatMessageReaction", dependent: :destroy
|
||||
has_many :bookmarks, as: :bookmarkable, dependent: :destroy
|
||||
has_many :upload_references, as: :target, dependent: :destroy
|
||||
has_many :uploads, through: :upload_references
|
||||
|
||||
# TODO (martin) Remove this when we drop the ChatUpload table
|
||||
has_many :chat_uploads, dependent: :destroy
|
||||
has_one :chat_webhook_event, dependent: :destroy
|
||||
has_many :chat_mentions, dependent: :destroy
|
||||
|
||||
scope :in_public_channel,
|
||||
-> {
|
||||
joins(:chat_channel).where(
|
||||
chat_channel: {
|
||||
chatable_type: ChatChannel.public_channel_chatable_types,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
scope :in_dm_channel,
|
||||
-> { joins(:chat_channel).where(chat_channel: { chatable_type: "DirectMessage" }) }
|
||||
|
||||
scope :created_before, ->(date) { where("chat_messages.created_at < ?", date) }
|
||||
|
||||
before_save { ensure_last_editor_id }
|
||||
|
||||
def validate_message(has_uploads:)
|
||||
WatchedWordsValidator.new(attributes: [:message]).validate(self)
|
||||
|
||||
if self.new_record? || self.changed.include?("message")
|
||||
Chat::DuplicateMessageValidator.new(self).validate
|
||||
end
|
||||
|
||||
if !has_uploads && message_too_short?
|
||||
self.errors.add(
|
||||
:base,
|
||||
I18n.t(
|
||||
"chat.errors.minimum_length_not_met",
|
||||
count: SiteSetting.chat_minimum_message_length,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
if message_too_long?
|
||||
self.errors.add(
|
||||
:base,
|
||||
I18n.t("chat.errors.message_too_long", count: SiteSetting.chat_maximum_message_length),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def attach_uploads(uploads)
|
||||
return if uploads.blank? || self.new_record?
|
||||
|
||||
now = Time.now
|
||||
ref_record_attrs =
|
||||
uploads.map do |upload|
|
||||
{
|
||||
upload_id: upload.id,
|
||||
target_id: self.id,
|
||||
target_type: "ChatMessage",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
end
|
||||
UploadReference.insert_all!(ref_record_attrs)
|
||||
end
|
||||
|
||||
def excerpt(max_length: 50)
|
||||
# just show the URL if the whole message is a URL, because we cannot excerpt oneboxes
|
||||
return message if UrlHelper.relaxed_parse(message).is_a?(URI)
|
||||
|
||||
# upload-only messages are better represented as the filename
|
||||
return uploads.first.original_filename if cooked.blank? && uploads.present?
|
||||
|
||||
# this may return blank for some complex things like quotes, that is acceptable
|
||||
PrettyText.excerpt(message, max_length, { text_entities: true })
|
||||
end
|
||||
|
||||
def cooked_for_excerpt
|
||||
(cooked.blank? && uploads.present?) ? "<p>#{uploads.first.original_filename}</p>" : cooked
|
||||
end
|
||||
|
||||
def push_notification_excerpt
|
||||
Emoji.gsub_emoji_to_unicode(message).truncate(400)
|
||||
end
|
||||
|
||||
def to_markdown
|
||||
upload_markdown =
|
||||
self
|
||||
.upload_references
|
||||
.includes(:upload)
|
||||
.order(:created_at)
|
||||
.map(&:to_markdown)
|
||||
.reject(&:empty?)
|
||||
|
||||
return self.message if upload_markdown.empty?
|
||||
|
||||
return ["#{self.message}\n"].concat(upload_markdown).join("\n") if self.message.present?
|
||||
|
||||
upload_markdown.join("\n")
|
||||
end
|
||||
|
||||
def cook
|
||||
ensure_last_editor_id
|
||||
|
||||
self.cooked = self.class.cook(self.message, user_id: self.last_editor_id)
|
||||
self.cooked_version = BAKED_VERSION
|
||||
end
|
||||
|
||||
def rebake!(invalidate_oneboxes: false, priority: nil)
|
||||
ensure_last_editor_id
|
||||
|
||||
previous_cooked = self.cooked
|
||||
new_cooked =
|
||||
self.class.cook(
|
||||
message,
|
||||
invalidate_oneboxes: invalidate_oneboxes,
|
||||
user_id: self.last_editor_id,
|
||||
)
|
||||
update_columns(cooked: new_cooked, cooked_version: BAKED_VERSION)
|
||||
args = { chat_message_id: self.id }
|
||||
args[:queue] = priority.to_s if priority && priority != :normal
|
||||
args[:is_dirty] = true if previous_cooked != new_cooked
|
||||
|
||||
Jobs.enqueue(:process_chat_message, args)
|
||||
end
|
||||
|
||||
def self.uncooked
|
||||
where("cooked_version <> ? or cooked_version IS NULL", BAKED_VERSION)
|
||||
end
|
||||
|
||||
MARKDOWN_FEATURES = %w[
|
||||
anchor
|
||||
bbcode-block
|
||||
bbcode-inline
|
||||
code
|
||||
category-hashtag
|
||||
censored
|
||||
chat-transcript
|
||||
discourse-local-dates
|
||||
emoji
|
||||
emojiShortcuts
|
||||
inlineEmoji
|
||||
html-img
|
||||
hashtag-autocomplete
|
||||
mentions
|
||||
unicodeUsernames
|
||||
onebox
|
||||
quotes
|
||||
spoiler-alert
|
||||
table
|
||||
text-post-process
|
||||
upload-protocol
|
||||
watched-words
|
||||
]
|
||||
|
||||
MARKDOWN_IT_RULES = %w[
|
||||
autolink
|
||||
list
|
||||
backticks
|
||||
newline
|
||||
code
|
||||
fence
|
||||
image
|
||||
table
|
||||
linkify
|
||||
link
|
||||
strikethrough
|
||||
blockquote
|
||||
emphasis
|
||||
]
|
||||
|
||||
def self.cook(message, opts = {})
|
||||
# A rule in our Markdown pipeline may have Guardian checks that require a
|
||||
# user to be present. The last editing user of the message will be more
|
||||
# generally up to date than the creating user. For example, we use
|
||||
# this when cooking #hashtags to determine whether we should render
|
||||
# the found hashtag based on whether the user can access the channel it
|
||||
# is referencing.
|
||||
cooked =
|
||||
PrettyText.cook(
|
||||
message,
|
||||
features_override: MARKDOWN_FEATURES + DiscoursePluginRegistry.chat_markdown_features.to_a,
|
||||
markdown_it_rules: MARKDOWN_IT_RULES,
|
||||
force_quote_link: true,
|
||||
user_id: opts[:user_id],
|
||||
hashtag_context: "chat-composer",
|
||||
)
|
||||
|
||||
result =
|
||||
Oneboxer.apply(cooked) do |url|
|
||||
if opts[:invalidate_oneboxes]
|
||||
Oneboxer.invalidate(url)
|
||||
InlineOneboxer.invalidate(url)
|
||||
end
|
||||
onebox = Oneboxer.cached_onebox(url)
|
||||
onebox
|
||||
end
|
||||
|
||||
cooked = result.to_html if result.changed?
|
||||
cooked
|
||||
end
|
||||
|
||||
def full_url
|
||||
"#{Discourse.base_url}#{url}"
|
||||
end
|
||||
|
||||
def url
|
||||
"/chat/c/-/#{self.chat_channel_id}/#{self.id}"
|
||||
end
|
||||
|
||||
def create_mentions(user_ids)
|
||||
return if user_ids.empty?
|
||||
|
||||
now = Time.zone.now
|
||||
mentions = []
|
||||
User
|
||||
.where(id: user_ids)
|
||||
.find_each do |user|
|
||||
mentions << { chat_message_id: self.id, user_id: user.id, created_at: now, updated_at: now }
|
||||
end
|
||||
|
||||
ChatMention.insert_all(mentions)
|
||||
end
|
||||
|
||||
def update_mentions(mentioned_user_ids)
|
||||
old_mentions = chat_mentions.pluck(:user_id)
|
||||
updated_mentions = mentioned_user_ids
|
||||
mentioned_user_ids_to_drop = old_mentions - updated_mentions
|
||||
mentioned_user_ids_to_add = updated_mentions - old_mentions
|
||||
|
||||
delete_mentions(mentioned_user_ids_to_drop)
|
||||
create_mentions(mentioned_user_ids_to_add)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_mentions(user_ids)
|
||||
chat_mentions.where(user_id: user_ids).destroy_all
|
||||
end
|
||||
|
||||
def message_too_short?
|
||||
message.length < SiteSetting.chat_minimum_message_length
|
||||
end
|
||||
|
||||
def message_too_long?
|
||||
message.length > SiteSetting.chat_maximum_message_length
|
||||
end
|
||||
|
||||
def ensure_last_editor_id
|
||||
self.last_editor_id ||= self.user_id
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: chat_messages
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# chat_channel_id :integer not null
|
||||
# user_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# deleted_at :datetime
|
||||
# deleted_by_id :integer
|
||||
# in_reply_to_id :integer
|
||||
# message :text
|
||||
# cooked :text
|
||||
# cooked_version :integer
|
||||
# last_editor_id :integer not null
|
||||
# thread_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# idx_chat_messages_by_created_at_not_deleted (created_at) WHERE (deleted_at IS NULL)
|
||||
# index_chat_messages_on_chat_channel_id_and_created_at (chat_channel_id,created_at)
|
||||
# index_chat_messages_on_chat_channel_id_and_id (chat_channel_id,id) WHERE (deleted_at IS NULL)
|
||||
# index_chat_messages_on_last_editor_id (last_editor_id)
|
||||
# index_chat_messages_on_thread_id (thread_id)
|
||||
#
|
|
@ -1,87 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatView
|
||||
attr_reader :user, :chat_channel, :chat_messages, :can_load_more_past, :can_load_more_future
|
||||
|
||||
def initialize(
|
||||
chat_channel:,
|
||||
chat_messages:,
|
||||
user:,
|
||||
can_load_more_past: nil,
|
||||
can_load_more_future: nil
|
||||
)
|
||||
@chat_channel = chat_channel
|
||||
@chat_messages = chat_messages
|
||||
@user = user
|
||||
@can_load_more_past = can_load_more_past
|
||||
@can_load_more_future = can_load_more_future
|
||||
end
|
||||
|
||||
def reviewable_ids
|
||||
return @reviewable_ids if defined?(@reviewable_ids)
|
||||
|
||||
@reviewable_ids = @user.staff? ? get_reviewable_ids : nil
|
||||
end
|
||||
|
||||
def user_flag_statuses
|
||||
return @user_flag_statuses if defined?(@user_flag_statuses)
|
||||
|
||||
@user_flag_statuses = get_user_flag_statuses
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_reviewable_ids
|
||||
sql = <<~SQL
|
||||
SELECT
|
||||
target_id,
|
||||
MAX(r.id) reviewable_id
|
||||
FROM
|
||||
reviewables r
|
||||
JOIN
|
||||
reviewable_scores s ON reviewable_id = r.id
|
||||
WHERE
|
||||
r.target_id IN (:message_ids) AND
|
||||
r.target_type = 'ChatMessage' AND
|
||||
s.status = :pending
|
||||
GROUP BY
|
||||
target_id
|
||||
SQL
|
||||
|
||||
ids = {}
|
||||
|
||||
DB
|
||||
.query(
|
||||
sql,
|
||||
pending: ReviewableScore.statuses[:pending],
|
||||
message_ids: @chat_messages.map(&:id),
|
||||
)
|
||||
.each { |row| ids[row.target_id] = row.reviewable_id }
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
def get_user_flag_statuses
|
||||
sql = <<~SQL
|
||||
SELECT
|
||||
target_id,
|
||||
s.status
|
||||
FROM
|
||||
reviewables r
|
||||
JOIN
|
||||
reviewable_scores s ON reviewable_id = r.id
|
||||
WHERE
|
||||
s.user_id = :user_id AND
|
||||
r.target_id IN (:message_ids) AND
|
||||
r.target_type = 'ChatMessage'
|
||||
SQL
|
||||
|
||||
statuses = {}
|
||||
|
||||
DB
|
||||
.query(sql, message_ids: @chat_messages.map(&:id), user_id: @user.id)
|
||||
.each { |row| statuses[row.target_id] = row.status }
|
||||
|
||||
statuses
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module Chatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
STI_CLASS_MAPPING = {
|
||||
"CategoryChannel" => Chat::CategoryChannel,
|
||||
"DirectMessageChannel" => Chat::DirectMessageChannel,
|
||||
}
|
||||
|
||||
# the model used when loading type column
|
||||
def self.sti_class_for(name)
|
||||
STI_CLASS_MAPPING[name] if STI_CLASS_MAPPING.key?(name)
|
||||
end
|
||||
|
||||
# the type column value
|
||||
def self.sti_name_for(klass)
|
||||
STI_CLASS_MAPPING.invert.fetch(klass)
|
||||
end
|
||||
|
||||
POLYMORPHIC_CLASS_MAPPING = { "DirectMessage" => Chat::DirectMessage }
|
||||
|
||||
# the model used when loading chatable_type column
|
||||
def self.polymorphic_class_for(name)
|
||||
POLYMORPHIC_CLASS_MAPPING[name] if POLYMORPHIC_CLASS_MAPPING.key?(name)
|
||||
end
|
||||
|
||||
# the chatable_type column value
|
||||
def self.polymorphic_name_for(klass)
|
||||
POLYMORPHIC_CLASS_MAPPING.invert.fetch(klass)
|
||||
end
|
||||
|
||||
def chat_channel
|
||||
channel_class.new(chatable: self)
|
||||
end
|
||||
|
||||
def create_chat_channel!(**args)
|
||||
channel_class.create!(args.merge(chatable: self))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def channel_class
|
||||
case self
|
||||
when Chat::DirectMessage
|
||||
Chat::DirectMessageChannel
|
||||
when Category
|
||||
Chat::CategoryChannel
|
||||
else
|
||||
raise("Unknown chatable #{self}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def chat_channel
|
||||
channel_class.new(chatable: self)
|
||||
end
|
||||
|
||||
def create_chat_channel!(**args)
|
||||
channel_class.create!(args.merge(chatable: self))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def channel_class
|
||||
"#{self.class}Channel".safe_constantize || raise("Unknown chatable #{self}")
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeletedChatUser < User
|
||||
def username
|
||||
I18n.t("chat.deleted_chat_username")
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
"/plugins/chat/images/deleted-chat-user-avatar.png"
|
||||
end
|
||||
|
||||
def bot?
|
||||
false
|
||||
end
|
||||
end
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectMessage < ActiveRecord::Base
|
||||
self.table_name = "direct_message_channels"
|
||||
|
||||
include Chatable
|
||||
|
||||
has_many :direct_message_users, foreign_key: :direct_message_channel_id
|
||||
has_many :users, through: :direct_message_users
|
||||
|
||||
def self.for_user_ids(user_ids)
|
||||
joins(:users)
|
||||
.group("direct_message_channels.id")
|
||||
.having("ARRAY[?] = ARRAY_AGG(users.id ORDER BY users.id)", user_ids.sort)
|
||||
&.first
|
||||
end
|
||||
|
||||
def user_can_access?(user)
|
||||
users.include?(user)
|
||||
end
|
||||
|
||||
def chat_channel_title_for_user(chat_channel, acting_user)
|
||||
users =
|
||||
(direct_message_users.map(&:user) - [acting_user]).map { |user| user || DeletedChatUser.new }
|
||||
|
||||
# direct message to self
|
||||
if users.empty?
|
||||
return I18n.t("chat.channel.dm_title.single_user", username: "@#{acting_user.username}")
|
||||
end
|
||||
|
||||
# all users deleted
|
||||
return chat_channel.id if !users.first
|
||||
|
||||
usernames_formatted = users.sort_by(&:username).map { |u| "@#{u.username}" }
|
||||
if usernames_formatted.size > 5
|
||||
return(
|
||||
I18n.t(
|
||||
"chat.channel.dm_title.multi_user_truncated",
|
||||
comma_separated_usernames: usernames_formatted[0..4].join(I18n.t("word_connector.comma")),
|
||||
count: usernames_formatted.length - 5,
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
I18n.t(
|
||||
"chat.channel.dm_title.multi_user",
|
||||
comma_separated_usernames: usernames_formatted.join(I18n.t("word_connector.comma")),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: direct_message_channels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
|
@ -1,29 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectMessageChannel < ChatChannel
|
||||
alias_attribute :direct_message, :chatable
|
||||
|
||||
def direct_message_channel?
|
||||
true
|
||||
end
|
||||
|
||||
def allowed_user_ids
|
||||
direct_message.user_ids
|
||||
end
|
||||
|
||||
def read_restricted?
|
||||
true
|
||||
end
|
||||
|
||||
def title(user)
|
||||
direct_message.chat_channel_title_for_user(self, user)
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
true
|
||||
end
|
||||
|
||||
def generate_auto_slug
|
||||
self.slug = nil
|
||||
end
|
||||
end
|
|
@ -1,149 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_dependency "reviewable"
|
||||
|
||||
class ReviewableChatMessage < Reviewable
|
||||
def self.action_aliases
|
||||
{
|
||||
agree_and_keep_hidden: :agree_and_delete,
|
||||
agree_and_silence: :agree_and_delete,
|
||||
agree_and_suspend: :agree_and_delete,
|
||||
delete_and_agree: :agree_and_delete,
|
||||
}
|
||||
end
|
||||
|
||||
def self.score_to_silence_user
|
||||
sensitivity_score(SiteSetting.chat_silence_user_sensitivity, scale: 0.6)
|
||||
end
|
||||
|
||||
def chat_message
|
||||
@chat_message ||= (target || ChatMessage.with_deleted.find_by(id: target_id))
|
||||
end
|
||||
|
||||
def chat_message_creator
|
||||
@chat_message_creator ||= chat_message.user
|
||||
end
|
||||
|
||||
def flagged_by_user_ids
|
||||
@flagged_by_user_ids ||= reviewable_scores.map(&:user_id)
|
||||
end
|
||||
|
||||
def post
|
||||
nil
|
||||
end
|
||||
|
||||
def build_actions(actions, guardian, args)
|
||||
return unless pending?
|
||||
return if chat_message.blank?
|
||||
|
||||
agree =
|
||||
actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title")
|
||||
|
||||
if chat_message.deleted_at?
|
||||
build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree)
|
||||
build_action(actions, :agree_and_keep_deleted, icon: "thumbs-up", bundle: agree)
|
||||
build_action(actions, :disagree_and_restore, icon: "thumbs-down")
|
||||
else
|
||||
build_action(actions, :agree_and_delete, icon: "far-eye-slash", bundle: agree)
|
||||
build_action(actions, :agree_and_keep_message, icon: "thumbs-up", bundle: agree)
|
||||
build_action(actions, :disagree, icon: "thumbs-down")
|
||||
end
|
||||
|
||||
if guardian.can_suspend?(chat_message_creator)
|
||||
build_action(
|
||||
actions,
|
||||
:agree_and_suspend,
|
||||
icon: "ban",
|
||||
bundle: agree,
|
||||
client_action: "suspend",
|
||||
)
|
||||
build_action(
|
||||
actions,
|
||||
:agree_and_silence,
|
||||
icon: "microphone-slash",
|
||||
bundle: agree,
|
||||
client_action: "silence",
|
||||
)
|
||||
end
|
||||
|
||||
build_action(actions, :ignore, icon: "external-link-alt")
|
||||
|
||||
build_action(actions, :delete_and_agree, icon: "far-trash-alt") unless chat_message.deleted_at?
|
||||
end
|
||||
|
||||
def perform_agree_and_keep_message(performed_by, args)
|
||||
agree
|
||||
end
|
||||
|
||||
def perform_agree_and_restore(performed_by, args)
|
||||
agree { chat_message.recover! }
|
||||
end
|
||||
|
||||
def perform_agree_and_delete(performed_by, args)
|
||||
agree { chat_message.trash!(performed_by) }
|
||||
end
|
||||
|
||||
def perform_disagree_and_restore(performed_by, args)
|
||||
disagree { chat_message.recover! }
|
||||
end
|
||||
|
||||
def perform_disagree(performed_by, args)
|
||||
disagree
|
||||
end
|
||||
|
||||
def perform_ignore(performed_by, args)
|
||||
ignore
|
||||
end
|
||||
|
||||
def perform_delete_and_ignore(performed_by, args)
|
||||
ignore { chat_message.trash!(performed_by) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def agree
|
||||
yield if block_given?
|
||||
create_result(:success, :approved) do |result|
|
||||
result.update_flag_stats = { status: :agreed, user_ids: flagged_by_user_ids }
|
||||
result.recalculate_score = true
|
||||
end
|
||||
end
|
||||
|
||||
def disagree
|
||||
yield if block_given?
|
||||
|
||||
UserSilencer.unsilence(chat_message_creator)
|
||||
|
||||
create_result(:success, :rejected) do |result|
|
||||
result.update_flag_stats = { status: :disagreed, user_ids: flagged_by_user_ids }
|
||||
result.recalculate_score = true
|
||||
end
|
||||
end
|
||||
|
||||
def ignore
|
||||
yield if block_given?
|
||||
create_result(:success, :ignored) do |result|
|
||||
result.update_flag_stats = { status: :ignored, user_ids: flagged_by_user_ids }
|
||||
end
|
||||
end
|
||||
|
||||
def build_action(
|
||||
actions,
|
||||
id,
|
||||
icon:,
|
||||
button_class: nil,
|
||||
bundle: nil,
|
||||
client_action: nil,
|
||||
confirm: false
|
||||
)
|
||||
actions.add(id, bundle: bundle) do |action|
|
||||
prefix = "reviewables.actions.#{id}"
|
||||
action.icon = icon
|
||||
action.button_class = button_class
|
||||
action.label = "chat.#{prefix}.title"
|
||||
action.description = "chat.#{prefix}.description"
|
||||
action.client_action = client_action
|
||||
action.confirm_message = "#{prefix}.confirm" if confirm
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ChannelMembershipsQuery
|
||||
def self.call(channel:, limit: 50, offset: 0, username: nil, count_only: false)
|
||||
query =
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:user)
|
||||
.includes(:user)
|
||||
.where(user: User.activated.not_suspended.not_staged)
|
||||
.where(chat_channel: channel, following: true)
|
||||
|
||||
return query.count if count_only
|
||||
|
||||
if channel.category_channel? && channel.read_restricted? && channel.allowed_group_ids
|
||||
query =
|
||||
query.where(
|
||||
"user_id IN (SELECT user_id FROM group_users WHERE group_id IN (?))",
|
||||
channel.allowed_group_ids,
|
||||
)
|
||||
end
|
||||
|
||||
if username.present?
|
||||
if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
|
||||
query = query.where("users.username_lower ILIKE ?", "%#{username}%")
|
||||
else
|
||||
query =
|
||||
query.where(
|
||||
"LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?",
|
||||
"%#{username}%",
|
||||
"%#{username}%",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
|
||||
query = query.order("users.username_lower ASC")
|
||||
else
|
||||
query = query.order("users.name ASC, users.username_lower ASC")
|
||||
end
|
||||
|
||||
query.offset(offset).limit(limit)
|
||||
end
|
||||
|
||||
def self.count(channel)
|
||||
call(channel: channel, count_only: true)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatChannelUnreadsQuery
|
||||
def self.call(channel_id:, user_id:)
|
||||
sql = <<~SQL
|
||||
module Chat
|
||||
class ChannelUnreadsQuery
|
||||
def self.call(channel_id:, user_id:)
|
||||
sql = <<~SQL
|
||||
SELECT (
|
||||
SELECT COUNT(*) AS unread_count
|
||||
FROM chat_messages
|
||||
|
@ -27,14 +28,15 @@ class ChatChannelUnreadsQuery
|
|||
) AS mention_count;
|
||||
SQL
|
||||
|
||||
DB
|
||||
.query(
|
||||
sql,
|
||||
channel_id: channel_id,
|
||||
user_id: user_id,
|
||||
notification_type: Notification.types[:chat_mention],
|
||||
)
|
||||
.first
|
||||
.to_h
|
||||
DB
|
||||
.query(
|
||||
sql,
|
||||
channel_id: channel_id,
|
||||
user_id: user_id,
|
||||
notification_type: Notification.types[:chat_mention],
|
||||
)
|
||||
.first
|
||||
.to_h
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChatChannelMembershipsQuery
|
||||
def self.call(channel:, limit: 50, offset: 0, username: nil, count_only: false)
|
||||
query =
|
||||
UserChatChannelMembership
|
||||
.joins(:user)
|
||||
.includes(:user)
|
||||
.where(user: User.activated.not_suspended.not_staged)
|
||||
.where(chat_channel: channel, following: true)
|
||||
|
||||
return query.count if count_only
|
||||
|
||||
if channel.category_channel? && channel.read_restricted? && channel.allowed_group_ids
|
||||
query =
|
||||
query.where(
|
||||
"user_id IN (SELECT user_id FROM group_users WHERE group_id IN (?))",
|
||||
channel.allowed_group_ids,
|
||||
)
|
||||
end
|
||||
|
||||
if username.present?
|
||||
if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
|
||||
query = query.where("users.username_lower ILIKE ?", "%#{username}%")
|
||||
else
|
||||
query =
|
||||
query.where(
|
||||
"LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?",
|
||||
"%#{username}%",
|
||||
"%#{username}%",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
|
||||
query = query.order("users.username_lower ASC")
|
||||
else
|
||||
query = query.order("users.name ASC, users.username_lower ASC")
|
||||
end
|
||||
|
||||
query.offset(offset).limit(limit)
|
||||
end
|
||||
|
||||
def self.count(channel)
|
||||
call(channel: channel, count_only: true)
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AdminChatIndexSerializer < ApplicationSerializer
|
||||
has_many :chat_channels, serializer: ChatChannelSerializer, embed: :objects
|
||||
has_many :incoming_chat_webhooks, serializer: IncomingChatWebhookSerializer, embed: :objects
|
||||
|
||||
def chat_channels
|
||||
object[:chat_channels]
|
||||
end
|
||||
|
||||
def incoming_chat_webhooks
|
||||
object[:incoming_chat_webhooks]
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BaseChatChannelMembershipSerializer < ApplicationSerializer
|
||||
attributes :following,
|
||||
:muted,
|
||||
:desktop_notification_level,
|
||||
:mobile_notification_level,
|
||||
:chat_channel_id,
|
||||
:last_read_message_id,
|
||||
:unread_count,
|
||||
:unread_mentions
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class AdminChatIndexSerializer < ApplicationSerializer
|
||||
has_many :chat_channels, serializer: Chat::ChannelSerializer, embed: :objects
|
||||
has_many :incoming_chat_webhooks, serializer: Chat::IncomingWebhookSerializer, embed: :objects
|
||||
|
||||
def chat_channels
|
||||
object[:chat_channels]
|
||||
end
|
||||
|
||||
def incoming_chat_webhooks
|
||||
object[:incoming_chat_webhooks]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class BaseChannelMembershipSerializer < ApplicationSerializer
|
||||
attributes :following,
|
||||
:muted,
|
||||
:desktop_notification_level,
|
||||
:mobile_notification_level,
|
||||
:chat_channel_id,
|
||||
:last_read_message_id,
|
||||
:unread_count,
|
||||
:unread_mentions
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ChannelIndexSerializer < ::Chat::StructuredChannelSerializer
|
||||
attributes :global_presence_channel_state
|
||||
|
||||
def global_presence_channel_state
|
||||
PresenceChannelStateSerializer.new(PresenceChannel.new("/chat/online").state, root: nil)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ChannelSearchSerializer < ::Chat::StructuredChannelSerializer
|
||||
has_many :users, serializer: BasicUserSerializer, embed: :objects
|
||||
|
||||
def users
|
||||
object[:users]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ChannelSerializer < ApplicationSerializer
|
||||
attributes :id,
|
||||
:auto_join_users,
|
||||
:allow_channel_wide_mentions,
|
||||
:chatable,
|
||||
:chatable_id,
|
||||
:chatable_type,
|
||||
:chatable_url,
|
||||
:description,
|
||||
:title,
|
||||
:slug,
|
||||
:last_message_sent_at,
|
||||
:status,
|
||||
:archive_failed,
|
||||
:archive_completed,
|
||||
:archived_messages,
|
||||
:total_messages,
|
||||
:archive_topic_id,
|
||||
:memberships_count,
|
||||
:current_user_membership,
|
||||
:meta,
|
||||
:threading_enabled
|
||||
|
||||
def threading_enabled
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions && object.threading_enabled
|
||||
end
|
||||
|
||||
def initialize(object, opts)
|
||||
super(object, opts)
|
||||
|
||||
@opts = opts
|
||||
@current_user_membership = opts[:membership]
|
||||
end
|
||||
|
||||
def include_description?
|
||||
object.description.present?
|
||||
end
|
||||
|
||||
def memberships_count
|
||||
object.user_count
|
||||
end
|
||||
|
||||
def chatable_url
|
||||
object.chatable_url
|
||||
end
|
||||
|
||||
def title
|
||||
object.name || object.title(scope.user)
|
||||
end
|
||||
|
||||
def chatable
|
||||
case object.chatable_type
|
||||
when "Category"
|
||||
BasicCategorySerializer.new(object.chatable, root: false).as_json
|
||||
when "DirectMessage"
|
||||
Chat::DirectMessageSerializer.new(object.chatable, scope: scope, root: false).as_json
|
||||
when "Site"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def archive
|
||||
object.chat_channel_archive
|
||||
end
|
||||
|
||||
def include_archive_status?
|
||||
!object.direct_message_channel? && scope.is_staff? && archive.present?
|
||||
end
|
||||
|
||||
def archive_completed
|
||||
archive.complete?
|
||||
end
|
||||
|
||||
def archive_failed
|
||||
archive.failed?
|
||||
end
|
||||
|
||||
def archived_messages
|
||||
archive.archived_messages
|
||||
end
|
||||
|
||||
def total_messages
|
||||
archive.total_messages
|
||||
end
|
||||
|
||||
def archive_topic_id
|
||||
archive.destination_topic_id
|
||||
end
|
||||
|
||||
def include_auto_join_users?
|
||||
scope.can_edit_chat_channel?
|
||||
end
|
||||
|
||||
def include_current_user_membership?
|
||||
@current_user_membership.present?
|
||||
end
|
||||
|
||||
def current_user_membership
|
||||
@current_user_membership.chat_channel = object
|
||||
|
||||
Chat::BaseChannelMembershipSerializer.new(
|
||||
@current_user_membership,
|
||||
scope: scope,
|
||||
root: false,
|
||||
).as_json
|
||||
end
|
||||
|
||||
def meta
|
||||
{
|
||||
message_bus_last_ids: {
|
||||
channel_message_bus_last_id: MessageBus.last_id("/chat/#{object.id}"),
|
||||
new_messages:
|
||||
@opts[:new_messages_message_bus_last_id] ||
|
||||
MessageBus.last_id(Chat::Publisher.new_messages_message_bus_channel(object.id)),
|
||||
new_mentions:
|
||||
@opts[:new_mentions_message_bus_last_id] ||
|
||||
MessageBus.last_id(Chat::Publisher.new_mentions_message_bus_channel(object.id)),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
alias_method :include_archive_topic_id?, :include_archive_status?
|
||||
alias_method :include_total_messages?, :include_archive_status?
|
||||
alias_method :include_archived_messages?, :include_archive_status?
|
||||
alias_method :include_archive_failed?, :include_archive_status?
|
||||
alias_method :include_archive_completed?, :include_archive_status?
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue