mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Introduce a Service::ActionBase class for service actions
This will help to enforce a consistent pattern for creating service actions. This patch also namespaces actions and policies, making everything related to a service available directly in `app/services/<concept-name>`, making things more consistent at that level too.
This commit is contained in:
committed by
Loïc Guitaut
parent
aa9c59a24b
commit
05b8ff436c
@@ -18,52 +18,12 @@ module Chat
|
||||
# Here, we can efficiently query the channel category permissions and figure
|
||||
# out which of the users provided should have their [Chat::UserChatChannelMembership]
|
||||
# records removed based on those security cases.
|
||||
class CalculateMembershipsForRemoval
|
||||
def self.call(scoped_users_query:, channel_ids: nil)
|
||||
channel_permissions_map =
|
||||
DB.query(<<~SQL, readonly: CategoryGroup.permission_types[:readonly])
|
||||
WITH category_group_channel_map AS (
|
||||
SELECT category_groups.group_id,
|
||||
category_groups.permission_type,
|
||||
chat_channels.id AS channel_id
|
||||
FROM category_groups
|
||||
INNER JOIN categories ON categories.id = category_groups.category_id
|
||||
INNER JOIN chat_channels ON categories.id = chat_channels.chatable_id
|
||||
AND chat_channels.chatable_type = 'Category'
|
||||
)
|
||||
|
||||
SELECT chat_channels.id AS channel_id,
|
||||
chat_channels.chatable_id AS category_id,
|
||||
(
|
||||
SELECT string_agg(category_group_channel_map.group_id::varchar, ',')
|
||||
FROM category_group_channel_map
|
||||
WHERE category_group_channel_map.permission_type < :readonly AND
|
||||
category_group_channel_map.channel_id = chat_channels.id
|
||||
) AS groups_with_write_permissions,
|
||||
(
|
||||
SELECT string_agg(category_group_channel_map.group_id::varchar, ',')
|
||||
FROM category_group_channel_map
|
||||
WHERE category_group_channel_map.permission_type = :readonly AND
|
||||
category_group_channel_map.channel_id = chat_channels.id
|
||||
) AS groups_with_readonly_permissions,
|
||||
categories.read_restricted
|
||||
FROM category_group_channel_map
|
||||
INNER JOIN chat_channels ON chat_channels.id = category_group_channel_map.channel_id
|
||||
INNER JOIN categories ON categories.id = chat_channels.chatable_id
|
||||
WHERE chat_channels.chatable_type = 'Category'
|
||||
#{channel_ids.present? ? "AND chat_channels.id IN (#{channel_ids.join(",")})" : ""}
|
||||
GROUP BY chat_channels.id, chat_channels.chatable_id, categories.read_restricted
|
||||
ORDER BY channel_id
|
||||
SQL
|
||||
|
||||
scoped_memberships =
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:chat_channel)
|
||||
.where(user_id: scoped_users_query.select(:id))
|
||||
.where(chat_channel_id: channel_permissions_map.map(&:channel_id))
|
||||
class CalculateMembershipsForRemoval < Service::ActionBase
|
||||
option :scoped_users_query
|
||||
option :channel_ids, [], optional: true
|
||||
|
||||
def call
|
||||
memberships_to_remove = []
|
||||
|
||||
scoped_memberships.find_each do |membership|
|
||||
channel_permission =
|
||||
channel_permissions_map.find { |cpm| cpm.channel_id == membership.chat_channel_id }
|
||||
@@ -102,6 +62,54 @@ module Chat
|
||||
|
||||
memberships_to_remove
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def channel_permissions_map
|
||||
@channel_permissions_map ||=
|
||||
DB.query(<<~SQL, readonly: CategoryGroup.permission_types[:readonly])
|
||||
WITH category_group_channel_map AS (
|
||||
SELECT category_groups.group_id,
|
||||
category_groups.permission_type,
|
||||
chat_channels.id AS channel_id
|
||||
FROM category_groups
|
||||
INNER JOIN categories ON categories.id = category_groups.category_id
|
||||
INNER JOIN chat_channels ON categories.id = chat_channels.chatable_id
|
||||
AND chat_channels.chatable_type = 'Category'
|
||||
)
|
||||
|
||||
SELECT chat_channels.id AS channel_id,
|
||||
chat_channels.chatable_id AS category_id,
|
||||
(
|
||||
SELECT string_agg(category_group_channel_map.group_id::varchar, ',')
|
||||
FROM category_group_channel_map
|
||||
WHERE category_group_channel_map.permission_type < :readonly AND
|
||||
category_group_channel_map.channel_id = chat_channels.id
|
||||
) AS groups_with_write_permissions,
|
||||
(
|
||||
SELECT string_agg(category_group_channel_map.group_id::varchar, ',')
|
||||
FROM category_group_channel_map
|
||||
WHERE category_group_channel_map.permission_type = :readonly AND
|
||||
category_group_channel_map.channel_id = chat_channels.id
|
||||
) AS groups_with_readonly_permissions,
|
||||
categories.read_restricted
|
||||
FROM category_group_channel_map
|
||||
INNER JOIN chat_channels ON chat_channels.id = category_group_channel_map.channel_id
|
||||
INNER JOIN categories ON categories.id = chat_channels.chatable_id
|
||||
WHERE chat_channels.chatable_type = 'Category'
|
||||
#{channel_ids.present? ? "AND chat_channels.id IN (#{channel_ids.join(",")})" : ""}
|
||||
GROUP BY chat_channels.id, chat_channels.chatable_id, categories.read_restricted
|
||||
ORDER BY channel_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def scoped_memberships
|
||||
@scoped_memberships ||=
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:chat_channel)
|
||||
.where(user_id: scoped_users_query.select(:id))
|
||||
.where(chat_channel_id: channel_permissions_map.map(&:channel_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,19 +2,11 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class CreateMembershipsForAutoJoin
|
||||
def self.call(channel:, contract:)
|
||||
query_args = {
|
||||
chat_channel_id: channel.id,
|
||||
start: contract.start_user_id,
|
||||
end: contract.end_user_id,
|
||||
suspended_until: Time.zone.now,
|
||||
last_seen_at: 3.months.ago,
|
||||
channel_category: channel.category.id,
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
everyone: Group::AUTO_GROUPS[:everyone],
|
||||
mode: ::Chat::UserChatChannelMembership.join_modes[:automatic],
|
||||
}
|
||||
class CreateMembershipsForAutoJoin < Service::ActionBase
|
||||
option :channel
|
||||
option :contract
|
||||
|
||||
def call
|
||||
::DB.query_single(<<~SQL, query_args)
|
||||
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
|
||||
@@ -44,6 +36,22 @@ module Chat
|
||||
RETURNING user_chat_channel_memberships.user_id
|
||||
SQL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_args
|
||||
{
|
||||
chat_channel_id: channel.id,
|
||||
start: contract.start_user_id,
|
||||
end: contract.end_user_id,
|
||||
suspended_until: Time.zone.now,
|
||||
last_seen_at: 3.months.ago,
|
||||
channel_category: channel.category.id,
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
everyone: Group::AUTO_GROUPS[:everyone],
|
||||
mode: ::Chat::UserChatChannelMembership.join_modes[:automatic],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ module Chat
|
||||
module Action
|
||||
# When updating the read state of chat channel memberships, we also need
|
||||
# to be sure to mark any mention-based notifications read at the same time.
|
||||
class MarkMentionsRead
|
||||
class MarkMentionsRead < Service::ActionBase
|
||||
# @param [User] user The user that we are marking notifications read for.
|
||||
# @param [Array] channel_ids The chat channels that are having their notifications
|
||||
# marked as read.
|
||||
@@ -12,7 +12,12 @@ module Chat
|
||||
# mentions read for in the channel.
|
||||
# @param [Integer] thread_id Optional, if provided then all notifications related
|
||||
# to messages in the thread will be marked as read.
|
||||
def self.call(user, channel_ids:, message_id: nil, thread_id: nil)
|
||||
param :user
|
||||
option :channel_ids, []
|
||||
option :message_id, optional: true
|
||||
option :thread_id, optional: true
|
||||
|
||||
def call
|
||||
::Notification
|
||||
.where(notification_type: Notification.types[:chat_mention])
|
||||
.where(user: user)
|
||||
|
||||
@@ -2,19 +2,11 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class PublishAndFollowDirectMessageChannel
|
||||
attr_reader :channel_membership
|
||||
class PublishAndFollowDirectMessageChannel < Service::ActionBase
|
||||
option :channel_membership
|
||||
|
||||
delegate :chat_channel, :user, to: :channel_membership
|
||||
|
||||
def self.call(...)
|
||||
new(...).call
|
||||
end
|
||||
|
||||
def initialize(channel_membership:)
|
||||
@channel_membership = channel_membership
|
||||
end
|
||||
|
||||
def call
|
||||
return unless chat_channel.direct_message_channel?
|
||||
return if users_allowing_communication.none?
|
||||
|
||||
@@ -7,12 +7,15 @@ module Chat
|
||||
# were removed and from which channel, as well as logging
|
||||
# this in staff actions so it's obvious why these users were
|
||||
# removed.
|
||||
class PublishAutoRemovedUser
|
||||
class PublishAutoRemovedUser < Service::ActionBase
|
||||
# @param [Symbol] event_type What caused the users to be removed,
|
||||
# each handler will define this, e.g. category_updated, user_removed_from_group
|
||||
# @param [Hash] users_removed_map A hash with channel_id as its keys and an
|
||||
# array of user_ids who were removed from the channel.
|
||||
def self.call(event_type:, users_removed_map:)
|
||||
option :event_type
|
||||
option :users_removed_map
|
||||
|
||||
def call
|
||||
return if users_removed_map.empty?
|
||||
|
||||
users_removed_map.each do |channel_id, user_ids|
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class RemoveMemberships
|
||||
def self.call(memberships:)
|
||||
class RemoveMemberships < Service::ActionBase
|
||||
option :memberships
|
||||
|
||||
def call
|
||||
memberships
|
||||
.destroy_all
|
||||
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |obj, hash|
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class ResetChannelsLastMessageIds
|
||||
class ResetChannelsLastMessageIds < Service::ActionBase
|
||||
# @param [Array] last_message_ids The message IDs to match with the
|
||||
# last_message_id in Chat::Channel which will be reset
|
||||
# to NULL or the most recent non-deleted message in the channel to
|
||||
# update read state.
|
||||
# @param [Integer] channel_ids The channel IDs to update. This is used
|
||||
# to scope the queries better.
|
||||
def self.call(last_message_ids, channel_ids)
|
||||
param :last_message_ids, []
|
||||
param :channel_ids, []
|
||||
|
||||
def call
|
||||
Chat::Channel
|
||||
.where(id: channel_ids)
|
||||
.where("last_message_id IN (?)", last_message_ids)
|
||||
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class ResetUserLastReadChannelMessage
|
||||
class ResetUserLastReadChannelMessage < Service::ActionBase
|
||||
# @param [Array] last_read_message_ids The message IDs to match with the
|
||||
# last_read_message_ids in UserChatChannelMembership which will be reset
|
||||
# to NULL or the most recent non-deleted message in the channel to
|
||||
# update read state.
|
||||
# @param [Integer] channel_ids The channel IDs of the memberships to update,
|
||||
# this is used to find the latest non-deleted message in the channel.
|
||||
def self.call(last_read_message_ids, channel_ids)
|
||||
sql = <<~SQL
|
||||
param :last_read_message_ids, []
|
||||
param :channel_ids, []
|
||||
|
||||
def call
|
||||
DB.exec(sql_query, last_read_message_ids:, channel_ids:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sql_query
|
||||
<<~SQL
|
||||
-- update the last_read_message_id to the most recent
|
||||
-- non-deleted message in the channel so unread counts are correct.
|
||||
-- the cte row_number is necessary to only return a single row
|
||||
@@ -33,8 +42,6 @@ module Chat
|
||||
SET last_read_message_id = NULL
|
||||
WHERE last_read_message_id IN (:last_read_message_ids);
|
||||
SQL
|
||||
|
||||
DB.exec(sql, last_read_message_ids: last_read_message_ids, channel_ids: channel_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class ResetUserLastReadThreadMessage
|
||||
class ResetUserLastReadThreadMessage < Service::ActionBase
|
||||
# @param [Array] last_read_message_ids The message IDs to match with the
|
||||
# last_read_message_ids in UserChatThreadMembership which will be reset
|
||||
# to NULL or the most recent non-deleted message in the thread to
|
||||
# update read state.
|
||||
# @param [Integer] thread_ids The thread IDs of the memberships to update,
|
||||
# this is used to find the latest non-deleted message in the thread.
|
||||
def self.call(last_read_message_ids, thread_ids)
|
||||
sql = <<~SQL
|
||||
param :last_read_message_ids, []
|
||||
param :thread_ids, []
|
||||
|
||||
def call
|
||||
DB.exec(sql_query, last_read_message_ids:, thread_ids:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sql_query
|
||||
<<~SQL
|
||||
-- update the last_read_message_id to the most recent
|
||||
-- non-deleted message in the thread so unread counts are correct.
|
||||
-- the cte row_number is necessary to only return a single row
|
||||
@@ -40,8 +49,6 @@ module Chat
|
||||
SET last_read_message_id = NULL
|
||||
WHERE last_read_message_id IN (:last_read_message_ids);
|
||||
SQL
|
||||
|
||||
DB.exec(sql, last_read_message_ids: last_read_message_ids, thread_ids: thread_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ module Chat
|
||||
policy :can_add_users_to_channel
|
||||
model :target_users, optional: true
|
||||
policy :satisfies_dms_max_users_limit,
|
||||
class_name: Chat::DirectMessageChannel::MaxUsersExcessPolicy
|
||||
class_name: Chat::DirectMessageChannel::Policy::MaxUsersExcess
|
||||
|
||||
transaction do
|
||||
step :upsert_memberships
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Channel::MessageCreationPolicy < PolicyBase
|
||||
class Chat::Channel::Policy::MessageCreation < Service::PolicyBase
|
||||
class DirectMessageStrategy
|
||||
class << self
|
||||
def call(guardian, channel)
|
||||
@@ -27,11 +27,11 @@ module Chat
|
||||
model :target_users
|
||||
policy :can_create_direct_message
|
||||
policy :satisfies_dms_max_users_limit,
|
||||
class_name: Chat::DirectMessageChannel::MaxUsersExcessPolicy
|
||||
class_name: Chat::DirectMessageChannel::Policy::MaxUsersExcess
|
||||
model :user_comm_screener
|
||||
policy :actor_allows_dms
|
||||
policy :targets_allow_dms_from_user,
|
||||
class_name: Chat::DirectMessageChannel::CanCommunicateAllPartiesPolicy
|
||||
class_name: Chat::DirectMessageChannel::Policy::CanCommunicateAllParties
|
||||
model :direct_message, :fetch_or_create_direct_message
|
||||
model :channel, :fetch_or_create_channel
|
||||
step :set_optional_name
|
||||
|
||||
@@ -26,7 +26,7 @@ module Chat
|
||||
model :channel
|
||||
step :enforce_membership
|
||||
model :membership
|
||||
policy :allowed_to_create_message_in_channel, class_name: Chat::Channel::MessageCreationPolicy
|
||||
policy :allowed_to_create_message_in_channel, class_name: Chat::Channel::Policy::MessageCreation
|
||||
model :reply, optional: true
|
||||
policy :ensure_reply_consistency
|
||||
model :thread, optional: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Chat::DirectMessageChannel::CanCommunicateAllPartiesPolicy < PolicyBase
|
||||
class Chat::DirectMessageChannel::Policy::CanCommunicateAllParties < Service::PolicyBase
|
||||
delegate :target_users, :user_comm_screener, to: :context
|
||||
|
||||
def call
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Chat::DirectMessageChannel::MaxUsersExcessPolicy < PolicyBase
|
||||
class Chat::DirectMessageChannel::Policy::MaxUsersExcess < Service::PolicyBase
|
||||
delegate :target_users, to: :context
|
||||
|
||||
def call
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::Channel::MessageCreationPolicy do
|
||||
RSpec.describe Chat::Channel::Policy::MessageCreation do
|
||||
subject(:policy) { described_class.new(context) }
|
||||
|
||||
fab!(:user)
|
||||
Reference in New Issue
Block a user