mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
PERF: auto join & leave chat channels (#29193)
Chat channels that are linked to a category can be set to automatically join users. This is handled by subscribing to the following events - group_destroyed - user_seen - user_confirmed_email - user_added_to_group - user_removed_from_group - category_updated - site_setting_changed (for `chat_allowed_groups`) As well as a - hourly background job (`AutoJoinUsers`) - `CreateCategoryChannel` service - `UpdateChannel` service There was however two issues with the current implementation 1. We were triggering a lot of background jobs, mostly because it was decided to batch to auto join/leave into groups of 1000 users, adding a lot of stress to the system 2. We had one "class" (a service or a background job) per "event" and all of them had slightly different ways to select users to join/leave, making it hard to keep everything in sync This PR "simply" adds two new servicesL `AutoJoinChannels` and `AutoLeaveChannels` that takes care, in an efficient way, of all the cases when users might automatically join a leave a chat channel. Every other changes come from the fact that we're now always calling either one of those services, depending on the event that happened. In the making of these classes, a few bugs were encountered and fixed, notably - A user is only ever able to access chat channels if and only if they're part of a group listed in the `chat_allowed_group` site setting - A category that has no associated "category groups" is only accessible to staff members (and not "Everyone") - A silenced user should not be able to automatically join channels - We should not attempt to automatically join users to deleted chat channels - There is no need to automatically join users to chat channels that have already more than `max_chat_auto_joined_users` users Internal - t/135259 & t/70607 * DEV: add specs for auto join/leave channels services * DEV: less hacky specs * DEV: no instance variables in specs
This commit is contained in:
parent
34ed35e174
commit
6dfe2fbe16
@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoJoinChannelBatch < ::Jobs::Base
|
||||
def execute(args)
|
||||
::Chat::AutoJoinChannelBatch.call(params: args) do
|
||||
on_failure { Rails.logger.error("Failed with unexpected error") }
|
||||
on_failed_contract do |contract|
|
||||
Rails.logger.error(contract.errors.full_messages.join(", "))
|
||||
end
|
||||
on_model_not_found(:channel) do |params:|
|
||||
Rails.logger.error("Channel not found (id=#{params.channel_id})")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -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
|
||||
module Chat
|
||||
class AutoJoinChannelMemberships < ::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
|
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoRemoveMembershipHandleCategoryUpdated < ::Jobs::Base
|
||||
def execute(args)
|
||||
::Chat::AutoRemove::HandleCategoryUpdated.call(params: args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoRemoveMembershipHandleChatAllowedGroupsChange < ::Jobs::Base
|
||||
def execute(args)
|
||||
::Chat::AutoRemove::HandleChatAllowedGroupsChange.call(params: args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoRemoveMembershipHandleDestroyedGroup < ::Jobs::Base
|
||||
def execute(args)
|
||||
::Chat::AutoRemove::HandleDestroyedGroup.call(params: args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module Chat
|
||||
class AutoRemoveMembershipHandleUserRemovedFromGroup < ::Jobs::Base
|
||||
def execute(args)
|
||||
::Chat::AutoRemove::HandleUserRemovedFromGroup.call(params: args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -5,62 +5,11 @@ module Jobs
|
||||
class AutoJoinUsers < ::Jobs::Scheduled
|
||||
every 1.hour
|
||||
|
||||
LAST_SEEN_DAYS = 30
|
||||
|
||||
def execute(_args)
|
||||
def execute(args = {})
|
||||
return if !SiteSetting.chat_enabled
|
||||
|
||||
allowed_group_permissions = [
|
||||
CategoryGroup.permission_types[:create_post],
|
||||
CategoryGroup.permission_types[:full],
|
||||
]
|
||||
|
||||
join_mode = ::Chat::UserChatChannelMembership.join_modes[:automatic]
|
||||
|
||||
sql = <<~SQL
|
||||
WITH users AS (
|
||||
SELECT id FROM users u
|
||||
JOIN user_options uo ON uo.user_id = u.id
|
||||
WHERE id > 0 AND (u.suspended_till IS NULL OR u.suspended_till <= :now)
|
||||
AND (u.last_seen_at IS NULL OR u.last_seen_at > :last_seen_at)
|
||||
AND u.active
|
||||
AND NOT u.staged
|
||||
AND uo.chat_enabled
|
||||
AND NOT EXISTS (SELECT 1 FROM anonymous_users a WHERE a.user_id = u.id)
|
||||
ORDER BY last_seen_at desc
|
||||
LIMIT :max_users
|
||||
)
|
||||
|
||||
INSERT INTO user_chat_channel_memberships (user_id, chat_channel_id, following, created_at, updated_at, join_mode)
|
||||
SELECT DISTINCT users.id AS user_id,
|
||||
chat_channels.id AS chat_channel_id,
|
||||
TRUE AS following,
|
||||
:now::timestamp AS created_at,
|
||||
:now::timestamp AS updated_at,
|
||||
:join_mode AS join_mode
|
||||
FROM users
|
||||
JOIN chat_channels on auto_join_users AND chatable_type = 'Category'
|
||||
JOIN categories c on c.id = chat_channels.chatable_id
|
||||
|
||||
LEFT OUTER JOIN user_chat_channel_memberships uccm ON uccm.chat_channel_id = chat_channels.id
|
||||
AND uccm.user_id = users.id
|
||||
LEFT OUTER JOIN group_users gu ON gu.user_id = users.id
|
||||
LEFT OUTER JOIN category_groups cg ON cg.group_id = gu.group_id
|
||||
AND cg.permission_type in (:allowed_group_permissions)
|
||||
AND c.id = cg.category_id
|
||||
|
||||
WHERE (cg.group_id is NOT null OR NOT c.read_restricted) AND uccm.id IS NULL
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
|
||||
DB.exec(
|
||||
sql,
|
||||
now: Time.zone.now,
|
||||
last_seen_at: LAST_SEEN_DAYS.days.ago,
|
||||
allowed_group_permissions: allowed_group_permissions,
|
||||
join_mode: join_mode,
|
||||
max_users: SiteSetting.max_chat_auto_joined_users,
|
||||
)
|
||||
::Chat::AutoJoinChannels.call(params: {})
|
||||
::Chat::AutoLeaveChannels.call(params: { event: args[:event]&.to_sym || :hourly_job })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -11,11 +11,12 @@ module Chat
|
||||
define_method(name) { true }
|
||||
end
|
||||
|
||||
STAFF_GROUP_IDS = Group::AUTO_GROUPS.values_at(:admins, :moderators, :staff)
|
||||
|
||||
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)
|
||||
STAFF_GROUP_IDS | category.secure_group_ids
|
||||
end
|
||||
|
||||
def title(_ = nil)
|
||||
|
@ -7,7 +7,7 @@ module Chat
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:user)
|
||||
.includes(:user)
|
||||
.where(user: User.human_users.activated.not_suspended.not_staged)
|
||||
.where(user: User.real.activated.not_staged.not_suspended.not_silenced)
|
||||
.where(chat_channel: channel)
|
||||
|
||||
query = query.where(following: true) if channel.category_channel?
|
||||
|
@ -1,115 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
# There is significant complexity around category channel permissions,
|
||||
# since they are inferred from [CategoryGroup] records and their corresponding
|
||||
# permission types.
|
||||
#
|
||||
# To be able to join and chat in a channel, a user must either be staff,
|
||||
# or be in a group that has either `full` or `create_post` permissions
|
||||
# via [CategoryGroup].
|
||||
#
|
||||
# However, there is an edge case. If there are no [CategoryGroup] records
|
||||
# for a given category, this means that the [Group::AUTO_GROUPS[:everyone]]
|
||||
# group has `full` access to the channel, therefore everyone can post in
|
||||
# the chat channel (so long as they are in one of the `SiteSetting.chat_allowed_groups`)
|
||||
#
|
||||
# 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 < 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 }
|
||||
|
||||
# If there is no channel in the map, this means there are no
|
||||
# category_groups for the channel.
|
||||
#
|
||||
# This in turn means the Everyone group with full permission
|
||||
# is the only group that can access the channel (no category_group
|
||||
# record is created in this case), we do not need to remove any users.
|
||||
next if channel_permission.blank?
|
||||
|
||||
group_ids_with_write_permission =
|
||||
channel_permission.groups_with_write_permissions.to_s.split(",").map(&:to_i)
|
||||
group_ids_with_read_permission =
|
||||
channel_permission.groups_with_readonly_permissions.to_s.split(",").map(&:to_i)
|
||||
|
||||
# None of the groups on the channel have permission to do anything
|
||||
# more than read only, remove the membership.
|
||||
if group_ids_with_write_permission.empty? && group_ids_with_read_permission.any?
|
||||
memberships_to_remove << membership.id
|
||||
next
|
||||
end
|
||||
|
||||
# At least one of the groups on the channel can create_post or
|
||||
# has full permission, remove the membership if the user is in none
|
||||
# of these groups.
|
||||
if group_ids_with_write_permission.any?
|
||||
scoped_user = scoped_users_query.where(id: membership.user_id).first
|
||||
|
||||
if !scoped_user&.in_any_groups?(group_ids_with_write_permission)
|
||||
memberships_to_remove << membership.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
@ -1,57 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class CreateMembershipsForAutoJoin < Service::ActionBase
|
||||
option :channel
|
||||
option :params
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
LEFT OUTER JOIN group_users gu ON gu.user_id = users.id
|
||||
LEFT OUTER JOIN category_groups cg ON cg.group_id = gu.group_id AND
|
||||
cg.permission_type <= :permission_type
|
||||
|
||||
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 IS NULL OR last_seen_at > :last_seen_at) AND
|
||||
uo.chat_enabled AND
|
||||
|
||||
(NOT EXISTS(SELECT 1 FROM category_groups WHERE category_id = :channel_category)
|
||||
OR EXISTS (SELECT 1 FROM category_groups WHERE category_id = :channel_category AND group_id = :everyone AND permission_type <= :permission_type)
|
||||
OR cg.category_id = :channel_category)
|
||||
|
||||
ON CONFLICT DO NOTHING
|
||||
|
||||
RETURNING user_chat_channel_memberships.user_id
|
||||
SQL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_args
|
||||
{
|
||||
chat_channel_id: channel.id,
|
||||
start: params.start_user_id,
|
||||
end: params.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
|
@ -8,28 +8,26 @@ module Chat
|
||||
delegate :chat_channel, :user, to: :channel_membership
|
||||
|
||||
def call
|
||||
return unless chat_channel.direct_message_channel?
|
||||
return if users_allowing_communication.none?
|
||||
return if !chat_channel.direct_message_channel?
|
||||
return if user_ids.empty?
|
||||
|
||||
chat_channel
|
||||
.user_chat_channel_memberships
|
||||
.where(user: users_allowing_communication)
|
||||
.where(user_id: user_ids)
|
||||
.update_all(following: true)
|
||||
Chat::Publisher.publish_new_channel(chat_channel, users_allowing_communication)
|
||||
|
||||
Chat::Publisher.publish_new_channel(chat_channel, user_ids)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def users_allowing_communication
|
||||
@users_allowing_communication ||= User.where(id: user_ids).to_a
|
||||
end
|
||||
|
||||
def user_ids
|
||||
UserCommScreener.new(
|
||||
acting_user: user,
|
||||
target_user_ids:
|
||||
chat_channel.user_chat_channel_memberships.where(following: false).pluck(:user_id),
|
||||
).allowing_actor_communication + Array.wrap(current_user_id)
|
||||
@user_ids ||=
|
||||
UserCommScreener.new(
|
||||
acting_user: user,
|
||||
target_user_ids:
|
||||
chat_channel.user_chat_channel_memberships.where(following: false).pluck(:user_id),
|
||||
).allowing_actor_communication + Array.wrap(current_user_id)
|
||||
end
|
||||
|
||||
def current_user_id
|
||||
|
@ -8,31 +8,29 @@ module Chat
|
||||
# this in staff actions so it's obvious why these users were
|
||||
# removed.
|
||||
class PublishAutoRemovedUser < Service::ActionBase
|
||||
# @param [Symbol] event_type What caused the users to be removed,
|
||||
# @param [Symbol] event 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.
|
||||
option :event_type
|
||||
option :event
|
||||
option :users_removed_map
|
||||
|
||||
def call
|
||||
return if users_removed_map.empty?
|
||||
|
||||
users_removed_map.each do |channel_id, user_ids|
|
||||
users_removed_map.each do |channel_id, all_user_ids|
|
||||
next if all_user_ids.empty?
|
||||
|
||||
job_spacer = JobTimeSpacer.new
|
||||
user_ids.in_groups_of(1000, false) do |user_id_batch|
|
||||
job_spacer.enqueue(
|
||||
Jobs::Chat::KickUsersFromChannel,
|
||||
{ channel_id: channel_id, user_ids: user_id_batch },
|
||||
)
|
||||
|
||||
all_user_ids.in_groups_of(1000, false) do |user_ids|
|
||||
job_spacer.enqueue(Jobs::Chat::KickUsersFromChannel, { channel_id:, user_ids: })
|
||||
end
|
||||
|
||||
if user_ids.any?
|
||||
StaffActionLogger.new(Discourse.system_user).log_custom(
|
||||
"chat_auto_remove_membership",
|
||||
{ users_removed: user_ids.length, channel_id: channel_id, event: event_type },
|
||||
)
|
||||
end
|
||||
StaffActionLogger.new(Discourse.system_user).log_custom(
|
||||
"chat_auto_remove_membership",
|
||||
{ users_removed: all_user_ids.size, channel_id:, event: },
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module Action
|
||||
class RemoveMemberships < Service::ActionBase
|
||||
option :memberships
|
||||
|
||||
def call
|
||||
memberships
|
||||
.destroy_all
|
||||
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |obj, hash|
|
||||
hash[obj.chat_channel_id] << obj.user_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,70 +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 Chat
|
||||
# Service responsible to create memberships for a channel and a section of user ids
|
||||
#
|
||||
# @example
|
||||
# Chat::AutoJoinChannelBatch.call(
|
||||
# params: {
|
||||
# channel_id: 1,
|
||||
# start_user_id: 27,
|
||||
# end_user_id: 58,
|
||||
# }
|
||||
# )
|
||||
#
|
||||
class AutoJoinChannelBatch
|
||||
include Service::Base
|
||||
|
||||
params do
|
||||
# Backward-compatible attributes
|
||||
attribute :chat_channel_id, :integer
|
||||
attribute :starts_at, :integer
|
||||
attribute :ends_at, :integer
|
||||
|
||||
# New attributes
|
||||
attribute :channel_id, :integer
|
||||
attribute :start_user_id, :integer
|
||||
attribute :end_user_id, :integer
|
||||
|
||||
validates :channel_id, :start_user_id, :end_user_id, presence: true
|
||||
validates :end_user_id, comparison: { greater_than_or_equal_to: :start_user_id }
|
||||
|
||||
# TODO (joffrey): remove after migration is done
|
||||
before_validation do
|
||||
self.channel_id ||= chat_channel_id
|
||||
self.start_user_id ||= starts_at
|
||||
self.end_user_id ||= ends_at
|
||||
end
|
||||
end
|
||||
model :channel
|
||||
step :create_memberships
|
||||
step :recalculate_user_count
|
||||
step :publish_new_channel
|
||||
|
||||
private
|
||||
|
||||
def fetch_channel(params:)
|
||||
::Chat::CategoryChannel.find_by(id: params.channel_id, auto_join_users: true)
|
||||
end
|
||||
|
||||
def create_memberships(channel:, params:)
|
||||
context[:added_user_ids] = ::Chat::Action::CreateMembershipsForAutoJoin.call(
|
||||
channel:,
|
||||
params:,
|
||||
)
|
||||
end
|
||||
|
||||
def recalculate_user_count(channel:, added_user_ids:)
|
||||
# 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::AutoJoinChannelMemberships
|
||||
return unless added_user_ids.one?
|
||||
::Chat::ChannelMembershipManager.new(channel).recalculate_user_count
|
||||
end
|
||||
|
||||
def publish_new_channel(channel:, added_user_ids:)
|
||||
::Chat::Publisher.publish_new_channel(channel, User.where(id: added_user_ids))
|
||||
end
|
||||
end
|
||||
end
|
105
plugins/chat/app/services/chat/auto_join_channels.rb
Normal file
105
plugins/chat/app/services/chat/auto_join_channels.rb
Normal file
@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class AutoJoinChannels
|
||||
include Service::Base
|
||||
|
||||
ALLOWED_GROUP_PERMISSIONS = [
|
||||
CategoryGroup.permission_types[:create_post],
|
||||
CategoryGroup.permission_types[:full],
|
||||
]
|
||||
|
||||
policy :chat_enabled?
|
||||
|
||||
params do
|
||||
attribute :user_id, :integer
|
||||
attribute :channel_id, :integer
|
||||
attribute :category_id, :integer
|
||||
end
|
||||
|
||||
step :create_memberships
|
||||
|
||||
private
|
||||
|
||||
def chat_enabled?
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def create_memberships(params:)
|
||||
automatic = ::Chat::UserChatChannelMembership.join_modes[:automatic]
|
||||
group_permissions = ALLOWED_GROUP_PERMISSIONS
|
||||
group_ids = SiteSetting.chat_allowed_groups_map
|
||||
everyone_allowed = group_ids.include?(Group::AUTO_GROUPS[:everyone])
|
||||
max_users = SiteSetting.max_chat_auto_joined_users
|
||||
now = Time.zone.now
|
||||
last_seen_at = 30.days.ago
|
||||
|
||||
# used to filter down to a specific user, chat channel, or category
|
||||
user_sql = params.user_id ? "AND u.id = #{params.user_id}" : ""
|
||||
channel_sql = params.channel_id ? "AND cc.id = #{params.channel_id}" : ""
|
||||
category_sql = params.category_id ? "AND c.id = #{params.category_id}" : ""
|
||||
|
||||
sql = <<~SQL
|
||||
WITH chat_users AS ( -- users that are allowed to join chat
|
||||
SELECT u.id
|
||||
FROM users u
|
||||
JOIN user_options uo ON uo.user_id = u.id AND uo.chat_enabled = TRUE
|
||||
WHERE u.id > 0
|
||||
AND u.active = TRUE
|
||||
AND u.staged = FALSE
|
||||
AND (u.suspended_till IS NULL OR u.suspended_till <= :now)
|
||||
AND (u.silenced_till IS NULL OR u.silenced_till <= :now)
|
||||
AND NOT EXISTS (SELECT 1 FROM anonymous_users au WHERE au.user_id = u.id)
|
||||
AND u.last_seen_at > :last_seen_at
|
||||
#{user_sql}
|
||||
#{everyone_allowed ? "" : "AND EXISTS (SELECT 1 FROM group_users gu WHERE gu.user_id = u.id AND gu.group_id IN (:group_ids))"}
|
||||
ORDER BY u.last_seen_at DESC
|
||||
LIMIT :max_users
|
||||
), valid_chat_channels AS ( -- auto joinable chat channels
|
||||
SELECT cc.id, cc.chatable_id
|
||||
FROM chat_channels cc
|
||||
WHERE cc.auto_join_users = TRUE
|
||||
AND cc.chatable_type = 'Category'
|
||||
AND cc.deleted_at IS NULL
|
||||
AND cc.user_count < :max_users
|
||||
#{channel_sql}
|
||||
), public AS ( -- public chat channels
|
||||
SELECT cu.id AS user_id, cc.id AS chat_channel_id
|
||||
FROM valid_chat_channels cc
|
||||
CROSS JOIN chat_users cu
|
||||
JOIN categories c ON c.id = cc.chatable_id AND c.read_restricted = FALSE #{category_sql}
|
||||
), private AS ( -- private chat channels
|
||||
SELECT DISTINCT gu.user_id, cc.id AS chat_channel_id
|
||||
FROM valid_chat_channels cc
|
||||
JOIN categories c ON c.id = cc.chatable_id AND c.read_restricted = TRUE #{category_sql}
|
||||
JOIN category_groups cg ON cg.category_id = c.id AND cg.permission_type IN (:group_permissions)
|
||||
JOIN group_users gu ON gu.group_id = cg.group_id AND gu.user_id IN (SELECT id FROM chat_users)
|
||||
)
|
||||
INSERT INTO user_chat_channel_memberships (user_id, chat_channel_id, following, join_mode, created_at, updated_at)
|
||||
SELECT p.user_id, p.chat_channel_id, TRUE, :automatic, :now, :now
|
||||
FROM (
|
||||
SELECT * FROM public
|
||||
UNION ALL
|
||||
SELECT * FROM private
|
||||
) p
|
||||
LEFT JOIN user_chat_channel_memberships uccm ON uccm.user_id = p.user_id AND uccm.chat_channel_id = p.chat_channel_id
|
||||
WHERE uccm.user_id IS NULL
|
||||
RETURNING chat_channel_id, user_id
|
||||
SQL
|
||||
|
||||
channel_to_users = Hash.new { |h, k| h[k] = [] }
|
||||
args = { now:, last_seen_at:, group_ids:, max_users:, group_permissions:, automatic: }
|
||||
|
||||
DB
|
||||
.query_array(sql, args)
|
||||
.each { |channel_id, user_id| channel_to_users[channel_id] << user_id }
|
||||
|
||||
::Chat::Channel
|
||||
.where(id: channel_to_users.keys)
|
||||
.find_each do |channel|
|
||||
::Chat::ChannelMembershipManager.new(channel).recalculate_user_count
|
||||
::Chat::Publisher.publish_new_channel(channel, channel_to_users[channel.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
84
plugins/chat/app/services/chat/auto_leave_channels.rb
Normal file
84
plugins/chat/app/services/chat/auto_leave_channels.rb
Normal file
@ -0,0 +1,84 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class AutoLeaveChannels
|
||||
include Service::Base
|
||||
|
||||
ALLOWED_GROUP_PERMISSIONS = [
|
||||
CategoryGroup.permission_types[:create_post],
|
||||
CategoryGroup.permission_types[:full],
|
||||
]
|
||||
|
||||
policy :chat_enabled?
|
||||
|
||||
params do
|
||||
attribute :event
|
||||
attribute :user_id, :integer
|
||||
attribute :group_id, :integer
|
||||
attribute :channel_id, :integer
|
||||
attribute :category_id, :integer
|
||||
end
|
||||
|
||||
step :remove_memberships
|
||||
|
||||
private
|
||||
|
||||
def chat_enabled?
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def remove_memberships(params:)
|
||||
group_ids = SiteSetting.chat_allowed_groups_map
|
||||
group_permissions = ALLOWED_GROUP_PERMISSIONS
|
||||
users_removed_map = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
if !group_ids.include?(Group::AUTO_GROUPS[:everyone])
|
||||
sql = <<~SQL
|
||||
DELETE FROM user_chat_channel_memberships uccm
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM group_users gu
|
||||
WHERE gu.user_id = uccm.user_id
|
||||
AND gu.group_id IN (:group_ids)
|
||||
)
|
||||
RETURNING chat_channel_id, user_id
|
||||
SQL
|
||||
|
||||
DB
|
||||
.query_array(sql, group_ids:)
|
||||
.each { |channel_id, user_id| users_removed_map[channel_id] << user_id }
|
||||
end
|
||||
|
||||
user_sql = params.user_id ? "AND u.id = #{params.user_id}" : ""
|
||||
channel_sql = params.channel_id ? "AND cc.id = #{params.channel_id}" : ""
|
||||
category_sql = params.category_id ? "AND c.id = #{params.category_id}" : ""
|
||||
|
||||
sql = <<~SQL
|
||||
WITH valid_permissions AS (
|
||||
SELECT gu.user_id, cg.category_id
|
||||
FROM group_users gu
|
||||
JOIN category_groups cg ON cg.group_id = gu.group_id AND cg.permission_type IN (:group_permissions)
|
||||
)
|
||||
DELETE FROM user_chat_channel_memberships
|
||||
WHERE (user_id, chat_channel_id) IN (
|
||||
SELECT uccm.user_id, uccm.chat_channel_id
|
||||
FROM user_chat_channel_memberships uccm
|
||||
JOIN users u ON u.id = uccm.user_id AND u.id > 0 AND u.moderator = FALSE AND u.admin = FALSE #{user_sql}
|
||||
JOIN chat_channels cc ON cc.id = uccm.chat_channel_id AND cc.chatable_type = 'Category' #{channel_sql}
|
||||
JOIN categories c ON c.id = cc.chatable_id AND c.read_restricted = TRUE #{category_sql}
|
||||
LEFT JOIN valid_permissions vp ON vp.user_id = uccm.user_id AND vp.category_id = c.id
|
||||
WHERE vp.user_id IS NULL
|
||||
)
|
||||
RETURNING chat_channel_id, user_id
|
||||
SQL
|
||||
|
||||
DB
|
||||
.query_array(sql, group_permissions:)
|
||||
.each { |channel_id, user_id| users_removed_map[channel_id] << user_id }
|
||||
|
||||
if users_removed_map.present?
|
||||
Chat::Action::PublishAutoRemovedUser.call(event: params.event, users_removed_map:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,81 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module AutoRemove
|
||||
# Fired from [Jobs::AutoRemoveMembershipHandleCategoryUpdated], which
|
||||
# in turn is enqueued whenever the [DiscourseEvent] for :category_updated
|
||||
# is triggered. Any users who can no longer access category-based channels
|
||||
# based on category_groups and in turn group_users will be removed from
|
||||
# those chat channels.
|
||||
#
|
||||
# If a user is in any groups that have the `full` or `create_post`
|
||||
# [CategoryGroup#permission_types] or if the category has no groups remaining,
|
||||
# then the user will remain in the channel.
|
||||
class HandleCategoryUpdated
|
||||
include Service::Base
|
||||
|
||||
policy :chat_enabled
|
||||
params do
|
||||
attribute :category_id, :integer
|
||||
|
||||
validates :category_id, presence: true
|
||||
end
|
||||
step :assign_defaults
|
||||
model :category
|
||||
model :category_channel_ids
|
||||
model :users
|
||||
step :remove_users_without_channel_permission
|
||||
step :publish
|
||||
|
||||
private
|
||||
|
||||
def assign_defaults
|
||||
context[:users_removed_map] = {}
|
||||
end
|
||||
|
||||
def chat_enabled
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def fetch_category(params:)
|
||||
Category.find_by(id: params.category_id)
|
||||
end
|
||||
|
||||
def fetch_category_channel_ids(category:)
|
||||
Chat::Channel.where(chatable: category).pluck(:id)
|
||||
end
|
||||
|
||||
def fetch_users(category_channel_ids:)
|
||||
User
|
||||
.real
|
||||
.activated
|
||||
.not_suspended
|
||||
.not_staged
|
||||
.joins(:user_chat_channel_memberships)
|
||||
.where("user_chat_channel_memberships.chat_channel_id IN (?)", category_channel_ids)
|
||||
.where("NOT admin AND NOT moderator")
|
||||
end
|
||||
|
||||
def remove_users_without_channel_permission(users:, category_channel_ids:)
|
||||
memberships_to_remove =
|
||||
Chat::Action::CalculateMembershipsForRemoval.call(
|
||||
scoped_users_query: users,
|
||||
channel_ids: category_channel_ids,
|
||||
)
|
||||
|
||||
return if memberships_to_remove.blank?
|
||||
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: Chat::UserChatChannelMembership.where(id: memberships_to_remove),
|
||||
)
|
||||
end
|
||||
|
||||
def publish(users_removed_map:)
|
||||
Chat::Action::PublishAutoRemovedUser.call(
|
||||
event_type: :category_updated,
|
||||
users_removed_map: users_removed_map,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,81 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module AutoRemove
|
||||
# Fired from [Jobs::AutoRemoveMembershipHandleChatAllowedGroupsChange], which
|
||||
# in turn is enqueued whenever the [DiscourseEvent] for :site_setting_changed
|
||||
# is triggered for the chat_allowed_groups setting.
|
||||
#
|
||||
# If any of the chat_allowed_groups is the everyone auto group then nothing
|
||||
# needs to be done.
|
||||
#
|
||||
# Otherwise, if there are no longer any chat_allowed_groups, we have to
|
||||
# remove all non-admin users from category channels. Otherwise we just
|
||||
# remove the ones who are not in any of the chat_allowed_groups.
|
||||
#
|
||||
# Direct message channel memberships are intentionally left alone,
|
||||
# these are private communications between two people.
|
||||
class HandleChatAllowedGroupsChange
|
||||
include Service::Base
|
||||
|
||||
policy :chat_enabled
|
||||
params { attribute :new_allowed_groups, :array }
|
||||
policy :not_everyone_allowed
|
||||
model :users
|
||||
model :memberships_to_remove
|
||||
step :remove_users_outside_allowed_groups
|
||||
step :publish
|
||||
|
||||
private
|
||||
|
||||
def chat_enabled
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def not_everyone_allowed(params:)
|
||||
params.new_allowed_groups.exclude?(Group::AUTO_GROUPS[:everyone])
|
||||
end
|
||||
|
||||
def fetch_users(params:)
|
||||
User
|
||||
.real
|
||||
.activated
|
||||
.not_suspended
|
||||
.not_staged
|
||||
.where("NOT admin AND NOT moderator")
|
||||
.joins(:user_chat_channel_memberships)
|
||||
.distinct
|
||||
.then do |users|
|
||||
break users if params.new_allowed_groups.blank?
|
||||
users.where(<<~SQL, params.new_allowed_groups)
|
||||
users.id NOT IN (
|
||||
SELECT DISTINCT group_users.user_id
|
||||
FROM group_users
|
||||
WHERE group_users.group_id IN (?)
|
||||
)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_memberships_to_remove(users:)
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:chat_channel)
|
||||
.where(user_id: users.pluck(:id))
|
||||
.where.not(chat_channel: { type: "DirectMessageChannel" })
|
||||
end
|
||||
|
||||
def remove_users_outside_allowed_groups(memberships_to_remove:)
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: memberships_to_remove,
|
||||
)
|
||||
end
|
||||
|
||||
def publish(users_removed_map:)
|
||||
Chat::Action::PublishAutoRemovedUser.call(
|
||||
event_type: :chat_allowed_groups_changed,
|
||||
users_removed_map: users_removed_map,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,116 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module AutoRemove
|
||||
# Fired from [Jobs::AutoRemoveMembershipHandleUserRemovedFromGroup], which
|
||||
# is in turn enqueued whenever the [DiscourseEvent] for :group_destroyed
|
||||
# is triggered.
|
||||
#
|
||||
# The :group_destroyed event provides us with the user_ids of the former
|
||||
# GroupUser records so we can scope this better.
|
||||
#
|
||||
# Since this could have potential wide-ranging impact, we have to check:
|
||||
# * The chat_allowed_groups [SiteSetting], and if any of the scoped users
|
||||
# are still allowed to use public chat channels based on this setting.
|
||||
# * The channel permissions of all the category chat channels the users
|
||||
# are a part of, based on [CategoryGroup] records
|
||||
#
|
||||
# If a user is in a groups that has the `full` or `create_post`
|
||||
# [CategoryGroup#permission_types] or if the category has no groups remaining,
|
||||
# then the user will remain in the channel.
|
||||
class HandleDestroyedGroup
|
||||
include Service::Base
|
||||
|
||||
policy :chat_enabled
|
||||
params do
|
||||
attribute :destroyed_group_user_ids, :array
|
||||
|
||||
validates :destroyed_group_user_ids, presence: true
|
||||
end
|
||||
step :assign_defaults
|
||||
policy :not_everyone_allowed
|
||||
model :scoped_users
|
||||
step :remove_users_outside_allowed_groups
|
||||
step :remove_users_without_channel_permission
|
||||
step :publish
|
||||
|
||||
private
|
||||
|
||||
def assign_defaults
|
||||
context[:users_removed_map] = {}
|
||||
end
|
||||
|
||||
def chat_enabled
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def not_everyone_allowed
|
||||
!SiteSetting.chat_allowed_groups_map.include?(Group::AUTO_GROUPS[:everyone])
|
||||
end
|
||||
|
||||
def fetch_scoped_users(params:)
|
||||
User
|
||||
.real
|
||||
.activated
|
||||
.not_suspended
|
||||
.not_staged
|
||||
.includes(:group_users)
|
||||
.where("NOT admin AND NOT moderator")
|
||||
.where(id: params.destroyed_group_user_ids)
|
||||
.joins(:user_chat_channel_memberships)
|
||||
.distinct
|
||||
end
|
||||
|
||||
def remove_users_outside_allowed_groups(scoped_users:)
|
||||
users = scoped_users
|
||||
|
||||
# Remove any of these users from all category channels if they
|
||||
# are not in any of the chat_allowed_groups or if there are no
|
||||
# chat allowed groups.
|
||||
if SiteSetting.chat_allowed_groups_map.any?
|
||||
group_user_sql = <<~SQL
|
||||
users.id NOT IN (
|
||||
SELECT DISTINCT group_users.user_id
|
||||
FROM group_users
|
||||
WHERE group_users.group_id IN (#{SiteSetting.chat_allowed_groups_map.join(",")})
|
||||
)
|
||||
SQL
|
||||
users = users.where(group_user_sql)
|
||||
end
|
||||
|
||||
user_ids_to_remove = users.pluck(:id)
|
||||
return if user_ids_to_remove.empty?
|
||||
|
||||
memberships_to_remove =
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:chat_channel)
|
||||
.where(user_id: user_ids_to_remove)
|
||||
.where.not(chat_channel: { type: "DirectMessageChannel" })
|
||||
|
||||
return if memberships_to_remove.empty?
|
||||
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: memberships_to_remove,
|
||||
)
|
||||
end
|
||||
|
||||
def remove_users_without_channel_permission(scoped_users:)
|
||||
memberships_to_remove =
|
||||
Chat::Action::CalculateMembershipsForRemoval.call(scoped_users_query: scoped_users)
|
||||
|
||||
return if memberships_to_remove.empty?
|
||||
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: Chat::UserChatChannelMembership.where(id: memberships_to_remove),
|
||||
)
|
||||
end
|
||||
|
||||
def publish(users_removed_map:)
|
||||
Chat::Action::PublishAutoRemovedUser.call(
|
||||
event_type: :destroyed_group,
|
||||
users_removed_map: users_removed_map,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,97 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
module AutoRemove
|
||||
# Fired from [Jobs::AutoRemoveMembershipHandleUserRemovedFromGroup], which
|
||||
# in turn is enqueued whenever the [DiscourseEvent] for :user_removed_from_group
|
||||
# is triggered.
|
||||
#
|
||||
# Staff users will never be affected by this, they can always chat regardless
|
||||
# of group permissions.
|
||||
#
|
||||
# Since this could have potential wide-ranging impact, we have to check:
|
||||
# * The chat_allowed_groups [SiteSetting], and if the scoped user
|
||||
# is still allowed to use public chat channels based on this setting.
|
||||
# * The channel permissions of all the category chat channels the user
|
||||
# is a part of, based on [CategoryGroup] records
|
||||
#
|
||||
# Direct message channel memberships are intentionally left alone,
|
||||
# these are private communications between two people.
|
||||
class HandleUserRemovedFromGroup
|
||||
include Service::Base
|
||||
|
||||
policy :chat_enabled
|
||||
params do
|
||||
attribute :user_id, :integer
|
||||
|
||||
validates :user_id, presence: true
|
||||
end
|
||||
step :assign_defaults
|
||||
policy :not_everyone_allowed
|
||||
model :user
|
||||
policy :user_not_staff
|
||||
step :remove_if_outside_chat_allowed_groups
|
||||
step :remove_from_private_channels
|
||||
step :publish
|
||||
|
||||
private
|
||||
|
||||
def assign_defaults
|
||||
context[:users_removed_map] = {}
|
||||
end
|
||||
|
||||
def chat_enabled
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def not_everyone_allowed
|
||||
!SiteSetting.chat_allowed_groups_map.include?(Group::AUTO_GROUPS[:everyone])
|
||||
end
|
||||
|
||||
def fetch_user(params:)
|
||||
User.find_by(id: params.user_id)
|
||||
end
|
||||
|
||||
def user_not_staff(user:)
|
||||
!user.staff?
|
||||
end
|
||||
|
||||
def remove_if_outside_chat_allowed_groups(user:)
|
||||
if SiteSetting.chat_allowed_groups_map.empty? ||
|
||||
!GroupUser.exists?(group_id: SiteSetting.chat_allowed_groups_map, user: user)
|
||||
memberships_to_remove =
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:chat_channel)
|
||||
.where(user_id: user.id)
|
||||
.where.not(chat_channel: { type: "DirectMessageChannel" })
|
||||
|
||||
return if memberships_to_remove.empty?
|
||||
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: memberships_to_remove,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_private_channels(user:)
|
||||
memberships_to_remove =
|
||||
Chat::Action::CalculateMembershipsForRemoval.call(
|
||||
scoped_users_query: User.where(id: user.id),
|
||||
)
|
||||
|
||||
return if memberships_to_remove.empty?
|
||||
|
||||
context[:users_removed_map] = Chat::Action::RemoveMemberships.call(
|
||||
memberships: Chat::UserChatChannelMembership.where(id: memberships_to_remove),
|
||||
)
|
||||
end
|
||||
|
||||
def publish(users_removed_map:)
|
||||
Chat::Action::PublishAutoRemovedUser.call(
|
||||
event_type: :user_removed_from_group,
|
||||
users_removed_map: users_removed_map,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -54,7 +54,7 @@ module Chat
|
||||
model :channel, :create_channel
|
||||
model :membership, :create_membership
|
||||
end
|
||||
step :enforce_automatic_channel_memberships
|
||||
step :auto_join_users_if_needed
|
||||
|
||||
private
|
||||
|
||||
@ -92,9 +92,8 @@ module Chat
|
||||
channel.user_chat_channel_memberships.create(user: guardian.user, following: true)
|
||||
end
|
||||
|
||||
def enforce_automatic_channel_memberships(channel:)
|
||||
return if !channel.auto_join_users?
|
||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||
def auto_join_users_if_needed(channel:)
|
||||
Chat::AutoJoinChannels.call(params: { channel_id: channel.id }) if channel.auto_join_users?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -361,10 +361,10 @@ module Chat
|
||||
|
||||
NEW_CHANNEL_MESSAGE_BUS_CHANNEL = "/chat/new-channel"
|
||||
|
||||
def self.publish_new_channel(chat_channel, users)
|
||||
def self.publish_new_channel(chat_channel, user_ids)
|
||||
Chat::UserChatChannelMembership
|
||||
.includes(:user)
|
||||
.where(chat_channel: chat_channel, user: users)
|
||||
.where(chat_channel: chat_channel, user_id: user_ids)
|
||||
.find_in_batches do |memberships|
|
||||
memberships.each do |membership|
|
||||
serialized_channel =
|
||||
|
@ -86,8 +86,7 @@ module Chat
|
||||
end
|
||||
|
||||
def auto_join_users_if_needed(channel:)
|
||||
return unless channel.auto_join_users?
|
||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||
Chat::AutoJoinChannels.call(params: { channel_id: channel.id }) if channel.auto_join_users?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -64,18 +64,5 @@ module Chat
|
||||
last_read_message_id: channel.chat_messages.last&.id,
|
||||
)
|
||||
end
|
||||
|
||||
def enforce_automatic_channel_memberships
|
||||
Jobs.enqueue(Jobs::Chat::AutoJoinChannelMemberships, chat_channel_id: channel.id)
|
||||
end
|
||||
|
||||
def enforce_automatic_user_membership(user)
|
||||
Jobs.enqueue(
|
||||
Jobs::Chat::AutoJoinChannelBatch,
|
||||
chat_channel_id: channel.id,
|
||||
starts_at: user.id,
|
||||
ends_at: user.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,12 +19,9 @@ module Chat
|
||||
category = Category.find_by(id: category_id)
|
||||
return if category.nil?
|
||||
|
||||
chat_channel = category.create_chat_channel!(auto_join_users: true, name: category.name)
|
||||
category.create_chat_channel!(auto_join_users: true, name: category.name)
|
||||
category.custom_fields[Chat::HAS_CHAT_ENABLED] = true
|
||||
category.save!
|
||||
|
||||
Chat::ChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
|
||||
chat_channel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -289,10 +289,7 @@ after_initialize do
|
||||
end
|
||||
|
||||
if name == :chat_allowed_groups
|
||||
Jobs.enqueue(
|
||||
Jobs::Chat::AutoRemoveMembershipHandleChatAllowedGroupsChange,
|
||||
new_allowed_groups: new_value,
|
||||
)
|
||||
Jobs.enqueue(Jobs::Chat::AutoJoinUsers, event: "chat_allowed_groups_changed")
|
||||
end
|
||||
end
|
||||
|
||||
@ -301,11 +298,8 @@ after_initialize do
|
||||
Chat::PostNotificationHandler.new(post, notified).handle
|
||||
end
|
||||
|
||||
on(:group_destroyed) do |group, user_ids|
|
||||
Jobs.enqueue(
|
||||
Jobs::Chat::AutoRemoveMembershipHandleDestroyedGroup,
|
||||
destroyed_group_user_ids: user_ids,
|
||||
)
|
||||
on(:group_destroyed) do |group, _user_ids|
|
||||
Chat::AutoLeaveChannels.call(params: { group_id: group.id, event: :group_destroyed })
|
||||
end
|
||||
|
||||
register_presence_channel_prefix("chat") do |channel_name|
|
||||
@ -358,52 +352,31 @@ after_initialize do
|
||||
|
||||
on(:user_seen) do |user|
|
||||
if user.last_seen_at == user.first_seen_at
|
||||
Chat::Channel
|
||||
.where(auto_join_users: true)
|
||||
.each do |channel|
|
||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_user_membership(user)
|
||||
end
|
||||
Chat::AutoJoinChannels.call(params: { user_id: user.id })
|
||||
end
|
||||
end
|
||||
|
||||
on(:user_confirmed_email) do |user|
|
||||
if user.active?
|
||||
Chat::Channel
|
||||
.where(auto_join_users: true)
|
||||
.each do |channel|
|
||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_user_membership(user)
|
||||
end
|
||||
end
|
||||
Chat::AutoJoinChannels.call(params: { user_id: user.id }) if user.active?
|
||||
end
|
||||
|
||||
on(:user_added_to_group) do |user, group|
|
||||
channels_to_add =
|
||||
Chat::Channel
|
||||
.distinct
|
||||
.where(auto_join_users: true, chatable_type: "Category")
|
||||
.joins(
|
||||
"INNER JOIN category_groups ON category_groups.category_id = chat_channels.chatable_id",
|
||||
)
|
||||
.where(category_groups: { group_id: group.id })
|
||||
|
||||
channels_to_add.each do |channel|
|
||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_user_membership(user)
|
||||
end
|
||||
on(:user_added_to_group) do |user, _group|
|
||||
Chat::AutoJoinChannels.call(params: { user_id: user.id })
|
||||
end
|
||||
|
||||
on(:user_removed_from_group) do |user, group|
|
||||
Jobs.enqueue(Jobs::Chat::AutoRemoveMembershipHandleUserRemovedFromGroup, user_id: user.id)
|
||||
on(:user_removed_from_group) do |user, _group|
|
||||
Chat::AutoLeaveChannels.call(params: { user_id: user.id, event: :user_removed_from_group })
|
||||
end
|
||||
|
||||
on(:category_updated) do |category|
|
||||
# There's a bug on core where this event is triggered with an `#update` result (true/false)
|
||||
if category.is_a?(Category) && category_channel = Chat::Channel.find_by(chatable: category)
|
||||
if category_channel.auto_join_users
|
||||
Chat::ChannelMembershipManager.new(category_channel).enforce_automatic_channel_memberships
|
||||
end
|
||||
next unless category.is_a?(Category)
|
||||
next unless category_channel = Chat::Channel.find_by(chatable: category)
|
||||
|
||||
Jobs.enqueue(Jobs::Chat::AutoRemoveMembershipHandleCategoryUpdated, category_id: category.id)
|
||||
if category_channel.auto_join_users
|
||||
Chat::AutoJoinChannels.call(params: { category_id: category.id })
|
||||
end
|
||||
Chat::AutoLeaveChannels.call(params: { category_id: category.id, event: :category_updated })
|
||||
end
|
||||
|
||||
# outgoing webhook events
|
||||
|
@ -4,24 +4,18 @@ describe Chat::Seeder do
|
||||
fab!(:staff_category) { Fabricate(:private_category, name: "Staff", group: Group[:staff]) }
|
||||
fab!(:general_category) { Fabricate(:category, name: "General") }
|
||||
|
||||
fab!(:staff_user1) do
|
||||
Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:staff], Group[:everyone]])
|
||||
end
|
||||
fab!(:staff_user2) do
|
||||
Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:staff], Group[:everyone]])
|
||||
end
|
||||
|
||||
fab!(:regular_user) { Fabricate(:user, last_seen_at: 1.minute.ago, groups: [Group[:everyone]]) }
|
||||
|
||||
before do
|
||||
SiteSetting.staff_category_id = staff_category.id
|
||||
SiteSetting.general_category_id = general_category.id
|
||||
Jobs.run_immediately!
|
||||
end
|
||||
|
||||
def assert_channel_was_correctly_seeded(channel, group)
|
||||
def assert_channel_was_correctly_seeded(channel, group, category)
|
||||
expect(channel).to be_present
|
||||
expect(channel.auto_join_users).to eq(true)
|
||||
expect(channel.name).to eq(category.name)
|
||||
|
||||
expect(category.custom_fields[Chat::HAS_CHAT_ENABLED]).to eq(true)
|
||||
|
||||
expected_members_count = GroupUser.where(group: group).count
|
||||
memberships_count =
|
||||
@ -31,37 +25,24 @@ describe Chat::Seeder do
|
||||
end
|
||||
|
||||
it "seeds default channels" do
|
||||
last_seen_at = 1.minute.ago
|
||||
|
||||
# By default, `chat_allowed_groups` is set to admins, moderators, and TL1
|
||||
Fabricate(:user, last_seen_at:, groups: [Group[:everyone], Group[:admins]])
|
||||
Fabricate(:user, last_seen_at:, groups: [Group[:everyone], Group[:moderators]])
|
||||
Fabricate(:user, last_seen_at:, groups: [Group[:everyone], Group[:trust_level_1]])
|
||||
|
||||
Chat::Seeder.new.execute
|
||||
|
||||
staff_channel = Chat::Channel.find_by(chatable_id: staff_category)
|
||||
general_channel = Chat::Channel.find_by(chatable_id: general_category)
|
||||
|
||||
assert_channel_was_correctly_seeded(staff_channel, Group[:staff])
|
||||
assert_channel_was_correctly_seeded(general_channel, Group[:everyone])
|
||||
assert_channel_was_correctly_seeded(staff_channel, Group[:staff], staff_category)
|
||||
assert_channel_was_correctly_seeded(general_channel, Group[:everyone], general_category)
|
||||
|
||||
expect(staff_category.custom_fields[Chat::HAS_CHAT_ENABLED]).to eq(true)
|
||||
expect(general_category.reload.custom_fields[Chat::HAS_CHAT_ENABLED]).to eq(true)
|
||||
expect(SiteSetting.needs_chat_seeded).to eq(false)
|
||||
end
|
||||
|
||||
it "applies a name to the general category channel" do
|
||||
expected_name = general_category.name
|
||||
|
||||
Chat::Seeder.new.execute
|
||||
|
||||
general_channel = Chat::Channel.find_by(chatable_id: general_category)
|
||||
expect(general_channel.name).to eq(expected_name)
|
||||
end
|
||||
|
||||
it "applies a name to the staff category channel" do
|
||||
expected_name = staff_category.name
|
||||
|
||||
Chat::Seeder.new.execute
|
||||
|
||||
staff_channel = Chat::Channel.find_by(chatable_id: staff_category)
|
||||
expect(staff_channel.name).to eq(expected_name)
|
||||
end
|
||||
|
||||
it "does nothing when 'SiteSetting.needs_chat_seeded' is false" do
|
||||
SiteSetting.needs_chat_seeded = false
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Automatic user removal from channels" do
|
||||
fab!(:user_1) { Fabricate(:user, trust_level: TrustLevel[1]) }
|
||||
fab!(:user_1) { Fabricate(:user, trust_level: 1) }
|
||||
let(:user_1_guardian) { Guardian.new(user_1) }
|
||||
fab!(:user_2) { Fabricate(:user, trust_level: TrustLevel[1]) }
|
||||
fab!(:user_2) { Fabricate(:user, trust_level: 3) }
|
||||
|
||||
fab!(:secret_group) { Fabricate(:group) }
|
||||
fab!(:private_category) { Fabricate(:private_category, group: secret_group) }
|
||||
@ -13,7 +13,6 @@ describe "Automatic user removal from channels" do
|
||||
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||
|
||||
before do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_1]
|
||||
SiteSetting.chat_enabled = true
|
||||
Jobs.run_immediately!
|
||||
|
||||
@ -47,35 +46,45 @@ describe "Automatic user removal from channels" do
|
||||
end
|
||||
|
||||
it "does not remove the user who is in one of the chat_allowed_groups" do
|
||||
user_2.change_trust_level!(TrustLevel[4])
|
||||
user_2.change_trust_level!(4)
|
||||
|
||||
expect { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_3] }.to change {
|
||||
Chat::UserChatChannelMembership.count
|
||||
}.by(-2)
|
||||
}.by(-3)
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_2, chat_channel: public_channel),
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "does not remove users from their DM channels" do
|
||||
it "removes users from their DM channels" do
|
||||
expect { SiteSetting.chat_allowed_groups = "" }.to change {
|
||||
Chat::UserChatChannelMembership.count
|
||||
}.by(-3)
|
||||
}.by(-5)
|
||||
|
||||
expect(Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: dm_channel)).to eq(
|
||||
true,
|
||||
false,
|
||||
)
|
||||
expect(Chat::UserChatChannelMembership.exists?(user: user_2, chat_channel: dm_channel)).to eq(
|
||||
true,
|
||||
false,
|
||||
)
|
||||
end
|
||||
|
||||
context "for staff users" do
|
||||
fab!(:staff_user) { Fabricate(:admin) }
|
||||
|
||||
it "does not remove them from public channels" do
|
||||
it "does not remove them from chat channels" do
|
||||
public_channel.add(staff_user)
|
||||
private_channel.add(staff_user)
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: staff_user,
|
||||
chat_channel: [public_channel, private_channel],
|
||||
).count,
|
||||
).to eq(2)
|
||||
|
||||
SiteSetting.chat_allowed_groups = ""
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: staff_user,
|
||||
@ -86,6 +95,7 @@ describe "Automatic user removal from channels" do
|
||||
|
||||
it "does not remove them from DM channels" do
|
||||
staff_dm_channel = Fabricate(:direct_message_channel, users: [user_1, staff_user])
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: staff_user,
|
||||
@ -105,21 +115,12 @@ describe "Automatic user removal from channels" do
|
||||
SiteSetting.chat_allowed_groups = group.id
|
||||
end
|
||||
|
||||
it "removes the user from the category channels" do
|
||||
group.remove(user_1)
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: user_1,
|
||||
chat_channel: [public_channel, private_channel],
|
||||
).count,
|
||||
).to eq(0)
|
||||
end
|
||||
it "removes the user from all channels" do
|
||||
expect(Chat::UserChatChannelMembership.where(user: user_1).count).to eq(3)
|
||||
|
||||
it "does not remove the user from DM channels" do
|
||||
group.remove(user_1)
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(user: user_1, chat_channel: dm_channel).count,
|
||||
).to eq(1)
|
||||
|
||||
expect(Chat::UserChatChannelMembership.where(user: user_1).count).to eq(0)
|
||||
end
|
||||
|
||||
context "for staff users" do
|
||||
@ -155,6 +156,7 @@ describe "Automatic user removal from channels" do
|
||||
|
||||
it "does not remove them from the corresponding channel" do
|
||||
secret_group.remove(user_1)
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: private_channel),
|
||||
).to eq(true)
|
||||
@ -167,6 +169,7 @@ describe "Automatic user removal from channels" do
|
||||
context "when the user is in no other groups that can interact with the channel" do
|
||||
it "removes them from the corresponding channel" do
|
||||
secret_group.remove(user_1)
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: private_channel),
|
||||
).to eq(false)
|
||||
@ -182,6 +185,7 @@ describe "Automatic user removal from channels" do
|
||||
context "when the group's permission changes from reply+see to just see for the category" do
|
||||
it "removes the user from the corresponding category channel" do
|
||||
private_category.update!(permissions: { secret_group.id => :readonly })
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: private_channel),
|
||||
).to eq(false)
|
||||
@ -197,6 +201,7 @@ describe "Automatic user removal from channels" do
|
||||
secret_group.add(staff_user)
|
||||
private_channel.add(staff_user)
|
||||
private_category.update!(permissions: { secret_group.id => :readonly })
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(
|
||||
user: staff_user,
|
||||
@ -210,6 +215,7 @@ describe "Automatic user removal from channels" do
|
||||
context "when the secret_group is no longer allowed to access the private category" do
|
||||
it "removes the user from the corresponding category channel" do
|
||||
private_category.update!(permissions: { Group::AUTO_GROUPS[:staff] => :full })
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: private_channel),
|
||||
).to eq(false)
|
||||
@ -225,6 +231,7 @@ describe "Automatic user removal from channels" do
|
||||
secret_group.add(staff_user)
|
||||
private_channel.add(staff_user)
|
||||
private_category.update!(permissions: {})
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(
|
||||
user: staff_user,
|
||||
@ -238,13 +245,13 @@ describe "Automatic user removal from channels" do
|
||||
|
||||
context "when a group is destroyed" do
|
||||
context "when it was the last group on the private category" do
|
||||
it "no users are removed because the category defaults to Everyone having full access" do
|
||||
it "remove users because the category defaults to staff having full access" do
|
||||
secret_group.destroy!
|
||||
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.exists?(user: user_1, chat_channel: private_channel),
|
||||
).to eq(true)
|
||||
expect(Chat::ChannelFetcher.all_secured_channel_ids(user_1_guardian)).to include(
|
||||
).to eq(false)
|
||||
expect(Chat::ChannelFetcher.all_secured_channel_ids(user_1_guardian)).to_not include(
|
||||
private_channel.id,
|
||||
)
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Jobs::Chat::AutoJoinChannelBatch do
|
||||
it "can successfully queue this job" do
|
||||
expect {
|
||||
Jobs.enqueue(
|
||||
described_class,
|
||||
channel_id: Fabricate(:chat_channel).id,
|
||||
start_user_id: 0,
|
||||
end_user_id: 10,
|
||||
)
|
||||
}.to change(Jobs::Chat::AutoJoinChannelBatch.jobs, :size).by(1)
|
||||
end
|
||||
|
||||
context "when contract fails" do
|
||||
before { Jobs.run_immediately! }
|
||||
|
||||
it "logs an error" do
|
||||
Rails.logger.expects(:error).with(regexp_matches(/Channel can't be blank/)).at_least_once
|
||||
|
||||
Jobs.enqueue(described_class)
|
||||
end
|
||||
end
|
||||
|
||||
context "when model is not found" do
|
||||
before { Jobs.run_immediately! }
|
||||
|
||||
it "logs an error" do
|
||||
Rails.logger.expects(:error).with("Channel not found (id=-999)").at_least_once
|
||||
|
||||
Jobs.enqueue(described_class, channel_id: -999, start_user_id: 1, end_user_id: 2)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,125 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Jobs::Chat::AutoJoinChannelMemberships do
|
||||
let(:user) { Fabricate(:user, last_seen_at: 15.minutes.ago) }
|
||||
let(:category) { Fabricate(:category, user: user) }
|
||||
let(:channel) { Fabricate(:category_channel, auto_join_users: true, chatable: category) }
|
||||
|
||||
describe "queues batches to automatically add users to a channel" do
|
||||
it "queues a batch for users with channel access" do
|
||||
assert_batches_enqueued(channel, 1)
|
||||
end
|
||||
|
||||
it "does nothing when the channel doesn't exist" do
|
||||
assert_batches_enqueued(Chat::Channel.new(id: -1), 0)
|
||||
end
|
||||
|
||||
it "does nothing when the chatable is not a category" do
|
||||
direct_message = Fabricate(:direct_message)
|
||||
channel.update!(chatable: direct_message)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "excludes users not seen in the last 3 months" do
|
||||
user.update!(last_seen_at: 3.months.ago)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "excludes users without chat enabled" do
|
||||
user.user_option.update!(chat_enabled: false)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "respects the max_chat_auto_joined_users setting" do
|
||||
SiteSetting.max_chat_auto_joined_users = 0
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "does nothing when we already reached the max_chat_auto_joined_users limit" do
|
||||
SiteSetting.max_chat_auto_joined_users = 1
|
||||
user_2 = Fabricate(:user, last_seen_at: 2.minutes.ago)
|
||||
Chat::UserChatChannelMembership.create!(
|
||||
user: user_2,
|
||||
chat_channel: channel,
|
||||
following: true,
|
||||
join_mode: Chat::UserChatChannelMembership.join_modes[:automatic],
|
||||
)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "ignores users that are already channel members" do
|
||||
Chat::UserChatChannelMembership.create!(user: user, chat_channel: channel, following: true)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "doesn't queue a batch when the user doesn't follow the channel" do
|
||||
Chat::UserChatChannelMembership.create!(user: user, chat_channel: channel, following: false)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "skips non-active users" do
|
||||
user.update!(active: false)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "skips suspended users" do
|
||||
user.update!(suspended_till: 3.years.from_now)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
it "skips staged users" do
|
||||
user.update!(staged: true)
|
||||
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
context "when the category has read restricted access" do
|
||||
fab!(:chatters_group) { Fabricate(:group) }
|
||||
let(:private_category) { Fabricate(:private_category, group: chatters_group) }
|
||||
let(:channel) { Fabricate(:chat_channel, auto_join_users: true, chatable: private_category) }
|
||||
|
||||
it "doesn't queue a batch if the user is not a group member" do
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
|
||||
context "when the user has category access to a group" do
|
||||
before { chatters_group.add(user) }
|
||||
|
||||
it "queues a batch" do
|
||||
assert_batches_enqueued(channel, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when chatable doesn’t exist anymore" do
|
||||
let(:channel) do
|
||||
Fabricate(
|
||||
:category_channel,
|
||||
auto_join_users: true,
|
||||
chatable_type: "Category",
|
||||
chatable_id: -1,
|
||||
)
|
||||
end
|
||||
|
||||
it "does nothing" do
|
||||
assert_batches_enqueued(channel, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def assert_batches_enqueued(channel, expected)
|
||||
expect { subject.execute(chat_channel_id: channel.id) }.to change(
|
||||
Jobs::Chat::AutoJoinChannelBatch.jobs,
|
||||
:size,
|
||||
).by(expected)
|
||||
end
|
||||
end
|
@ -4,21 +4,28 @@ describe Jobs::Chat::AutoJoinUsers do
|
||||
subject(:job) { described_class.new }
|
||||
|
||||
fab!(:channel) { Fabricate(:category_channel, auto_join_users: true) }
|
||||
fab!(:user) { Fabricate(:user, last_seen_at: 1.minute.ago, active: true) }
|
||||
fab!(:user) { Fabricate(:user, last_seen_at: 1.minute.ago, trust_level: 1) }
|
||||
fab!(:group)
|
||||
fab!(:user_without_chat) do
|
||||
user = Fabricate(:user)
|
||||
user.user_option.update!(chat_enabled: false)
|
||||
user
|
||||
end
|
||||
fab!(:stage_user) { Fabricate(:user, staged: true) }
|
||||
fab!(:staged_user) { Fabricate(:user, staged: true) }
|
||||
fab!(:suspended_user) { Fabricate(:user, suspended_till: 1.day.from_now) }
|
||||
fab!(:silenced_user) { Fabricate(:user, silenced_till: 2.day.from_now) }
|
||||
fab!(:inactive_user) { Fabricate(:user, active: false) }
|
||||
fab!(:anonymous_user) { Fabricate(:anonymous) }
|
||||
fab!(:anonymous_user) do
|
||||
# When using the `anonymous` fabricator, the `::Chat::AutoJoinChannels` is called in the
|
||||
# `on(:user_added_to_group)` hook **before** the `anonymous_user` record is created in the `after_create` hook
|
||||
anon = Fabricate(:user)
|
||||
AnonymousUser.create!(user: anon, master_user: anon, active: true)
|
||||
anon
|
||||
end
|
||||
|
||||
before { Jobs.run_immediately! }
|
||||
|
||||
it "is does not auto join users without permissions" do
|
||||
it "does not auto join users without permissions" do
|
||||
channel.category.read_restricted = true
|
||||
channel.category.set_permissions(group => :full)
|
||||
channel.category.save!
|
||||
@ -38,20 +45,14 @@ describe Jobs::Chat::AutoJoinUsers do
|
||||
|
||||
it "works for simple workflows" do
|
||||
# this is just to avoid test fragility, we should always have negative users
|
||||
bot_id = (User.minimum(:id) - 1)
|
||||
bot_id = -1 if bot_id > 0
|
||||
_bot_user = Fabricate(:user, id: bot_id)
|
||||
|
||||
membership = Chat::UserChatChannelMembership.find_by(user: user, chat_channel: channel)
|
||||
expect(membership).to be_nil
|
||||
_bot = Fabricate(:user, id: [User.minimum(:id), 0].min - 1)
|
||||
|
||||
job.execute({})
|
||||
|
||||
# should exclude bot / inactive / staged / suspended users
|
||||
# note category fabricator creates a user so we are stuck with that user in the channel
|
||||
# excludes bot / chat disabled / inactive / staged / suspended / silenced / anonymous users
|
||||
expect(
|
||||
Chat::UserChatChannelMembership.where(chat_channel: channel).pluck(:user_id),
|
||||
).to contain_exactly(user.id, channel.category.user.id)
|
||||
).to contain_exactly(user.id)
|
||||
|
||||
membership = Chat::UserChatChannelMembership.find_by(user: user, chat_channel: channel)
|
||||
expect(membership.following).to eq(true)
|
||||
|
@ -55,7 +55,7 @@ RSpec.describe Chat::CategoryChannel do
|
||||
end
|
||||
|
||||
context "when channel is not public" do
|
||||
let(:staff_groups) { Group::AUTO_GROUPS.slice(:staff, :moderators, :admins).values }
|
||||
let(:staff_groups) { Group::AUTO_GROUPS.values_at(:staff, :moderators, :admins) }
|
||||
let(:group) { Fabricate(:group) }
|
||||
let(:private_category) { Fabricate(:private_category, group: group) }
|
||||
let(:channel) { Fabricate(:category_channel, chatable: private_category) }
|
||||
|
@ -183,7 +183,7 @@ describe Chat do
|
||||
|
||||
describe "auto-joining users to a channel" do
|
||||
fab!(:chatters_group) { Fabricate(:group) }
|
||||
fab!(:user) { Fabricate(:user, last_seen_at: 15.minutes.ago) }
|
||||
fab!(:user) { Fabricate(:user, last_seen_at: 15.minutes.ago, trust_level: 1) }
|
||||
let!(:channel) { Fabricate(:category_channel, auto_join_users: true, chatable: category) }
|
||||
|
||||
before { Jobs.run_immediately! }
|
||||
@ -212,7 +212,7 @@ describe Chat do
|
||||
|
||||
describe "when a user is created" do
|
||||
fab!(:category)
|
||||
let(:user) { Fabricate(:user, last_seen_at: nil, first_seen_at: nil) }
|
||||
let(:user) { Fabricate(:user, last_seen_at: nil, first_seen_at: nil, trust_level: 1) }
|
||||
|
||||
it "queues a job to auto-join the user the first time they log in" do
|
||||
user.update_last_seen!
|
||||
@ -220,13 +220,6 @@ describe Chat do
|
||||
assert_user_following_state(user, channel, following: true)
|
||||
end
|
||||
|
||||
it "does nothing if it's not the first time we see the user" do
|
||||
user.update!(first_seen_at: 2.minute.ago)
|
||||
user.update_last_seen!
|
||||
|
||||
assert_user_following_state(user, channel, following: false)
|
||||
end
|
||||
|
||||
it "does nothing if auto-join is disabled" do
|
||||
channel.update!(auto_join_users: false)
|
||||
|
||||
|
@ -78,9 +78,11 @@ describe Chat::ChannelMembershipsQuery do
|
||||
end
|
||||
|
||||
it "returns the membership if the user still has access through a staff group" do
|
||||
chatters_group.remove(user_1)
|
||||
user_1.update!(admin: true)
|
||||
Group.find_by(id: Group::AUTO_GROUPS[:staff]).add(user_1)
|
||||
|
||||
chatters_group.remove(user_1)
|
||||
|
||||
memberships = described_class.call(channel: channel_1)
|
||||
|
||||
expect(memberships.pluck(:user_id)).to include(user_1.id)
|
||||
@ -291,6 +293,24 @@ describe Chat::ChannelMembershipsQuery do
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is silenced" do
|
||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||
fab!(:silenced_user) { Fabricate(:user, silenced_till: 5.days.from_now) }
|
||||
|
||||
before do
|
||||
Chat::UserChatChannelMembership.create(
|
||||
user: silenced_user,
|
||||
chat_channel: channel_1,
|
||||
following: true,
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn’t list silenced users" do
|
||||
memberships = described_class.call(channel: channel_1)
|
||||
expect(memberships).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is inactive" do
|
||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||
fab!(:inactive_user)
|
||||
|
@ -12,7 +12,7 @@ describe UsersController do
|
||||
end
|
||||
|
||||
it "triggers the auto-join process" do
|
||||
user = Fabricate(:user, last_seen_at: 1.minute.ago, active: false)
|
||||
user = Fabricate(:user, last_seen_at: 1.minute.ago, active: false, trust_level: 1)
|
||||
email_token = Fabricate(:email_token, user: user)
|
||||
|
||||
put "/u/activate-account/#{email_token.token}"
|
||||
|
@ -1,172 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::Action::CreateMembershipsForAutoJoin do
|
||||
subject(:action) { described_class.call(channel:, params:) }
|
||||
|
||||
fab!(:channel) { Fabricate(:chat_channel, auto_join_users: true) }
|
||||
fab!(:user_1) { Fabricate(:user, last_seen_at: 15.minutes.ago) }
|
||||
|
||||
let(:start_user_id) { user_1.id }
|
||||
let(:end_user_id) { user_1.id }
|
||||
let(:params) { OpenStruct.new(start_user_id: start_user_id, end_user_id: end_user_id) }
|
||||
|
||||
it "adds correct members" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
|
||||
it "sets the reason to automatic" do
|
||||
action
|
||||
expect(channel.membership_for(user_1)).to be_automatic
|
||||
end
|
||||
|
||||
context "with others users not in the batch" do
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
|
||||
it "adds correct members" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
end
|
||||
|
||||
context "with suspended users" do
|
||||
before { user_1.update!(suspended_till: 1.year.from_now) }
|
||||
|
||||
it "skips suspended users" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "with users not seen recently" do
|
||||
before { user_1.update!(last_seen_at: 4.months.ago) }
|
||||
|
||||
it "skips users last_seen more than 3 months ago" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "with never seen users" do
|
||||
before { user_1.update!(last_seen_at: nil) }
|
||||
|
||||
it "includes users with last_seen set to null" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
end
|
||||
|
||||
context "with disabled chat users" do
|
||||
before { user_1.user_option.update!(chat_enabled: false) }
|
||||
|
||||
it "skips users without chat_enabled" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "with anonymous users" do
|
||||
fab!(:user_1) { Fabricate(:anonymous, last_seen_at: 15.minutes.ago) }
|
||||
|
||||
it "skips anonymous users" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "with inactive users" do
|
||||
before { user_1.update!(active: false) }
|
||||
|
||||
it "skips inactive users" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "with staged users" do
|
||||
before { user_1.update!(staged: true) }
|
||||
|
||||
it "skips staged users" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is already a member" do
|
||||
before { channel.add(user_1) }
|
||||
|
||||
it "is a noop" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when category is restricted" do
|
||||
fab!(:user_1) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
fab!(:group_1) { Fabricate(:group) }
|
||||
fab!(:channel) { Fabricate(:private_category_channel, group: group_1, auto_join_users: true) }
|
||||
|
||||
let(:end_user_id) { user_2.id }
|
||||
|
||||
before { group_1.add(user_1) }
|
||||
|
||||
it "only joins users with access to the category through the group" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
|
||||
context "when the user has access through multiple groups" do
|
||||
fab!(:group_2) { Fabricate(:group) }
|
||||
|
||||
before do
|
||||
channel.category.category_groups.create!(
|
||||
group_id: group_2.id,
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
group_2.add(user_1)
|
||||
end
|
||||
|
||||
it "correctly joins the user" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
end
|
||||
|
||||
context "when the category group is read only" do
|
||||
fab!(:channel) { Fabricate(:private_category_channel, auto_join_users: true) }
|
||||
|
||||
before do
|
||||
channel.category.category_groups.create!(
|
||||
group_id: group_1.id,
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
group_1.add(user_1)
|
||||
end
|
||||
|
||||
it "doesn’t join the users of the group" do
|
||||
expect(action).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when the category group has create post permission" do
|
||||
fab!(:channel) { Fabricate(:private_category_channel, auto_join_users: true) }
|
||||
|
||||
before do
|
||||
channel.category.category_groups.create!(
|
||||
group_id: group_1.id,
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
)
|
||||
group_1.add(user_1)
|
||||
end
|
||||
|
||||
it "correctly joins the user" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
end
|
||||
|
||||
context "when user has allowed groups and disallowed groups" do
|
||||
fab!(:group_2) { Fabricate(:group) }
|
||||
|
||||
before do
|
||||
channel.category.category_groups.create!(
|
||||
group_id: group_2.id,
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
group_2.add(user_1)
|
||||
end
|
||||
|
||||
it "correctly joins the user" do
|
||||
expect(action).to eq([user_1.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -41,12 +41,11 @@ RSpec.describe Chat::Action::PublishAndFollowDirectMessageChannel do
|
||||
end
|
||||
|
||||
context "when at least one user allows communication" do
|
||||
let(:users) { channel.user_chat_channel_memberships.map(&:user) }
|
||||
|
||||
before { channel.user_chat_channel_memberships.update_all(following: false) }
|
||||
|
||||
it "publishes the channel" do
|
||||
Chat::Publisher.expects(:publish_new_channel).with(channel, includes(*users))
|
||||
user_ids = channel.user_chat_channel_memberships.map(&:user_id)
|
||||
Chat::Publisher.expects(:publish_new_channel).with(channel, includes(*user_ids))
|
||||
action
|
||||
end
|
||||
|
||||
|
@ -1,162 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoRemove::HandleCategoryUpdated do
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params:) }
|
||||
|
||||
let(:params) { { category_id: updated_category.id } }
|
||||
|
||||
fab!(:updated_category) { Fabricate(:category) }
|
||||
fab!(:user_1) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
fab!(:admin_1) { Fabricate(:admin) }
|
||||
fab!(:admin_2) { Fabricate(:admin) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel, chatable: updated_category) }
|
||||
fab!(:channel_2) { Fabricate(:chat_channel, chatable: updated_category) }
|
||||
|
||||
context "when chat is not enabled" do
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "if the category is deleted" do
|
||||
before { updated_category.destroy! }
|
||||
|
||||
it "fails to find category model" do
|
||||
expect(result).to fail_to_find_a_model(:category)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no channels associated with the category" do
|
||||
before do
|
||||
channel_1.destroy!
|
||||
channel_2.destroy!
|
||||
end
|
||||
|
||||
it "fails to find category_channel_ids model" do
|
||||
expect(result).to fail_to_find_a_model(:category_channel_ids)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the category has no more category_group records" do
|
||||
before do
|
||||
[user_1, user_2, admin_1, admin_2].each do |user|
|
||||
channel_1.add(user)
|
||||
channel_2.add(user)
|
||||
end
|
||||
updated_category.category_groups.delete_all
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "does not kick any users since the default permission is Everyone (full)" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2, admin_1, admin_2],
|
||||
chat_channel: [channel_1, channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context "when the category still has category_group records" do
|
||||
before do
|
||||
[user_1, user_2, admin_1, admin_2].each do |user|
|
||||
channel_1.add(user)
|
||||
channel_2.add(user)
|
||||
end
|
||||
|
||||
group_1 = Fabricate(:group)
|
||||
CategoryGroup.create(
|
||||
group: group_1,
|
||||
category: updated_category,
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
|
||||
group_2 = Fabricate(:group)
|
||||
CategoryGroup.create(
|
||||
group: group_2,
|
||||
category: updated_category,
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
|
||||
group_1.add(user_1)
|
||||
group_2.add(user_1)
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "kicks all regular users who are not in any groups with reply + see permissions" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2],
|
||||
chat_channel: [channel_1, channel_2],
|
||||
).count
|
||||
}.to 2
|
||||
end
|
||||
|
||||
it "does not kick admin users who are not in any groups with reply + see permissions" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [admin_1, admin_2],
|
||||
chat_channel: [channel_1, channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
|
||||
it "enqueues a job to kick each batch of users from the channel" do
|
||||
freeze_time
|
||||
result
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_2.id],
|
||||
channel_id: channel_1.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_2.id],
|
||||
channel_id: channel_2.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "logs a staff action" do
|
||||
result
|
||||
|
||||
changes =
|
||||
UserHistory
|
||||
.where(custom_type: "chat_auto_remove_membership")
|
||||
.all
|
||||
.map { |uh| uh.slice(:details, :acting_user_id) }
|
||||
|
||||
expect(changes).to match_array(
|
||||
[
|
||||
{
|
||||
details: "users_removed: 1\nchannel_id: #{channel_1.id}\nevent: category_updated",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
},
|
||||
{
|
||||
details: "users_removed: 1\nchannel_id: #{channel_2.id}\nevent: category_updated",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
},
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,173 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoRemove::HandleChatAllowedGroupsChange do
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params:) }
|
||||
|
||||
let(:params) { { new_allowed_groups: } }
|
||||
fab!(:user_1) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:user_2) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:admin_1) { Fabricate(:admin) }
|
||||
fab!(:admin_2) { Fabricate(:admin) }
|
||||
|
||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [admin_1, user_1]) }
|
||||
fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||
|
||||
fab!(:public_channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:public_channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
context "when chat is not enabled" do
|
||||
let(:new_allowed_groups) { "1|2" }
|
||||
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "when new_allowed_groups is empty" do
|
||||
let(:new_allowed_groups) { "" }
|
||||
|
||||
before do
|
||||
public_channel_1.add(user_1)
|
||||
public_channel_1.add(user_2)
|
||||
public_channel_2.add(user_1)
|
||||
public_channel_2.add(user_2)
|
||||
public_channel_1.add(admin_1)
|
||||
public_channel_1.add(admin_2)
|
||||
freeze_time
|
||||
end
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "removes users from all public channels" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2],
|
||||
chat_channel: [public_channel_1, public_channel_2],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
it "does not remove admin users from public channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [admin_1, admin_2],
|
||||
chat_channel: [public_channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
|
||||
it "does not remove users from direct message channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(chat_channel: [dm_channel_1, dm_channel_2]).count
|
||||
}
|
||||
end
|
||||
|
||||
it "enqueues a job to kick each batch of users from the channel" do
|
||||
result
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_1.id, user_2.id],
|
||||
channel_id: public_channel_1.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_1.id, user_2.id],
|
||||
channel_id: public_channel_2.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "logs a staff action" do
|
||||
result
|
||||
|
||||
changes =
|
||||
UserHistory
|
||||
.where(custom_type: "chat_auto_remove_membership")
|
||||
.all
|
||||
.map { |uh| uh.slice(:details, :acting_user_id) }
|
||||
|
||||
expect(changes).to match_array(
|
||||
[
|
||||
{
|
||||
details:
|
||||
"users_removed: 2\nchannel_id: #{public_channel_1.id}\nevent: chat_allowed_groups_changed",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
},
|
||||
{
|
||||
details:
|
||||
"users_removed: 2\nchannel_id: #{public_channel_2.id}\nevent: chat_allowed_groups_changed",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
},
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when new_allowed_groups includes all the users in public channels" do
|
||||
let(:new_allowed_groups) { Group::AUTO_GROUPS[:trust_level_1] }
|
||||
|
||||
before do
|
||||
public_channel_1.add(user_1)
|
||||
public_channel_2.add(user_1)
|
||||
end
|
||||
|
||||
it "does nothing" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }
|
||||
expect(result).to fail_to_find_a_model(:users)
|
||||
end
|
||||
end
|
||||
|
||||
context "when new_allowed_groups includes everyone" do
|
||||
let(:new_allowed_groups) { Group::AUTO_GROUPS[:everyone] }
|
||||
|
||||
it { is_expected.to fail_a_policy(:not_everyone_allowed) }
|
||||
|
||||
it "does nothing" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }
|
||||
end
|
||||
end
|
||||
|
||||
context "when some users are not in any of the new allowed groups" do
|
||||
let(:new_allowed_groups) { Group::AUTO_GROUPS[:trust_level_4] }
|
||||
|
||||
before do
|
||||
public_channel_1.add(user_1)
|
||||
public_channel_1.add(user_2)
|
||||
public_channel_2.add(user_1)
|
||||
public_channel_2.add(user_2)
|
||||
user_1.change_trust_level!(TrustLevel[2])
|
||||
user_2.change_trust_level!(TrustLevel[4])
|
||||
end
|
||||
|
||||
it "removes them from public channels" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
chat_channel: [public_channel_1, public_channel_2],
|
||||
).count
|
||||
}.by(-2)
|
||||
end
|
||||
|
||||
it "does not remove them from direct message channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(chat_channel: [dm_channel_2]).count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,253 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoRemove::HandleDestroyedGroup do
|
||||
describe described_class::Contract, type: :model do
|
||||
it { is_expected.to validate_presence_of(:destroyed_group_user_ids) }
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params:) }
|
||||
|
||||
let(:params) { { destroyed_group_user_ids: [admin_1.id, admin_2.id, user_1.id, user_2.id] } }
|
||||
fab!(:user_1) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:user_2) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:admin_1) { Fabricate(:admin) }
|
||||
fab!(:admin_2) { Fabricate(:admin) }
|
||||
|
||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [admin_1, user_1]) }
|
||||
fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
context "when chat is not enabled" do
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "if none of the group_user_ids users exist" do
|
||||
before { User.where(id: params[:destroyed_group_user_ids]).destroy_all }
|
||||
|
||||
it "fails to find scoped_users model" do
|
||||
expect(result).to fail_to_find_a_model(:scoped_users)
|
||||
end
|
||||
end
|
||||
|
||||
describe "step remove_users_outside_allowed_groups" do
|
||||
context "when chat_allowed_groups is empty" do
|
||||
before do
|
||||
SiteSetting.chat_allowed_groups = ""
|
||||
channel_1.add(user_1)
|
||||
channel_1.add(user_2)
|
||||
channel_2.add(user_1)
|
||||
channel_2.add(user_2)
|
||||
channel_1.add(admin_1)
|
||||
channel_1.add(admin_2)
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "removes the destroyed_group_user_ids from all public channels" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2],
|
||||
chat_channel: [channel_1, channel_2],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
it "does not remove admin users from public channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [admin_1, admin_2],
|
||||
chat_channel: [channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
|
||||
it "does not remove regular or admin users from direct message channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
chat_channel: [dm_channel_1, dm_channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
|
||||
it "enqueues a job to kick each batch of users from the channel" do
|
||||
freeze_time
|
||||
result
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_1.id, user_2.id],
|
||||
channel_id: channel_1.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [user_1.id, user_2.id],
|
||||
channel_id: channel_2.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "logs a staff action" do
|
||||
result
|
||||
actions = UserHistory.where(custom_type: "chat_auto_remove_membership")
|
||||
expect(actions.count).to eq(2)
|
||||
expect(
|
||||
actions.exists?(
|
||||
details: "users_removed: 2\nchannel_id: #{channel_2.id}\nevent: destroyed_group",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
custom_type: "chat_auto_remove_membership",
|
||||
),
|
||||
).to eq(true)
|
||||
expect(
|
||||
actions.exists?(
|
||||
details: "users_removed: 2\nchannel_id: #{channel_1.id}\nevent: destroyed_group",
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
custom_type: "chat_auto_remove_membership",
|
||||
),
|
||||
).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when chat_allowed_groups includes all the users in public channels" do
|
||||
before do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_1]
|
||||
channel_1.add(user_1)
|
||||
channel_1.add(user_2)
|
||||
channel_2.add(user_1)
|
||||
channel_2.add(user_2)
|
||||
channel_1.add(admin_1)
|
||||
channel_1.add(admin_2)
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "does not remove any memberships" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }
|
||||
end
|
||||
end
|
||||
|
||||
context "when chat_allowed_groups includes everyone" do
|
||||
before do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
channel_1.add(user_1)
|
||||
channel_1.add(user_2)
|
||||
channel_2.add(user_1)
|
||||
channel_2.add(user_2)
|
||||
channel_1.add(admin_1)
|
||||
channel_1.add(admin_2)
|
||||
end
|
||||
|
||||
it { is_expected.to fail_a_policy(:not_everyone_allowed) }
|
||||
|
||||
it "does not remove any memberships" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "step remove_users_without_channel_permission" do
|
||||
before do
|
||||
channel_1.add(user_1)
|
||||
channel_1.add(user_2)
|
||||
channel_2.add(user_1)
|
||||
channel_2.add(user_2)
|
||||
channel_1.add(admin_1)
|
||||
channel_1.add(admin_2)
|
||||
end
|
||||
|
||||
context "when channel category not read_restricted with no category_groups" do
|
||||
before do
|
||||
channel_1.chatable.update!(read_restricted: false)
|
||||
channel_1.chatable.category_groups.destroy_all
|
||||
end
|
||||
|
||||
it "does not remove any memberships" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }
|
||||
end
|
||||
end
|
||||
|
||||
context "when category channel not read_restricted with no full/create_post permission groups" do
|
||||
before do
|
||||
channel_1.chatable.update!(read_restricted: false)
|
||||
CategoryGroup.create!(
|
||||
category: channel_1.chatable,
|
||||
group_id: Group::AUTO_GROUPS[:everyone],
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
CategoryGroup.create!(
|
||||
category: channel_1.chatable,
|
||||
group_id: Group::AUTO_GROUPS[:trust_level_1],
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "removes the destroyed_group_user_ids from the channel" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2],
|
||||
chat_channel: [channel_1],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
it "does not remove any admin destroyed_group_user_ids from the channel" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [admin_1, admin_2],
|
||||
chat_channel: [channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context "when category channel not read_restricted with at least one full/create_post permission group" do
|
||||
before do
|
||||
channel_1.chatable.update!(read_restricted: false)
|
||||
CategoryGroup.create!(
|
||||
category: channel_1.chatable,
|
||||
group_id: Group::AUTO_GROUPS[:everyone],
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
CategoryGroup.create!(
|
||||
category: channel_1.chatable,
|
||||
group_id: Group::AUTO_GROUPS[:trust_level_2],
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
)
|
||||
end
|
||||
|
||||
context "when one of the users is not in any of the groups" do
|
||||
before { user_2.change_trust_level!(TrustLevel[3]) }
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "removes the destroyed_group_user_ids from the channel" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [user_1, user_2],
|
||||
chat_channel: [channel_1],
|
||||
).count
|
||||
}.to 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,246 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoRemove::HandleUserRemovedFromGroup do
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params:) }
|
||||
|
||||
let(:params) { { user_id: removed_user.id } }
|
||||
fab!(:removed_user) { Fabricate(:user) }
|
||||
fab!(:user_1) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
|
||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [removed_user, user_1]) }
|
||||
fab!(:dm_channel_2) { Fabricate(:direct_message_channel, users: [removed_user, user_2]) }
|
||||
|
||||
fab!(:public_channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:public_channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
context "when chat is not enabled" do
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "if user is deleted" do
|
||||
before { removed_user.destroy! }
|
||||
|
||||
it "fails to find the user model" do
|
||||
expect(result).to fail_to_find_a_model(:user)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user is no longer in any of the chat_allowed_groups" do
|
||||
before do
|
||||
SiteSetting.chat_allowed_groups = Fabricate(:group).id
|
||||
public_channel_1.add(removed_user)
|
||||
public_channel_2.add(removed_user)
|
||||
end
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "removes them from public channels" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [public_channel_1, public_channel_2],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
it "does not remove them from direct message channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [dm_channel_1, dm_channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
|
||||
it "enqueues a job to kick each batch of users from the channel" do
|
||||
freeze_time
|
||||
result
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [removed_user.id],
|
||||
channel_id: public_channel_1.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
expect(
|
||||
job_enqueued?(
|
||||
job: Jobs::Chat::KickUsersFromChannel,
|
||||
at: 5.seconds.from_now,
|
||||
args: {
|
||||
user_ids: [removed_user.id],
|
||||
channel_id: public_channel_2.id,
|
||||
},
|
||||
),
|
||||
).to eq(true)
|
||||
end
|
||||
|
||||
it "logs staff actions" do
|
||||
result
|
||||
|
||||
expect(
|
||||
UserHistory
|
||||
.where(
|
||||
acting_user_id: Discourse.system_user.id,
|
||||
custom_type: "chat_auto_remove_membership",
|
||||
)
|
||||
.last(2)
|
||||
.map(&:details),
|
||||
).to contain_exactly(
|
||||
"users_removed: 1\nchannel_id: #{public_channel_1.id}\nevent: user_removed_from_group",
|
||||
"users_removed: 1\nchannel_id: #{public_channel_2.id}\nevent: user_removed_from_group",
|
||||
)
|
||||
end
|
||||
|
||||
context "when the user is staff" do
|
||||
fab!(:removed_user) { Fabricate(:admin) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:user_not_staff) }
|
||||
|
||||
it "does not remove them from public channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [public_channel_1, public_channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context "when the only chat_allowed_group is everyone" do
|
||||
before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone] }
|
||||
|
||||
it { is_expected.to fail_a_policy(:not_everyone_allowed) }
|
||||
|
||||
it "does not remove them from public channels" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [public_channel_1, public_channel_2],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for private channels" do
|
||||
fab!(:group_1) { Fabricate(:group) }
|
||||
fab!(:group_2) { Fabricate(:group) }
|
||||
fab!(:private_category) { Fabricate(:private_category, group: group_1) }
|
||||
fab!(:private_channel_1) { Fabricate(:chat_channel, chatable: private_category) }
|
||||
|
||||
before do
|
||||
group_1.add(removed_user)
|
||||
group_2.add(removed_user)
|
||||
SiteSetting.chat_allowed_groups = [group_1.id, group_2.id].join("|")
|
||||
CategoryGroup.create(
|
||||
category: private_category,
|
||||
group: group_2,
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
private_channel_1.add(removed_user)
|
||||
end
|
||||
|
||||
context "when the user remains in one of the groups that can access a private channel" do
|
||||
before { group_1.remove(removed_user) }
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "does not remove them from that channel" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [private_channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user in remains in one of the groups but that group only has readonly access to the channel" do
|
||||
before do
|
||||
CategoryGroup.find_by(group: group_2, category: private_category).update!(
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
group_1.remove(removed_user)
|
||||
end
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "removes them from that channel" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [private_channel_1],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
context "when the user is staff" do
|
||||
fab!(:removed_user) { Fabricate(:admin) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:user_not_staff) }
|
||||
|
||||
it "does not remove them from that channel" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [private_channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user is no longer in any group that can access a private channel" do
|
||||
before do
|
||||
group_1.remove(removed_user)
|
||||
group_2.remove(removed_user)
|
||||
end
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "removes them from that channel" do
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [private_channel_1],
|
||||
).count
|
||||
}.to 0
|
||||
end
|
||||
|
||||
context "when the user is staff" do
|
||||
fab!(:removed_user) { Fabricate(:admin) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:user_not_staff) }
|
||||
|
||||
it "does not remove them from that channel" do
|
||||
expect { result }.not_to change {
|
||||
Chat::UserChatChannelMembership.where(
|
||||
user: [removed_user],
|
||||
chat_channel: [private_channel_1],
|
||||
).count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,124 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Chat::AutoJoinChannelBatch do
|
||||
describe Chat::AutoJoinChannelBatch::Contract, type: :model do
|
||||
subject(:contract) { described_class.new(start_user_id: 10) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:channel_id) }
|
||||
it { is_expected.to validate_presence_of(:start_user_id) }
|
||||
it { is_expected.to validate_presence_of(:end_user_id) }
|
||||
it do
|
||||
is_expected.to validate_comparison_of(:end_user_id).is_greater_than_or_equal_to(
|
||||
:start_user_id,
|
||||
)
|
||||
end
|
||||
|
||||
describe "Backward compatibility" do
|
||||
subject(:contract) { described_class.new(args) }
|
||||
|
||||
before { contract.valid? }
|
||||
|
||||
context "when providing 'chat_channel_id'" do
|
||||
let(:args) { { chat_channel_id: 2 } }
|
||||
|
||||
it "sets 'channel_id'" do
|
||||
expect(contract.channel_id).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when providing 'starts_at'" do
|
||||
let(:args) { { starts_at: 5 } }
|
||||
|
||||
it "sets 'start_user_id'" do
|
||||
expect(contract.start_user_id).to eq(5)
|
||||
end
|
||||
end
|
||||
|
||||
context "when providing 'ends_at'" do
|
||||
let(:args) { { ends_at: 8 } }
|
||||
|
||||
it "sets 'end_user_id'" do
|
||||
expect(contract.end_user_id).to eq(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params:) }
|
||||
|
||||
fab!(:channel) { Fabricate(:chat_channel, auto_join_users: true) }
|
||||
|
||||
let(:channel_id) { channel.id }
|
||||
let(:user_ids) { [Fabricate(:user).id] }
|
||||
let(:start_user_id) { user_ids.first }
|
||||
let(:end_user_id) { user_ids.last }
|
||||
let(:params) do
|
||||
{ channel_id: channel_id, start_user_id: start_user_id, end_user_id: end_user_id }
|
||||
end
|
||||
|
||||
context "when arguments are invalid" do
|
||||
let(:channel_id) { nil }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when arguments are valid" do
|
||||
context "when channel does not exist" do
|
||||
let(:channel_id) { -1 }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
|
||||
context "when channel is not a category channel" do
|
||||
fab!(:channel) { Fabricate(:direct_message_channel, auto_join_users: true) }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
|
||||
context "when channel is not in auto_join_users mode" do
|
||||
before { channel.update!(auto_join_users: false) }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
|
||||
context "when channel is found" do
|
||||
context "when more than one membership is created" do
|
||||
let(:user_ids) { Fabricate.times(2, :user).map(&:id) }
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "does not recalculate user count" do
|
||||
::Chat::ChannelMembershipManager.any_instance.expects(:recalculate_user_count).never
|
||||
result
|
||||
end
|
||||
|
||||
it "publishes an event for each user" do
|
||||
messages =
|
||||
MessageBus.track_publish(::Chat::Publisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL) do
|
||||
result
|
||||
end
|
||||
expect(messages.length).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when only one membership is created" do
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "recalculates user count" do
|
||||
::Chat::ChannelMembershipManager.any_instance.expects(:recalculate_user_count).once
|
||||
result
|
||||
end
|
||||
|
||||
it "publishes an event" do
|
||||
messages =
|
||||
MessageBus.track_publish(::Chat::Publisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL) do
|
||||
result
|
||||
end
|
||||
expect(messages.length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
348
plugins/chat/spec/services/chat/auto_join_channels_spec.rb
Normal file
348
plugins/chat/spec/services/chat/auto_join_channels_spec.rb
Normal file
@ -0,0 +1,348 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoJoinChannels do
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params: {}) }
|
||||
|
||||
let!(:previous_events) { DiscourseEvent.events.dup }
|
||||
|
||||
before { DiscourseEvent.events.clear }
|
||||
after { previous_events.each { |event, handlers| DiscourseEvent.events[event] = handlers } }
|
||||
|
||||
context "when chat is disabled" do
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled?) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
let(:trust_level) { 1 } # SiteSetting.chat_allowed_groups defaults to admins, moderators, and TL1 users
|
||||
let(:last_seen_at) { 5.minutes.ago } # Users must have been seen "recently" to be auto-joined to a channel
|
||||
|
||||
fab!(:public_category) { Fabricate(:category) }
|
||||
fab!(:private_category) { Fabricate(:category, read_restricted: true) }
|
||||
|
||||
fab!(:private_group_readonly) { Fabricate(:group) }
|
||||
fab!(:private_group_create_post) { Fabricate(:group) }
|
||||
fab!(:private_group_full) { Fabricate(:group) }
|
||||
|
||||
fab!(:private_category_group_readonly) do
|
||||
Fabricate(
|
||||
:category_group,
|
||||
category: private_category,
|
||||
group: private_group_readonly,
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:private_category_group_create_post) do
|
||||
Fabricate(
|
||||
:category_group,
|
||||
category: private_category,
|
||||
group: private_group_create_post,
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:private_category_group_full) do
|
||||
Fabricate(
|
||||
:category_group,
|
||||
category: private_category,
|
||||
group: private_group_full,
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
end
|
||||
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "with a non-auto joinable public channel" do
|
||||
fab!(:non_auto_joinable_public_channel) do
|
||||
Fabricate(:chat_channel, chatable: public_category)
|
||||
end
|
||||
|
||||
let!(:user) { Fabricate(:user, trust_level:, last_seen_at:) }
|
||||
|
||||
it "doesn't automatically join users" do
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an auto joinable public channel" do
|
||||
fab!(:auto_joinable_public_channel) do
|
||||
Fabricate(:chat_channel, chatable: public_category, auto_join_users: true)
|
||||
end
|
||||
|
||||
it "automatically joins users" do
|
||||
2.times { Fabricate(:user, trust_level:, last_seen_at:) }
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(2)
|
||||
end
|
||||
|
||||
it "automatically join users when everyone is allowed" do
|
||||
SiteSetting.chat_allowed_groups = [
|
||||
Group::AUTO_GROUPS[:everyone],
|
||||
Group::AUTO_GROUPS[:trust_level_3],
|
||||
].join(",")
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "always automatically joins moderators" do
|
||||
SiteSetting.chat_allowed_groups = Fabricate(:group).id
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
Fabricate(:moderator, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "always automatically joins admins" do
|
||||
SiteSetting.chat_allowed_groups = Fabricate(:group).id
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
Fabricate(:admin, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "automatically follows the channel in automatic mode" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.to change {
|
||||
Chat::UserChatChannelMembership
|
||||
.where(user:, chat_channel: auto_joinable_public_channel)
|
||||
.where(following: true, join_mode: :automatic)
|
||||
.count
|
||||
}.from(0).to(1)
|
||||
end
|
||||
|
||||
it "recalculates user count" do
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
::Chat::ChannelMembershipManager.any_instance.expects(:recalculate_user_count).once
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
it "publishes new channel to auto-joined users" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
::Chat::Publisher
|
||||
.expects(:publish_new_channel)
|
||||
.once
|
||||
.with(auto_joinable_public_channel, [user.id])
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
it "supports filtering down to a specific user" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { described_class.call(params: { user_id: user.id }) }.to change {
|
||||
Chat::UserChatChannelMembership.count
|
||||
}.from(0).to(1)
|
||||
end
|
||||
|
||||
it "supports filtering down to a specific channel" do
|
||||
Fabricate(:chat_channel, chatable: public_category, auto_join_users: true)
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect {
|
||||
described_class.call(params: { channel_id: auto_joinable_public_channel.id })
|
||||
}.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "supports filtering down to a specific public category" do
|
||||
Fabricate(:chat_channel, chatable: Fabricate(:category), auto_join_users: true)
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { described_class.call(params: { category_id: public_category.id }) }.to change {
|
||||
Chat::UserChatChannelMembership.count
|
||||
}.from(0).to(1)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users who have chat disabled" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
user.user_option.update!(chat_enabled: false)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join bots" do
|
||||
Fabricate(:bot, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join inactive users" do
|
||||
Fabricate(:user, trust_level:, last_seen_at:, active: false)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join staged users" do
|
||||
Fabricate(:user, trust_level:, last_seen_at:, staged: true)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join suspended users" do
|
||||
Fabricate(:user, trust_level:, last_seen_at:, suspended_till: 1.day.from_now)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join silenced users" do
|
||||
Fabricate(:user, trust_level:, last_seen_at:, silenced_till: 1.day.from_now)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join anonymous users" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
AnonymousUser.create!(user:, master_user: user, active: true)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users who haven't been seen recently" do
|
||||
Fabricate(:user, trust_level:, last_seen_at: 31.days.ago)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users who aren't in the allowed groups" do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_3]
|
||||
|
||||
(0..4).each { |tl| Fabricate(:user, trust_level: tl, last_seen_at:) }
|
||||
|
||||
# TL3 + TL4 = 2 users
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(2)
|
||||
end
|
||||
|
||||
it "limits the number of users who can be auto-joined to SiteSetting.max_chat_auto_joined_users" do
|
||||
SiteSetting.max_chat_auto_joined_users = 1
|
||||
|
||||
_user_1 = Fabricate(:user, trust_level:, last_seen_at: 10.days.ago)
|
||||
user_2 = Fabricate(:user, trust_level:, last_seen_at: 5.days.ago)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
expect(Chat::UserChatChannelMembership.last.user).to eq(user_2)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users on deleted channels" do
|
||||
auto_joinable_public_channel.update!(deleted_at: 1.day.ago)
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users to channels that have reached the maximum user count" do
|
||||
auto_joinable_public_channel.update!(user_count: SiteSetting.max_chat_auto_joined_users)
|
||||
|
||||
Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't create duplicate memberships" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
|
||||
Chat::UserChatChannelMembership.create!(user:, chat_channel: auto_joinable_public_channel)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
|
||||
it "doesn't recalculate user count if no users were auto-joined" do
|
||||
::Chat::ChannelMembershipManager.any_instance.expects(:recalculate_user_count).never
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
it "doesn't publish new channel if no users were auto-joined" do
|
||||
::Chat::Publisher.expects(:publish_new_channel).never
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-auto joinable private channel" do
|
||||
fab!(:non_auto_joinable_private_channel) do
|
||||
Fabricate(:chat_channel, chatable: private_category)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
private_group_full.add(user)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an auto joinable private channel" do
|
||||
fab!(:auto_joinable_private_channel) do
|
||||
Fabricate(:chat_channel, chatable: private_category, auto_join_users: true)
|
||||
end
|
||||
|
||||
it "automatically join users who have 'full' access to the category" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
private_group_full.add(user)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "automatically join users who have 'create post' access to the category" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
private_group_create_post.add(user)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it "doesn't automatically join users who have 'readonly' access to the category" do
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
private_group_readonly.add(user)
|
||||
|
||||
expect { result }.not_to change { Chat::UserChatChannelMembership.count }.from(0)
|
||||
end
|
||||
|
||||
it "doesn't automatically join moderators to an admin-only private channel" do
|
||||
private_category_group_full.update!(group_id: Group::AUTO_GROUPS[:admins])
|
||||
|
||||
Fabricate(:moderator, trust_level:, last_seen_at:)
|
||||
admin = Fabricate(:admin, trust_level:, last_seen_at:)
|
||||
|
||||
expect { result }.to change { Chat::UserChatChannelMembership.count }.from(0).to(1)
|
||||
expect(Chat::UserChatChannelMembership.last.user).to eq(admin)
|
||||
end
|
||||
|
||||
it "supports filtering down to a specific private category" do
|
||||
another_private_category = Fabricate(:category, read_restricted: true)
|
||||
another_private_group = Fabricate(:group)
|
||||
|
||||
Fabricate(
|
||||
:category_group,
|
||||
category: another_private_category,
|
||||
group: another_private_group,
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
|
||||
Fabricate(:chat_channel, chatable: another_private_category, auto_join_users: true)
|
||||
|
||||
user = Fabricate(:user, trust_level:, last_seen_at:)
|
||||
private_group_full.add(user)
|
||||
another_private_group.add(user)
|
||||
|
||||
expect { described_class.call(params: { category_id: private_category.id }) }.to change {
|
||||
Chat::UserChatChannelMembership.count
|
||||
}.from(0).to(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
155
plugins/chat/spec/services/chat/auto_leave_channels_spec.rb
Normal file
155
plugins/chat/spec/services/chat/auto_leave_channels_spec.rb
Normal file
@ -0,0 +1,155 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::AutoLeaveChannels do
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params: {}) }
|
||||
|
||||
let!(:previous_events) { DiscourseEvent.events.dup }
|
||||
|
||||
before { DiscourseEvent.events.clear }
|
||||
after { previous_events.each { |event, handlers| DiscourseEvent.events[event] = handlers } }
|
||||
|
||||
context "when chat is disabled" do
|
||||
before { SiteSetting.chat_enabled = false }
|
||||
|
||||
it { is_expected.to fail_a_policy(:chat_enabled?) }
|
||||
end
|
||||
|
||||
context "when chat is enabled" do
|
||||
before { SiteSetting.chat_enabled = true }
|
||||
|
||||
context "when users are not allowed to chat" do
|
||||
fab!(:uccm_1) { Fabricate(:user_chat_channel_membership) }
|
||||
fab!(:uccm_2) { Fabricate(:user_chat_channel_membership_for_dm) }
|
||||
|
||||
it "removes all their memberships" do
|
||||
expect { result }.to change { ::Chat::UserChatChannelMembership.count }.from(2).to(0)
|
||||
end
|
||||
|
||||
it "publishes automatically removed users" do
|
||||
::Chat::Action::PublishAutoRemovedUser
|
||||
.expects(:call)
|
||||
.once
|
||||
.with(
|
||||
event: :some_event_name,
|
||||
users_removed_map: {
|
||||
uccm_1.chat_channel_id => [uccm_1.user_id],
|
||||
uccm_2.chat_channel_id => [uccm_2.user_id],
|
||||
},
|
||||
)
|
||||
|
||||
described_class.call(params: { event: :some_event_name })
|
||||
end
|
||||
end
|
||||
|
||||
context "when everyone is allowed to chat" do
|
||||
fab!(:uccm) { Fabricate(:user_chat_channel_membership) }
|
||||
|
||||
before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone] }
|
||||
|
||||
it "does not remove memberships" do
|
||||
expect { result }.not_to change { ::Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the category's permission changes" do
|
||||
fab!(:user) { Fabricate(:user, trust_level: 1) }
|
||||
fab!(:group) { Fabricate(:group) }
|
||||
fab!(:category) { Fabricate(:private_category, group:) }
|
||||
fab!(:chat_channel) { Fabricate(:chat_channel, chatable: category) }
|
||||
fab!(:uccm) { Fabricate(:user_chat_channel_membership, user:, chat_channel:) }
|
||||
|
||||
before { group.add(user) }
|
||||
|
||||
context "when there's no permission anymore" do
|
||||
before { CategoryGroup.where(category:).destroy_all }
|
||||
|
||||
it "removes user membership" do
|
||||
expect { result }.to change { ::Chat::UserChatChannelMembership.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it "publishes automatically removed users" do
|
||||
::Chat::Action::PublishAutoRemovedUser
|
||||
.expects(:call)
|
||||
.once
|
||||
.with(event: nil, users_removed_map: { uccm.chat_channel_id => [uccm.user_id] })
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
it "does not remove bot membership" do
|
||||
bot = Fabricate(:bot, trust_level: 1)
|
||||
Fabricate(:user_chat_channel_membership, user: bot, chat_channel:)
|
||||
|
||||
expect { result }.not_to change {
|
||||
::Chat::UserChatChannelMembership.where(user: bot).count
|
||||
}.from(1)
|
||||
end
|
||||
|
||||
it "does not remove moderator membership" do
|
||||
user.update!(moderator: true)
|
||||
|
||||
expect { result }.not_to change { ::Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
|
||||
it "does not remove admin membership" do
|
||||
user.update!(admin: true)
|
||||
|
||||
expect { result }.not_to change { ::Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
|
||||
context "with another category/channel/user" do
|
||||
fab!(:user_2) { Fabricate(:user, trust_level: 1) }
|
||||
fab!(:category_2) { Fabricate(:private_category, group:) }
|
||||
fab!(:chat_channel_2) { Fabricate(:chat_channel, chatable: category_2) }
|
||||
fab!(:uccm_2) do
|
||||
Fabricate(:user_chat_channel_membership, user: user_2, chat_channel: chat_channel_2)
|
||||
end
|
||||
|
||||
it "supports filtering by user_id" do
|
||||
expect { described_class.call(params: { user_id: user.id }) }.to change {
|
||||
::Chat::UserChatChannelMembership.count
|
||||
}.from(2).to(1)
|
||||
end
|
||||
|
||||
it "supports filtering by channel_id" do
|
||||
expect { described_class.call(params: { channel_id: chat_channel.id }) }.to change {
|
||||
::Chat::UserChatChannelMembership.count
|
||||
}.from(2).to(1)
|
||||
end
|
||||
|
||||
it "supports filtering by category_id" do
|
||||
expect { described_class.call(params: { category_id: category.id }) }.to change {
|
||||
::Chat::UserChatChannelMembership.count
|
||||
}.from(2).to(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "removes membership when permission is 'readonly'" do
|
||||
CategoryGroup.find_by(category:, group:).update!(
|
||||
permission_type: CategoryGroup.permission_types[:readonly],
|
||||
)
|
||||
|
||||
expect { result }.to change { ::Chat::UserChatChannelMembership.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it "does not remove membership when permission is 'create_post'" do
|
||||
CategoryGroup.find_by(category:, group:).update!(
|
||||
permission_type: CategoryGroup.permission_types[:create_post],
|
||||
)
|
||||
|
||||
expect { result }.not_to change { ::Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
|
||||
it "does not remove membership when permission is 'full'" do
|
||||
CategoryGroup.find_by(category:, group:).update!(
|
||||
permission_type: CategoryGroup.permission_types[:full],
|
||||
)
|
||||
|
||||
expect { result }.not_to change { ::Chat::UserChatChannelMembership.count }.from(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -70,10 +70,7 @@ RSpec.describe Chat::CreateCategoryChannel do
|
||||
end
|
||||
|
||||
it "does not enforce automatic memberships" do
|
||||
Chat::ChannelMembershipManager
|
||||
.any_instance
|
||||
.expects(:enforce_automatic_channel_memberships)
|
||||
.never
|
||||
Chat::AutoJoinChannels.expects(:call).never
|
||||
result
|
||||
end
|
||||
|
||||
@ -88,10 +85,7 @@ RSpec.describe Chat::CreateCategoryChannel do
|
||||
let(:params) { { guardian: guardian, category_id: category_id, auto_join_users: "" } }
|
||||
|
||||
it "defaults to false" do
|
||||
Chat::ChannelMembershipManager
|
||||
.any_instance
|
||||
.expects(:enforce_automatic_channel_memberships)
|
||||
.never
|
||||
Chat::AutoJoinChannels.expects(:call).never
|
||||
result
|
||||
end
|
||||
end
|
||||
@ -100,10 +94,7 @@ RSpec.describe Chat::CreateCategoryChannel do
|
||||
let(:params) { { guardian: guardian, category_id: category_id, auto_join_users: "true" } }
|
||||
|
||||
it "enforces automatic memberships" do
|
||||
Chat::ChannelMembershipManager
|
||||
.any_instance
|
||||
.expects(:enforce_automatic_channel_memberships)
|
||||
.once
|
||||
Chat::AutoJoinChannels.expects(:call).once
|
||||
result
|
||||
end
|
||||
end
|
||||
|
@ -98,12 +98,8 @@ RSpec.describe Chat::UpdateChannel do
|
||||
end
|
||||
|
||||
it "auto joins users" do
|
||||
expect_enqueued_with(
|
||||
job: Jobs::Chat::AutoJoinChannelMemberships,
|
||||
args: {
|
||||
chat_channel_id: channel.id,
|
||||
},
|
||||
) { result }
|
||||
::Chat::AutoJoinChannels.expects(:call).with(params: { channel_id: channel.id })
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user