Compare commits
9 Commits
composer-c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38fdd842f5 | ||
|
|
aeab38aff1 | ||
|
|
aa8eff5e16 | ||
|
|
293cb7bde2 | ||
|
|
cfee0cfee9 | ||
|
|
c5e5b6d5ab | ||
|
|
184ce647ea | ||
|
|
f57ba758ce | ||
|
|
12a18d4d55 |
@ -2,7 +2,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Graceful",
|
name: "Graceful",
|
||||||
value: "https://github.com/discourse/graceful",
|
value: "https://github.com/discourse/graceful",
|
||||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
|
preview: "https://discourse.theme-creator.io/theme/awesomerobot/graceful",
|
||||||
description: "A light and graceful theme for Discourse.",
|
description: "A light and graceful theme for Discourse.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040",
|
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040",
|
||||||
@ -10,8 +10,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Material Design Theme",
|
name: "Material Design Theme",
|
||||||
value: "https://github.com/discourse/material-design-stock-theme",
|
value: "https://github.com/discourse/material-design-stock-theme",
|
||||||
preview:
|
preview: "https://discourse.theme-creator.io/theme/tshenry/material-design",
|
||||||
"https://theme-creator.discourse.org/theme/tshenry/material-design",
|
|
||||||
description:
|
description:
|
||||||
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
||||||
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142",
|
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142",
|
||||||
@ -19,7 +18,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Minima",
|
name: "Minima",
|
||||||
value: "https://github.com/discourse/minima",
|
value: "https://github.com/discourse/minima",
|
||||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
|
preview: "https://discourse.theme-creator.io/theme/awesomerobot/minima",
|
||||||
description: "A minimal theme with reduced UI elements and focus on text.",
|
description: "A minimal theme with reduced UI elements and focus on text.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178",
|
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178",
|
||||||
@ -27,7 +26,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Sam's Simple Theme",
|
name: "Sam's Simple Theme",
|
||||||
value: "https://github.com/discourse/discourse-simple-theme",
|
value: "https://github.com/discourse/discourse-simple-theme",
|
||||||
preview: "https://theme-creator.discourse.org/theme/sam/simple",
|
preview: "https://discourse.theme-creator.io/theme/sam/simple",
|
||||||
description:
|
description:
|
||||||
"Simplified front page design with classic colors and typography.",
|
"Simplified front page design with classic colors and typography.",
|
||||||
meta_url:
|
meta_url:
|
||||||
@ -36,6 +35,8 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Brand Header",
|
name: "Brand Header",
|
||||||
value: "https://github.com/discourse/discourse-brand-header",
|
value: "https://github.com/discourse/discourse-brand-header",
|
||||||
|
preview:
|
||||||
|
"https://discourse.theme-creator.io/theme/vinothkannans/brand-header",
|
||||||
description:
|
description:
|
||||||
"Add an extra top header with your logo, navigation links and social icons.",
|
"Add an extra top header with your logo, navigation links and social icons.",
|
||||||
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
||||||
@ -45,7 +46,7 @@ export const POPULAR_THEMES = [
|
|||||||
name: "Custom Header Links",
|
name: "Custom Header Links",
|
||||||
value: "https://github.com/discourse/discourse-custom-header-links",
|
value: "https://github.com/discourse/discourse-custom-header-links",
|
||||||
preview:
|
preview:
|
||||||
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
|
"https://discourse.theme-creator.io/theme/awesomerobot/custom-header-links",
|
||||||
description: "Easily add custom text-based links to the header.",
|
description: "Easily add custom text-based links to the header.",
|
||||||
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
||||||
component: true,
|
component: true,
|
||||||
@ -61,7 +62,7 @@ export const POPULAR_THEMES = [
|
|||||||
name: "Category Banners",
|
name: "Category Banners",
|
||||||
value: "https://github.com/discourse/discourse-category-banners",
|
value: "https://github.com/discourse/discourse-category-banners",
|
||||||
preview:
|
preview:
|
||||||
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
|
"https://discourse.theme-creator.io/theme/awesomerobot/discourse-category-banners",
|
||||||
description:
|
description:
|
||||||
"Show banners on category pages using your existing category details.",
|
"Show banners on category pages using your existing category details.",
|
||||||
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
||||||
@ -70,7 +71,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Kanban Board",
|
name: "Kanban Board",
|
||||||
value: "https://github.com/discourse/discourse-kanban-theme",
|
value: "https://github.com/discourse/discourse-kanban-theme",
|
||||||
preview: "https://theme-creator.discourse.org/theme/david/kanban",
|
preview: "https://discourse.theme-creator.io/theme/david/kanban",
|
||||||
description: "Display and organize topics using a Kanban board interface.",
|
description: "Display and organize topics using a Kanban board interface.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/kanban-board-theme-component/118164",
|
"https://meta.discourse.org/t/kanban-board-theme-component/118164",
|
||||||
@ -84,10 +85,19 @@ export const POPULAR_THEMES = [
|
|||||||
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
||||||
component: true,
|
component: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Sidebar Theme Toggle",
|
||||||
|
value: "https://github.com/discourse/discourse-sidebar-theme-toggle",
|
||||||
|
description:
|
||||||
|
"Displays a theme selector in the sidebar menu’s footer provided there is more than one user-selectable theme.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/sidebar-theme-toggle/242802",
|
||||||
|
component: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Header Submenus",
|
name: "Header Submenus",
|
||||||
value: "https://github.com/discourse/discourse-header-submenus",
|
value: "https://github.com/discourse/discourse-header-submenus",
|
||||||
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
|
preview:
|
||||||
|
"https://discourse.theme-creator.io/theme/awesomerobot/header-submenus",
|
||||||
description: "Lets you build a header menu with submenus (dropdowns).",
|
description: "Lets you build a header menu with submenus (dropdowns).",
|
||||||
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
||||||
component: true,
|
component: true,
|
||||||
@ -104,7 +114,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Easy Responsive Footer",
|
name: "Easy Responsive Footer",
|
||||||
value: "https://github.com/discourse/Discourse-easy-footer",
|
value: "https://github.com/discourse/Discourse-easy-footer",
|
||||||
preview: "https://theme-creator.discourse.org/theme/Johani/easy-footer",
|
preview: "https://discourse.theme-creator.io/theme/Johani/easy-footer",
|
||||||
description: "Add a fully responsive footer without writing any HTML.",
|
description: "Add a fully responsive footer without writing any HTML.",
|
||||||
meta_url: "https://meta.discourse.org/t/easy-responsive-footer/95818",
|
meta_url: "https://meta.discourse.org/t/easy-responsive-footer/95818",
|
||||||
component: true,
|
component: true,
|
||||||
|
|||||||
@ -46,7 +46,8 @@ class Bookmark < ActiveRecord::Base
|
|||||||
validates :name, length: { maximum: 100 }
|
validates :name, length: { maximum: 100 }
|
||||||
|
|
||||||
def registered_bookmarkable
|
def registered_bookmarkable
|
||||||
Bookmark.registered_bookmarkable_from_type(self.bookmarkable_type)
|
type = Bookmark.polymorphic_class_for(self.bookmarkable_type).name
|
||||||
|
Bookmark.registered_bookmarkable_from_type(type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def polymorphic_columns_present
|
def polymorphic_columns_present
|
||||||
|
|||||||
@ -129,7 +129,7 @@ class Reviewable < ActiveRecord::Base
|
|||||||
update_args = {
|
update_args = {
|
||||||
status: statuses[:pending],
|
status: statuses[:pending],
|
||||||
id: target.id,
|
id: target.id,
|
||||||
type: target.class.name,
|
type: target.class.sti_name,
|
||||||
potential_spam: potential_spam == true ? true : nil,
|
potential_spam: potential_spam == true ? true : nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -213,7 +213,10 @@ task "docker:test" do
|
|||||||
@good &&= run_or_fail("bundle exec rspec #{params.join(" ")}".strip)
|
@good &&= run_or_fail("bundle exec rspec #{params.join(" ")}".strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
@good &&= run_or_fail("bundle exec rspec spec/system".strip) if ENV["RUN_SYSTEM_TESTS"]
|
if ENV["RUN_SYSTEM_TESTS"]
|
||||||
|
@good &&= run_or_fail("bin/ember-cli --build")
|
||||||
|
@good &&= run_or_fail("bundle exec rspec spec/system")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
unless ENV["SKIP_PLUGINS"]
|
unless ENV["SKIP_PLUGINS"]
|
||||||
|
|||||||
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChannelThreadsController < Chat::Api
|
class Chat::Api::ChannelThreadsController < Chat::ApiController
|
||||||
def show
|
def show
|
||||||
with_service(Chat::Service::LookupThread) do
|
with_service(::Chat::LookupThread) do
|
||||||
on_success { render_serialized(result.thread, ChatThreadSerializer, root: "thread") }
|
on_success { render_serialized(result.thread, ::Chat::ThreadSerializer, root: "thread") }
|
||||||
on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound }
|
on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound }
|
||||||
on_failed_policy(:threading_enabled_for_channel) { raise Discourse::NotFound }
|
on_failed_policy(:threading_enabled_for_channel) { raise Discourse::NotFound }
|
||||||
on_model_not_found(:thread) { raise Discourse::NotFound }
|
on_model_not_found(:thread) { raise Discourse::NotFound }
|
||||||
@ -1,13 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsController
|
class Chat::Api::ChannelsArchivesController < Chat::Api::ChannelsController
|
||||||
def create
|
def create
|
||||||
existing_archive = channel_from_params.chat_channel_archive
|
existing_archive = channel_from_params.chat_channel_archive
|
||||||
|
|
||||||
if existing_archive.present?
|
if existing_archive.present?
|
||||||
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
|
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
|
||||||
raise Discourse::InvalidAccess if !existing_archive.failed?
|
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
|
return render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -20,12 +20,12 @@ class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsControl
|
|||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Chat::ChatChannelArchiveService.create_archive_process(
|
Chat::ChannelArchiveService.create_archive_process(
|
||||||
chat_channel: channel_from_params,
|
chat_channel: channel_from_params,
|
||||||
acting_user: current_user,
|
acting_user: current_user,
|
||||||
topic_params: topic_params,
|
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
|
return render json: failed_json.merge(errors: err.errors), status: 400
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -3,19 +3,19 @@
|
|||||||
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
||||||
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
|
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
|
def index
|
||||||
permitted = params.permit(:filter, :limit, :offset, :status)
|
permitted = params.permit(:filter, :limit, :offset, :status)
|
||||||
|
|
||||||
options = { filter: permitted[:filter], limit: (permitted[:limit] || 25).to_i }
|
options = { filter: permitted[:filter], limit: (permitted[:limit] || 25).to_i }
|
||||||
options[:offset] = permitted[:offset].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)
|
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
||||||
channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options)
|
channels = Chat::ChannelFetcher.secured_public_channels(guardian, memberships, options)
|
||||||
serialized_channels =
|
serialized_channels =
|
||||||
channels.map do |channel|
|
channels.map do |channel|
|
||||||
ChatChannelSerializer.new(
|
Chat::ChannelSerializer.new(
|
||||||
channel,
|
channel,
|
||||||
scope: Guardian.new(current_user),
|
scope: Guardian.new(current_user),
|
||||||
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
|
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
|
||||||
@ -29,7 +29,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
with_service Chat::Service::TrashChannel do
|
with_service Chat::TrashChannel do
|
||||||
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
|
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -43,7 +43,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
raise Discourse::InvalidParameters.new(:name)
|
raise Discourse::InvalidParameters.new(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
if ChatChannel.exists?(
|
if Chat::Channel.exists?(
|
||||||
chatable_type: "Category",
|
chatable_type: "Category",
|
||||||
chatable_id: channel_params[:chatable_id],
|
chatable_id: channel_params[:chatable_id],
|
||||||
name: channel_params[:name],
|
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)
|
channel.user_chat_channel_memberships.create!(user: current_user, following: true)
|
||||||
|
|
||||||
if channel.auto_join_users
|
if channel.auto_join_users
|
||||||
Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||||
end
|
end
|
||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel,
|
channel,
|
||||||
ChatChannelSerializer,
|
Chat::ChannelSerializer,
|
||||||
membership: channel.membership_for(current_user),
|
membership: channel.membership_for(current_user),
|
||||||
root: "channel",
|
root: "channel",
|
||||||
)
|
)
|
||||||
@ -83,7 +83,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
def show
|
def show
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params,
|
channel_from_params,
|
||||||
ChatChannelSerializer,
|
Chat::ChannelSerializer,
|
||||||
membership: channel_from_params.membership_for(current_user),
|
membership: channel_from_params.membership_for(current_user),
|
||||||
root: "channel",
|
root: "channel",
|
||||||
)
|
)
|
||||||
@ -96,11 +96,11 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
auto_join_limiter(channel_from_params).performed!
|
auto_join_limiter(channel_from_params).performed!
|
||||||
end
|
end
|
||||||
|
|
||||||
with_service(Chat::Service::UpdateChannel, **params_to_edit) do
|
with_service(Chat::UpdateChannel, **params_to_edit) do
|
||||||
on_success do
|
on_success do
|
||||||
render_serialized(
|
render_serialized(
|
||||||
result.channel,
|
result.channel,
|
||||||
ChatChannelSerializer,
|
Chat::ChannelSerializer,
|
||||||
root: "channel",
|
root: "channel",
|
||||||
membership: result.channel.membership_for(current_user),
|
membership: result.channel.membership_for(current_user),
|
||||||
)
|
)
|
||||||
@ -116,7 +116,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
def channel_from_params
|
def channel_from_params
|
||||||
@channel ||=
|
@channel ||=
|
||||||
begin
|
begin
|
||||||
channel = ChatChannel.find(params.require(:channel_id))
|
channel = Chat::Channel.find(params.require(:channel_id))
|
||||||
guardian.ensure_can_preview_chat_channel!(channel)
|
guardian.ensure_can_preview_chat_channel!(channel)
|
||||||
channel
|
channel
|
||||||
end
|
end
|
||||||
@ -126,7 +126,7 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||||||
@membership ||=
|
@membership ||=
|
||||||
begin
|
begin
|
||||||
membership =
|
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?
|
raise Discourse::NotFound if membership.blank?
|
||||||
membership
|
membership
|
||||||
end
|
end
|
||||||
@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatChannelsController
|
class Chat::Api::ChannelsCurrentUserMembershipController < Chat::Api::ChannelsController
|
||||||
def create
|
def create
|
||||||
guardian.ensure_can_join_chat_channel!(channel_from_params)
|
guardian.ensure_can_join_chat_channel!(channel_from_params)
|
||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params.add(current_user),
|
channel_from_params.add(current_user),
|
||||||
UserChatChannelMembershipSerializer,
|
Chat::UserChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -14,7 +14,7 @@ class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatCh
|
|||||||
def destroy
|
def destroy
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params.remove(current_user),
|
channel_from_params.remove(current_user),
|
||||||
UserChatChannelMembershipSerializer,
|
Chat::UserChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
|
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
|
def update
|
||||||
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
|
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
|
||||||
membership_from_params.update!(settings_params.to_h)
|
membership_from_params.update!(settings_params.to_h)
|
||||||
render_serialized(
|
render_serialized(
|
||||||
membership_from_params,
|
membership_from_params,
|
||||||
UserChatChannelMembershipSerializer,
|
Chat::UserChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsController
|
class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
|
||||||
def index
|
def index
|
||||||
params.permit(:username, :offset, :limit)
|
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)
|
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
||||||
|
|
||||||
memberships =
|
memberships =
|
||||||
ChatChannelMembershipsQuery.call(
|
Chat::ChannelMembershipsQuery.call(
|
||||||
channel: channel_from_params,
|
channel: channel_from_params,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@ -17,7 +17,7 @@ class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsCont
|
|||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
memberships,
|
memberships,
|
||||||
UserChatChannelMembershipSerializer,
|
Chat::UserChannelMembershipSerializer,
|
||||||
root: "memberships",
|
root: "memberships",
|
||||||
meta: {
|
meta: {
|
||||||
total_rows: channel_from_params.user_count,
|
total_rows: channel_from_params.user_count,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsController
|
class Chat::Api::ChannelsMessagesMovesController < Chat::Api::ChannelsController
|
||||||
def create
|
def create
|
||||||
move_params = params.require(:move)
|
move_params = params.require(:move)
|
||||||
move_params.require(:message_ids)
|
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)
|
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params)
|
||||||
destination_channel =
|
destination_channel =
|
||||||
Chat::ChatChannelFetcher.find_with_access_check(
|
Chat::ChannelFetcher.find_with_access_check(move_params[:destination_channel_id], guardian)
|
||||||
move_params[:destination_channel_id],
|
|
||||||
guardian,
|
|
||||||
)
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
message_ids = move_params[:message_ids].map(&:to_i)
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatChatablesController < Chat::Api
|
class Chat::Api::ChatablesController < Chat::ApiController
|
||||||
def index
|
def index
|
||||||
params.require(:filter)
|
params.require(:filter)
|
||||||
filter = params[:filter].downcase
|
filter = params[:filter].downcase
|
||||||
|
|
||||||
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
|
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
||||||
|
|
||||||
public_channels =
|
public_channels =
|
||||||
Chat::ChatChannelFetcher.secured_public_channels(
|
Chat::ChannelFetcher.secured_public_channels(
|
||||||
guardian,
|
guardian,
|
||||||
memberships,
|
memberships,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
@ -41,7 +42,7 @@ class Chat::Api::ChatChatablesController < Chat::Api
|
|||||||
direct_message_channels =
|
direct_message_channels =
|
||||||
if users.count > 0
|
if users.count > 0
|
||||||
# FIXME: investigate the cost of this query
|
# FIXME: investigate the cost of this query
|
||||||
ChatChannel
|
Chat::Channel
|
||||||
.includes(chatable: :users)
|
.includes(chatable: :users)
|
||||||
.joins(direct_message: :direct_message_users)
|
.joins(direct_message: :direct_message_users)
|
||||||
.group(1)
|
.group(1)
|
||||||
@ -75,7 +76,7 @@ class Chat::Api::ChatChatablesController < Chat::Api
|
|||||||
users: users_without_channel,
|
users: users_without_channel,
|
||||||
memberships: memberships,
|
memberships: memberships,
|
||||||
},
|
},
|
||||||
ChatChannelSearchSerializer,
|
Chat::ChannelSearchSerializer,
|
||||||
root: false,
|
root: false,
|
||||||
)
|
)
|
||||||
end
|
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
|
||||||
32
plugins/chat/app/controllers/chat/api_controller.rb
Normal file
32
plugins/chat/app/controllers/chat/api_controller.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class ApiController < ::Chat::BaseController
|
||||||
|
before_action :ensure_logged_in
|
||||||
|
before_action :ensure_can_chat
|
||||||
|
|
||||||
|
include Chat::WithServiceHelper
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_chat
|
||||||
|
raise Discourse::NotFound unless SiteSetting.chat_enabled
|
||||||
|
guardian.ensure_can_chat!
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_actions_for_service
|
||||||
|
proc do
|
||||||
|
on_success { render(json: success_json) }
|
||||||
|
on_failure { render(json: failed_json, status: 422) }
|
||||||
|
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
|
||||||
|
on_failed_contract do
|
||||||
|
render(
|
||||||
|
json:
|
||||||
|
failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages),
|
||||||
|
status: 400,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
22
plugins/chat/app/controllers/chat/base_controller.rb
Normal file
22
plugins/chat/app/controllers/chat/base_controller.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class BaseController < ::ApplicationController
|
||||||
|
before_action :ensure_logged_in
|
||||||
|
before_action :ensure_can_chat
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_chat
|
||||||
|
raise Discourse::NotFound unless SiteSetting.chat_enabled
|
||||||
|
guardian.ensure_can_chat!
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_channel_and_chatable_with_access_check(chat_channel_id: nil)
|
||||||
|
params.require(:chat_channel_id) if chat_channel_id.blank?
|
||||||
|
id_or_name = chat_channel_id || params[:chat_channel_id]
|
||||||
|
@chat_channel = Chat::ChannelFetcher.find_with_access_check(id_or_name, guardian)
|
||||||
|
@chatable = @chat_channel.chatable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
481
plugins/chat/app/controllers/chat/chat_controller.rb
Normal file
481
plugins/chat/app/controllers/chat/chat_controller.rb
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class ChatController < ::Chat::BaseController
|
||||||
|
PAST_MESSAGE_LIMIT = 40
|
||||||
|
FUTURE_MESSAGE_LIMIT = 40
|
||||||
|
PAST = "past"
|
||||||
|
FUTURE = "future"
|
||||||
|
CHAT_DIRECTIONS = [PAST, FUTURE]
|
||||||
|
|
||||||
|
# Other endpoints use set_channel_and_chatable_with_access_check, but
|
||||||
|
# these endpoints require a standalone find because they need to be
|
||||||
|
# able to get deleted channels and recover them.
|
||||||
|
before_action :find_chatable, only: %i[enable_chat disable_chat]
|
||||||
|
before_action :find_chat_message,
|
||||||
|
only: %i[delete restore lookup_message edit_message rebake message_link]
|
||||||
|
before_action :set_channel_and_chatable_with_access_check,
|
||||||
|
except: %i[
|
||||||
|
respond
|
||||||
|
enable_chat
|
||||||
|
disable_chat
|
||||||
|
message_link
|
||||||
|
lookup_message
|
||||||
|
set_user_chat_status
|
||||||
|
dismiss_retention_reminder
|
||||||
|
flag
|
||||||
|
]
|
||||||
|
|
||||||
|
def respond
|
||||||
|
render
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_chat
|
||||||
|
chat_channel = Chat::Channel.with_deleted.find_by(chatable_id: @chatable)
|
||||||
|
|
||||||
|
guardian.ensure_can_join_chat_channel!(chat_channel) if chat_channel
|
||||||
|
|
||||||
|
if chat_channel && chat_channel.trashed?
|
||||||
|
chat_channel.recover!
|
||||||
|
elsif chat_channel
|
||||||
|
return render_json_error I18n.t("chat.already_enabled")
|
||||||
|
else
|
||||||
|
chat_channel = @chatable.chat_channel
|
||||||
|
guardian.ensure_can_join_chat_channel!(chat_channel)
|
||||||
|
end
|
||||||
|
|
||||||
|
success = chat_channel.save
|
||||||
|
if success && chat_channel.chatable_has_custom_fields?
|
||||||
|
@chatable.custom_fields[Chat::HAS_CHAT_ENABLED] = true
|
||||||
|
@chatable.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
if success
|
||||||
|
membership = Chat::ChannelMembershipManager.new(channel).follow(user)
|
||||||
|
render_serialized(chat_channel, Chat::ChannelSerializer, membership: membership)
|
||||||
|
else
|
||||||
|
render_json_error(chat_channel)
|
||||||
|
end
|
||||||
|
|
||||||
|
Chat::ChannelMembershipManager.new(channel).follow(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_chat
|
||||||
|
chat_channel = Chat::Channel.with_deleted.find_by(chatable_id: @chatable)
|
||||||
|
guardian.ensure_can_join_chat_channel!(chat_channel)
|
||||||
|
return render json: success_json if chat_channel.trashed?
|
||||||
|
chat_channel.trash!(current_user)
|
||||||
|
|
||||||
|
success = chat_channel.save
|
||||||
|
if success
|
||||||
|
if chat_channel.chatable_has_custom_fields?
|
||||||
|
@chatable.custom_fields.delete(Chat::HAS_CHAT_ENABLED)
|
||||||
|
@chatable.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
render_json_error(chat_channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_message
|
||||||
|
raise Discourse::InvalidAccess if current_user.silenced?
|
||||||
|
|
||||||
|
Chat::MessageRateLimiter.run!(current_user)
|
||||||
|
|
||||||
|
@user_chat_channel_membership =
|
||||||
|
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
|
||||||
|
current_user,
|
||||||
|
following: true,
|
||||||
|
)
|
||||||
|
raise Discourse::InvalidAccess unless @user_chat_channel_membership
|
||||||
|
|
||||||
|
reply_to_msg_id = params[:in_reply_to_id]
|
||||||
|
if reply_to_msg_id
|
||||||
|
rm = Chat::Message.find(reply_to_msg_id)
|
||||||
|
raise Discourse::NotFound if rm.chat_channel_id != @chat_channel.id
|
||||||
|
end
|
||||||
|
|
||||||
|
content = params[:message]
|
||||||
|
|
||||||
|
chat_message_creator =
|
||||||
|
Chat::MessageCreator.create(
|
||||||
|
chat_channel: @chat_channel,
|
||||||
|
user: current_user,
|
||||||
|
in_reply_to_id: reply_to_msg_id,
|
||||||
|
content: content,
|
||||||
|
staged_id: params[:staged_id],
|
||||||
|
upload_ids: params[:upload_ids],
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
|
||||||
|
|
||||||
|
@user_chat_channel_membership.update!(
|
||||||
|
last_read_message_id: chat_message_creator.chat_message.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if @chat_channel.direct_message_channel?
|
||||||
|
# If any of the channel users is ignoring, muting, or preventing DMs from
|
||||||
|
# the current user then we shold not auto-follow the channel once again or
|
||||||
|
# publish the new channel.
|
||||||
|
user_ids_allowing_communication =
|
||||||
|
UserCommScreener.new(
|
||||||
|
acting_user: current_user,
|
||||||
|
target_user_ids: @chat_channel.user_chat_channel_memberships.pluck(:user_id),
|
||||||
|
).allowing_actor_communication
|
||||||
|
|
||||||
|
if user_ids_allowing_communication.any?
|
||||||
|
Chat::Publisher.publish_new_channel(
|
||||||
|
@chat_channel,
|
||||||
|
@chat_channel.chatable.users.where(id: user_ids_allowing_communication),
|
||||||
|
)
|
||||||
|
|
||||||
|
@chat_channel
|
||||||
|
.user_chat_channel_memberships
|
||||||
|
.where(user_id: user_ids_allowing_communication)
|
||||||
|
.update_all(following: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Chat::Publisher.publish_user_tracking_state(
|
||||||
|
current_user,
|
||||||
|
@chat_channel.id,
|
||||||
|
chat_message_creator.chat_message.id,
|
||||||
|
)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_message
|
||||||
|
chat_message_updater =
|
||||||
|
Chat::MessageUpdater.update(
|
||||||
|
guardian: guardian,
|
||||||
|
chat_message: @message,
|
||||||
|
new_content: params[:new_message],
|
||||||
|
upload_ids: params[:upload_ids] || [],
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_json_error(chat_message_updater.error) if chat_message_updater.failed?
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_user_last_read
|
||||||
|
membership =
|
||||||
|
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
|
||||||
|
current_user,
|
||||||
|
following: true,
|
||||||
|
)
|
||||||
|
raise Discourse::NotFound if membership.nil?
|
||||||
|
|
||||||
|
if membership.last_read_message_id &&
|
||||||
|
params[:message_id].to_i < membership.last_read_message_id
|
||||||
|
raise Discourse::InvalidParameters.new(:message_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless Chat::Message.with_deleted.exists?(
|
||||||
|
chat_channel_id: @chat_channel.id,
|
||||||
|
id: params[:message_id],
|
||||||
|
)
|
||||||
|
raise Discourse::NotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
membership.update!(last_read_message_id: params[:message_id])
|
||||||
|
|
||||||
|
Notification
|
||||||
|
.where(notification_type: Notification.types[:chat_mention])
|
||||||
|
.where(user: current_user)
|
||||||
|
.where(read: false)
|
||||||
|
.joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id")
|
||||||
|
.joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id")
|
||||||
|
.where("chat_messages.id <= ?", params[:message_id].to_i)
|
||||||
|
.where("chat_messages.chat_channel_id = ?", @chat_channel.id)
|
||||||
|
.update_all(read: true)
|
||||||
|
|
||||||
|
Chat::Publisher.publish_user_tracking_state(
|
||||||
|
current_user,
|
||||||
|
@chat_channel.id,
|
||||||
|
params[:message_id],
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages
|
||||||
|
page_size = params[:page_size]&.to_i || 1000
|
||||||
|
direction = params[:direction].to_s
|
||||||
|
message_id = params[:message_id]
|
||||||
|
if page_size > 50 ||
|
||||||
|
(
|
||||||
|
message_id.blank? ^ direction.blank? &&
|
||||||
|
(direction.present? && !CHAT_DIRECTIONS.include?(direction))
|
||||||
|
)
|
||||||
|
raise Discourse::InvalidParameters
|
||||||
|
end
|
||||||
|
|
||||||
|
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
|
||||||
|
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
|
||||||
|
|
||||||
|
if message_id.present?
|
||||||
|
condition = direction == PAST ? "<" : ">"
|
||||||
|
messages = messages.where("id #{condition} ?", message_id.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE: This order is reversed when we return the Chat::View below if the direction
|
||||||
|
# is not FUTURE.
|
||||||
|
order = direction == FUTURE ? "ASC" : "DESC"
|
||||||
|
messages = messages.order("created_at #{order}, id #{order}").limit(page_size).to_a
|
||||||
|
|
||||||
|
can_load_more_past = nil
|
||||||
|
can_load_more_future = nil
|
||||||
|
|
||||||
|
if direction == FUTURE
|
||||||
|
can_load_more_future = messages.size == page_size
|
||||||
|
elsif direction == PAST
|
||||||
|
can_load_more_past = messages.size == page_size
|
||||||
|
else
|
||||||
|
# When direction is blank, we'll return the latest messages.
|
||||||
|
can_load_more_future = false
|
||||||
|
can_load_more_past = messages.size == page_size
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_view =
|
||||||
|
Chat::View.new(
|
||||||
|
chat_channel: @chat_channel,
|
||||||
|
chat_messages: direction == FUTURE ? messages : messages.reverse,
|
||||||
|
user: current_user,
|
||||||
|
can_load_more_past: can_load_more_past,
|
||||||
|
can_load_more_future: can_load_more_future,
|
||||||
|
)
|
||||||
|
render_serialized(chat_view, Chat::ViewSerializer, root: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def react
|
||||||
|
params.require(%i[message_id emoji react_action])
|
||||||
|
guardian.ensure_can_react!
|
||||||
|
|
||||||
|
Chat::MessageReactor.new(current_user, @chat_channel).react!(
|
||||||
|
message_id: params[:message_id],
|
||||||
|
react_action: params[:react_action].to_sym,
|
||||||
|
emoji: params[:emoji],
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
guardian.ensure_can_delete_chat!(@message, @chatable)
|
||||||
|
|
||||||
|
Chat::MessageDestroyer.new.trash_message(@message, current_user)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def restore
|
||||||
|
chat_channel = @message.chat_channel
|
||||||
|
guardian.ensure_can_restore_chat!(@message, chat_channel.chatable)
|
||||||
|
updated = @message.recover!
|
||||||
|
if updated
|
||||||
|
Chat::Publisher.publish_restore!(chat_channel, @message)
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
render_json_error(@message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebake
|
||||||
|
guardian.ensure_can_rebake_chat_message!(@message)
|
||||||
|
@message.rebake!(invalidate_oneboxes: true)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_link
|
||||||
|
raise Discourse::NotFound if @message.blank? || @message.deleted_at.present?
|
||||||
|
raise Discourse::NotFound if @message.chat_channel.blank?
|
||||||
|
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
|
||||||
|
render json:
|
||||||
|
success_json.merge(
|
||||||
|
chat_channel_id: @chat_channel.id,
|
||||||
|
chat_channel_title: @chat_channel.title(current_user),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup_message
|
||||||
|
set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
|
||||||
|
|
||||||
|
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
|
||||||
|
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
|
||||||
|
|
||||||
|
past_messages =
|
||||||
|
messages
|
||||||
|
.where("created_at < ?", @message.created_at)
|
||||||
|
.order(created_at: :desc)
|
||||||
|
.limit(PAST_MESSAGE_LIMIT)
|
||||||
|
|
||||||
|
future_messages =
|
||||||
|
messages
|
||||||
|
.where("created_at > ?", @message.created_at)
|
||||||
|
.order(created_at: :asc)
|
||||||
|
.limit(FUTURE_MESSAGE_LIMIT)
|
||||||
|
|
||||||
|
can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT
|
||||||
|
can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT
|
||||||
|
messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat)
|
||||||
|
chat_view =
|
||||||
|
Chat::View.new(
|
||||||
|
chat_channel: @chat_channel,
|
||||||
|
chat_messages: messages,
|
||||||
|
user: current_user,
|
||||||
|
can_load_more_past: can_load_more_past,
|
||||||
|
can_load_more_future: can_load_more_future,
|
||||||
|
)
|
||||||
|
render_serialized(chat_view, Chat::ViewSerializer, root: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_user_chat_status
|
||||||
|
params.require(:chat_enabled)
|
||||||
|
|
||||||
|
current_user.user_option.update(chat_enabled: params[:chat_enabled])
|
||||||
|
render json: { chat_enabled: current_user.user_option.chat_enabled }
|
||||||
|
end
|
||||||
|
|
||||||
|
def invite_users
|
||||||
|
params.require(:user_ids)
|
||||||
|
|
||||||
|
users =
|
||||||
|
User
|
||||||
|
.includes(:groups)
|
||||||
|
.joins(:user_option)
|
||||||
|
.where(user_options: { chat_enabled: true })
|
||||||
|
.not_suspended
|
||||||
|
.where(id: params[:user_ids])
|
||||||
|
users.each do |user|
|
||||||
|
guardian = Guardian.new(user)
|
||||||
|
if guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel)
|
||||||
|
data = {
|
||||||
|
message: "chat.invitation_notification",
|
||||||
|
chat_channel_id: @chat_channel.id,
|
||||||
|
chat_channel_title: @chat_channel.title(user),
|
||||||
|
chat_channel_slug: @chat_channel.slug,
|
||||||
|
invited_by_username: current_user.username,
|
||||||
|
}
|
||||||
|
data[:chat_message_id] = params[:chat_message_id] if params[:chat_message_id]
|
||||||
|
user.notifications.create(
|
||||||
|
notification_type: Notification.types[:chat_invitation],
|
||||||
|
high_priority: true,
|
||||||
|
data: data.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_retention_reminder
|
||||||
|
params.require(:chatable_type)
|
||||||
|
guardian.ensure_can_chat!
|
||||||
|
unless Chat::Channel.chatable_types.include?(params[:chatable_type])
|
||||||
|
raise Discourse::InvalidParameters
|
||||||
|
end
|
||||||
|
|
||||||
|
field =
|
||||||
|
(
|
||||||
|
if Chat::Channel.public_channel_chatable_types.include?(params[:chatable_type])
|
||||||
|
:dismissed_channel_retention_reminder
|
||||||
|
else
|
||||||
|
:dismissed_dm_retention_reminder
|
||||||
|
end
|
||||||
|
)
|
||||||
|
current_user.user_option.update(field => true)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_messages
|
||||||
|
params.require(:message_ids)
|
||||||
|
|
||||||
|
message_ids = params[:message_ids].map(&:to_i)
|
||||||
|
markdown =
|
||||||
|
Chat::TranscriptService.new(
|
||||||
|
@chat_channel,
|
||||||
|
current_user,
|
||||||
|
messages_or_ids: message_ids,
|
||||||
|
).generate_markdown
|
||||||
|
render json: success_json.merge(markdown: markdown)
|
||||||
|
end
|
||||||
|
|
||||||
|
def flag
|
||||||
|
RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed!
|
||||||
|
|
||||||
|
permitted_params =
|
||||||
|
params.permit(
|
||||||
|
%i[chat_message_id flag_type_id message is_warning take_action queue_for_review],
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_message =
|
||||||
|
Chat::Message.includes(:chat_channel, :revisions).find(permitted_params[:chat_message_id])
|
||||||
|
|
||||||
|
flag_type_id = permitted_params[:flag_type_id].to_i
|
||||||
|
|
||||||
|
if !ReviewableScore.types.values.include?(flag_type_id)
|
||||||
|
raise Discourse::InvalidParameters.new(:flag_type_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
set_channel_and_chatable_with_access_check(chat_channel_id: chat_message.chat_channel_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
Chat::ReviewQueue.new.flag_message(chat_message, guardian, flag_type_id, permitted_params)
|
||||||
|
|
||||||
|
if result[:success]
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
render_json_error(result[:errors])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_draft
|
||||||
|
if params[:data].present?
|
||||||
|
Chat::Draft.find_or_initialize_by(
|
||||||
|
user: current_user,
|
||||||
|
chat_channel_id: @chat_channel.id,
|
||||||
|
).update!(data: params[:data])
|
||||||
|
else
|
||||||
|
Chat::Draft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def preloaded_chat_message_query
|
||||||
|
query =
|
||||||
|
Chat::Message
|
||||||
|
.includes(in_reply_to: [:user, chat_webhook_event: [:incoming_chat_webhook]])
|
||||||
|
.includes(:revisions)
|
||||||
|
.includes(user: :primary_group)
|
||||||
|
.includes(chat_webhook_event: :incoming_chat_webhook)
|
||||||
|
.includes(reactions: :user)
|
||||||
|
.includes(:bookmarks)
|
||||||
|
.includes(:uploads)
|
||||||
|
.includes(chat_channel: :chatable)
|
||||||
|
|
||||||
|
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
|
||||||
|
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_chatable
|
||||||
|
@chatable = Category.find_by(id: params[:chatable_id])
|
||||||
|
guardian.ensure_can_moderate_chat!(@chatable)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_chat_message
|
||||||
|
@message = preloaded_chat_message_query.with_deleted
|
||||||
|
@message = @message.where(chat_channel_id: params[:chat_channel_id]) if params[
|
||||||
|
:chat_channel_id
|
||||||
|
]
|
||||||
|
@message = @message.find_by(id: params[:message_id])
|
||||||
|
raise Discourse::NotFound unless @message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -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
|
||||||
10
plugins/chat/app/controllers/chat/emojis_controller.rb
Normal file
10
plugins/chat/app/controllers/chat/emojis_controller.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class EmojisController < ::Chat::BaseController
|
||||||
|
def index
|
||||||
|
emojis = Emoji.all.group_by(&:group)
|
||||||
|
render json: MultiJson.dump(emojis)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -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(&object.method(:default_actions_for_service).call) if default_actions
|
||||||
instance_exec(&(block || proc {}))
|
instance_exec(&(block || proc {}))
|
||||||
end
|
end
|
||||||
Chat::ServiceRunner.call(service, object, **dependencies, &merged_block)
|
ServiceRunner.call(service, object, **dependencies, &merged_block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_service(service, dependencies)
|
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
|
||||||
40
plugins/chat/app/jobs/regular/chat/channel_archive.rb
Normal file
40
plugins/chat/app/jobs/regular/chat/channel_archive.rb
Normal file
@ -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
|
||||||
63
plugins/chat/app/jobs/regular/chat/channel_delete.rb
Normal file
63
plugins/chat/app/jobs/regular/chat/channel_delete.rb
Normal file
@ -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
|
||||||
15
plugins/chat/app/jobs/regular/chat/delete_user_messages.rb
Normal file
15
plugins/chat/app/jobs/regular/chat/delete_user_messages.rb
Normal file
@ -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
|
||||||
148
plugins/chat/app/jobs/regular/chat/notify_mentioned.rb
Normal file
148
plugins/chat/app/jobs/regular/chat/notify_mentioned.rb
Normal file
@ -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
|
||||||
88
plugins/chat/app/jobs/regular/chat/notify_watching.rb
Normal file
88
plugins/chat/app/jobs/regular/chat/notify_watching.rb
Normal file
@ -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
|
||||||
27
plugins/chat/app/jobs/regular/chat/process_message.rb
Normal file
27
plugins/chat/app/jobs/regular/chat/process_message.rb
Normal file
@ -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
|
|
||||||
17
plugins/chat/app/jobs/scheduled/chat/auto_join_users.rb
Normal file
17
plugins/chat/app/jobs/scheduled/chat/auto_join_users.rb
Normal file
@ -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
|
||||||
40
plugins/chat/app/jobs/scheduled/chat/delete_old_messages.rb
Normal file
40
plugins/chat/app/jobs/scheduled/chat/delete_old_messages.rb
Normal file
@ -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
|
||||||
15
plugins/chat/app/jobs/scheduled/chat/email_notifications.rb
Normal file
15
plugins/chat/app/jobs/scheduled/chat/email_notifications.rb
Normal file
@ -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
|
||||||
16
plugins/chat/app/jobs/scheduled/chat/periodical_updates.rb
Normal file
16
plugins/chat/app/jobs/scheduled/chat/periodical_updates.rb
Normal file
@ -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
|
|
||||||
51
plugins/chat/app/models/chat/category_channel.rb
Normal file
51
plugins/chat/app/models/chat/category_channel.rb
Normal file
@ -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
|
||||||
196
plugins/chat/app/models/chat/channel.rb
Normal file
196
plugins/chat/app/models/chat/channel.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatChannelArchive < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_channel
|
class ChannelArchive < ActiveRecord::Base
|
||||||
belongs_to :archived_by, class_name: "User"
|
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?
|
def complete?
|
||||||
self.archived_messages >= self.total_messages && self.chat_channel.chat_messages.count.zero?
|
self.archived_messages >= self.total_messages && self.chat_channel.chat_messages.count.zero?
|
||||||
end
|
end
|
||||||
|
|
||||||
def failed?
|
def failed?
|
||||||
!complete? && self.archive_error.present?
|
!complete? && self.archive_error.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_topic?
|
def new_topic?
|
||||||
self.destination_topic_title.present?
|
self.destination_topic_title.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
17
plugins/chat/app/models/chat/deleted_user.rb
Normal file
17
plugins/chat/app/models/chat/deleted_user.rb
Normal file
@ -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
|
||||||
72
plugins/chat/app/models/chat/direct_message.rb
Normal file
72
plugins/chat/app/models/chat/direct_message.rb
Normal file
@ -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
|
||||||
|
#
|
||||||
35
plugins/chat/app/models/chat/direct_message_channel.rb
Normal file
35
plugins/chat/app/models/chat/direct_message_channel.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DirectMessageUser < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :direct_message, foreign_key: :direct_message_channel_id
|
class DirectMessageUser < ActiveRecord::Base
|
||||||
belongs_to :user
|
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
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
@ -1,13 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatDraft < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :user
|
class Draft < ActiveRecord::Base
|
||||||
belongs_to :chat_channel
|
belongs_to :user
|
||||||
|
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||||
|
|
||||||
validate :data_length
|
self.table_name = "chat_drafts"
|
||||||
def data_length
|
|
||||||
if self.data && self.data.length > SiteSetting.max_chat_draft_length
|
validate :data_length
|
||||||
self.errors.add(:base, I18n.t("chat.errors.draft_too_long"))
|
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
|
end
|
||||||
end
|
end
|
||||||
@ -1,13 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class IncomingChatWebhook < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_channel
|
class IncomingWebhook < ActiveRecord::Base
|
||||||
has_many :chat_webhook_events
|
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
|
before_create { self.key = SecureRandom.hex(12) }
|
||||||
"#{Discourse.base_url}/chat/hooks/#{key}.json"
|
|
||||||
|
def url
|
||||||
|
"#{Discourse.base_url}/chat/hooks/#{key}.json"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1,9 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatMention < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :user
|
class Mention < ActiveRecord::Base
|
||||||
belongs_to :chat_message
|
self.table_name = "chat_mentions"
|
||||||
belongs_to :notification, dependent: :destroy
|
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :chat_message, class_name: "Chat::Message"
|
||||||
|
belongs_to :notification, dependent: :destroy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
360
plugins/chat/app/models/chat/message.rb
Normal file
360
plugins/chat/app/models/chat/message.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatMessageReaction < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_message
|
class MessageReaction < ActiveRecord::Base
|
||||||
belongs_to :user
|
self.table_name = "chat_message_reactions"
|
||||||
|
|
||||||
|
belongs_to :chat_message, class_name: "Chat::Message"
|
||||||
|
belongs_to :user
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
@ -1,8 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatMessageRevision < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_message
|
class MessageRevision < ActiveRecord::Base
|
||||||
belongs_to :user
|
self.table_name = "chat_message_revisions"
|
||||||
|
|
||||||
|
belongs_to :chat_message, class_name: "Chat::Message"
|
||||||
|
belongs_to :user
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
159
plugins/chat/app/models/chat/reviewable_message.rb
Normal file
159
plugins/chat/app/models/chat/reviewable_message.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatThread < ActiveRecord::Base
|
module Chat
|
||||||
EXCERPT_LENGTH = 150
|
class Thread < ActiveRecord::Base
|
||||||
|
EXCERPT_LENGTH = 150
|
||||||
|
|
||||||
belongs_to :channel, foreign_key: "channel_id", class_name: "ChatChannel"
|
self.table_name = "chat_threads"
|
||||||
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"
|
|
||||||
|
|
||||||
has_many :chat_messages,
|
belongs_to :channel, foreign_key: "channel_id", class_name: "Chat::Channel"
|
||||||
-> { order("chat_messages.created_at ASC, chat_messages.id ASC") },
|
belongs_to :original_message_user, foreign_key: "original_message_user_id", class_name: "User"
|
||||||
foreign_key: :thread_id,
|
belongs_to :original_message, foreign_key: "original_message_id", class_name: "Chat::Message"
|
||||||
primary_key: :id
|
|
||||||
|
|
||||||
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
|
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
|
||||||
"#{channel.url}/t/#{self.id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def relative_url
|
def url
|
||||||
"#{channel.relative_url}/t/#{self.id}"
|
"#{channel.url}/t/#{self.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def excerpt
|
def relative_url
|
||||||
original_message.excerpt(max_length: EXCERPT_LENGTH)
|
"#{channel.relative_url}/t/#{self.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def excerpt
|
||||||
|
original_message.excerpt(max_length: EXCERPT_LENGTH)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -5,11 +5,15 @@
|
|||||||
#
|
#
|
||||||
# NOTE: Do not use this model anymore, chat messages are linked to uploads via
|
# NOTE: Do not use this model anymore, chat messages are linked to uploads via
|
||||||
# the UploadReference table now, just like everything else.
|
# the UploadReference table now, just like everything else.
|
||||||
class ChatUpload < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_message
|
class Upload < ActiveRecord::Base
|
||||||
belongs_to :upload
|
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
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
@ -1,18 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UserChatChannelMembership < ActiveRecord::Base
|
module Chat
|
||||||
NOTIFICATION_LEVELS = { never: 0, mention: 1, always: 2 }
|
class UserChatChannelMembership < ActiveRecord::Base
|
||||||
|
self.table_name = "user_chat_channel_memberships"
|
||||||
|
|
||||||
belongs_to :user
|
NOTIFICATION_LEVELS = { never: 0, mention: 1, always: 2 }
|
||||||
belongs_to :chat_channel
|
|
||||||
belongs_to :last_read_message, class_name: "ChatMessage", optional: true
|
|
||||||
|
|
||||||
enum :desktop_notification_level, NOTIFICATION_LEVELS, prefix: :desktop_notifications
|
belongs_to :user
|
||||||
enum :mobile_notification_level, NOTIFICATION_LEVELS, prefix: :mobile_notifications
|
belongs_to :last_read_message, class_name: "Chat::Message", optional: true
|
||||||
enum :join_mode, { manual: 0, automatic: 1 }
|
belongs_to :chat_channel, class_name: "Chat::Channel", foreign_key: :chat_channel_id
|
||||||
|
|
||||||
attribute :unread_count, default: 0
|
enum :desktop_notification_level, NOTIFICATION_LEVELS, prefix: :desktop_notifications
|
||||||
attribute :unread_mentions, default: 0
|
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
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
95
plugins/chat/app/models/chat/view.rb
Normal file
95
plugins/chat/app/models/chat/view.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatWebhookEvent < ActiveRecord::Base
|
module Chat
|
||||||
belongs_to :chat_message
|
class WebhookEvent < ActiveRecord::Base
|
||||||
belongs_to :incoming_chat_webhook
|
self.table_name = "chat_webhook_events"
|
||||||
|
|
||||||
delegate :username, to: :incoming_chat_webhook
|
belongs_to :chat_message, class_name: "Chat::Message"
|
||||||
delegate :emoji, to: :incoming_chat_webhook
|
belongs_to :incoming_chat_webhook, class_name: "Chat::IncomingWebhook"
|
||||||
|
|
||||||
|
delegate :username, to: :incoming_chat_webhook
|
||||||
|
delegate :emoji, to: :incoming_chat_webhook
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == 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
|
|
||||||
55
plugins/chat/app/models/concerns/chat/chatable.rb
Normal file
55
plugins/chat/app/models/concerns/chat/chatable.rb
Normal file
@ -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
|
|
||||||
49
plugins/chat/app/queries/chat/channel_memberships_query.rb
Normal file
49
plugins/chat/app/queries/chat/channel_memberships_query.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatChannelUnreadsQuery
|
module Chat
|
||||||
def self.call(channel_id:, user_id:)
|
class ChannelUnreadsQuery
|
||||||
sql = <<~SQL
|
def self.call(channel_id:, user_id:)
|
||||||
|
sql = <<~SQL
|
||||||
SELECT (
|
SELECT (
|
||||||
SELECT COUNT(*) AS unread_count
|
SELECT COUNT(*) AS unread_count
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
@ -27,14 +28,15 @@ class ChatChannelUnreadsQuery
|
|||||||
) AS mention_count;
|
) AS mention_count;
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
DB
|
DB
|
||||||
.query(
|
.query(
|
||||||
sql,
|
sql,
|
||||||
channel_id: channel_id,
|
channel_id: channel_id,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
notification_type: Notification.types[:chat_mention],
|
notification_type: Notification.types[:chat_mention],
|
||||||
)
|
)
|
||||||
.first
|
.first
|
||||||
.to_h
|
.to_h
|
||||||
|
end
|
||||||
end
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user