mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 02:40:53 -06:00
d65a839577
For sites that are configured to mute some or all categories and tags for users by default, groups can now be configured to set members' notification level to normal from the group manage UI.
649 lines
19 KiB
Ruby
649 lines
19 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class GroupsController < ApplicationController
|
|
requires_login only: [
|
|
:set_notifications,
|
|
:mentionable,
|
|
:messageable,
|
|
:check_name,
|
|
:update,
|
|
:histories,
|
|
:request_membership,
|
|
:search,
|
|
:new
|
|
]
|
|
|
|
skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
|
|
skip_before_action :check_xhr, only: [:show]
|
|
after_action :add_noindex_header
|
|
|
|
TYPE_FILTERS = {
|
|
my: Proc.new { |groups, user|
|
|
raise Discourse::NotFound unless user
|
|
Group.member_of(groups, user)
|
|
},
|
|
owner: Proc.new { |groups, user|
|
|
raise Discourse::NotFound unless user
|
|
Group.owner_of(groups, user)
|
|
},
|
|
public: Proc.new { |groups|
|
|
groups.where(public_admission: true, automatic: false)
|
|
},
|
|
close: Proc.new { |groups|
|
|
groups.where(public_admission: false, automatic: false)
|
|
},
|
|
automatic: Proc.new { |groups|
|
|
groups.where(automatic: true)
|
|
}
|
|
}
|
|
|
|
def index
|
|
unless SiteSetting.enable_group_directory? || current_user&.staff?
|
|
raise Discourse::InvalidAccess.new(:enable_group_directory)
|
|
end
|
|
|
|
order = %w{name user_count}.delete(params[:order])
|
|
dir = params[:asc].to_s == "true" ? "ASC" : "DESC"
|
|
sort = order ? "#{order} #{dir}" : nil
|
|
groups = Group.visible_groups(current_user, sort)
|
|
type_filters = TYPE_FILTERS.keys
|
|
|
|
if (username = params[:username]).present?
|
|
raise Discourse::NotFound unless user = User.find_by_username(username)
|
|
groups = TYPE_FILTERS[:my].call(groups.members_visible_groups(current_user, sort), user)
|
|
type_filters = type_filters - [:my, :owner]
|
|
end
|
|
|
|
if (filter = params[:filter]).present?
|
|
groups = Group.search_groups(filter, groups: groups)
|
|
end
|
|
|
|
if !guardian.is_staff?
|
|
# hide automatic groups from all non stuff to de-clutter page
|
|
groups = groups.where("automatic IS FALSE OR groups.id = #{Group::AUTO_GROUPS[:moderators]}")
|
|
type_filters.delete(:automatic)
|
|
end
|
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
end
|
|
|
|
if type = params[:type]&.to_sym
|
|
raise Discourse::InvalidParameters.new(:type) unless callback = TYPE_FILTERS[type]
|
|
groups = callback.call(groups, current_user)
|
|
end
|
|
|
|
if current_user
|
|
group_users = GroupUser.where(group: groups, user: current_user)
|
|
user_group_ids = group_users.pluck(:group_id)
|
|
owner_group_ids = group_users.where(owner: true).pluck(:group_id)
|
|
else
|
|
type_filters = type_filters - [:my, :owner]
|
|
end
|
|
|
|
# count the total before doing pagination
|
|
total = groups.count
|
|
|
|
page = params[:page].to_i
|
|
page_size = MobileDetection.mobile_device?(request.user_agent) ? 15 : 36
|
|
groups = groups.offset(page * page_size).limit(page_size)
|
|
|
|
render_json_dump(
|
|
groups: serialize_data(groups,
|
|
BasicGroupSerializer,
|
|
user_group_ids: user_group_ids || [],
|
|
owner_group_ids: owner_group_ids || []
|
|
),
|
|
extras: {
|
|
type_filters: type_filters
|
|
},
|
|
total_rows_groups: total,
|
|
load_more_groups: groups_path(
|
|
page: page + 1,
|
|
type: type,
|
|
order: order,
|
|
asc: params[:asc],
|
|
filter: filter
|
|
)
|
|
)
|
|
end
|
|
|
|
def show
|
|
respond_to do |format|
|
|
group = find_group(:id)
|
|
|
|
format.html do
|
|
@title = group.full_name.present? ? group.full_name.capitalize : group.name
|
|
@description_meta = group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title
|
|
render :show
|
|
end
|
|
|
|
format.json do
|
|
groups = Group.visible_groups(current_user)
|
|
groups = groups.where(automatic: false) if !guardian.is_staff?
|
|
|
|
render_json_dump(
|
|
group: serialize_data(group, GroupShowSerializer, root: nil),
|
|
extras: {
|
|
visible_group_names: groups.pluck(:name)
|
|
}
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def new
|
|
end
|
|
|
|
def edit
|
|
end
|
|
|
|
def update
|
|
group = Group.find(params[:id])
|
|
guardian.ensure_can_edit!(group) unless current_user.admin
|
|
|
|
if group.update(group_params(automatic: group.automatic))
|
|
GroupActionLogger.new(current_user, group).log_change_group_settings
|
|
render json: success_json
|
|
else
|
|
render_json_error(group)
|
|
end
|
|
end
|
|
|
|
def posts
|
|
group = find_group(:group_id)
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
posts = group.posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(20)
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
end
|
|
|
|
def posts_feed
|
|
group = find_group(:group_id)
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
@posts = group.posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(50)
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}"
|
|
@link = Discourse.base_url
|
|
@description = I18n.t("rss_description.group_posts", group_name: group.name)
|
|
render 'posts/latest', formats: [:rss]
|
|
end
|
|
|
|
def mentions
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
|
group = find_group(:group_id)
|
|
posts = group.mentioned_posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(20)
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
end
|
|
|
|
def mentions_feed
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
|
group = find_group(:group_id)
|
|
@posts = group.mentioned_posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(50)
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}"
|
|
@link = Discourse.base_url
|
|
@description = I18n.t("rss_description.group_mentions", group_name: group.name)
|
|
render 'posts/latest', formats: [:rss]
|
|
end
|
|
|
|
def members
|
|
group = find_group(:group_id)
|
|
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
limit = (params[:limit] || 50).to_i
|
|
offset = params[:offset].to_i
|
|
|
|
raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000
|
|
raise Discourse::InvalidParameters.new(:offset) if offset < 0
|
|
|
|
dir = (params[:asc] && params[:asc].present?) ? 'ASC' : 'DESC'
|
|
if params[:desc]
|
|
Discourse.deprecate(":desc is deprecated please use :asc instead", output_in_test: true)
|
|
dir = (params[:desc] && params[:desc].present?) ? 'DESC' : 'ASC'
|
|
end
|
|
order = ""
|
|
|
|
if params[:requesters]
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
users = group.requesters
|
|
total = users.count
|
|
|
|
if (filter = params[:filter]).present?
|
|
filter = filter.split(',') if filter.include?(',')
|
|
|
|
if current_user&.admin
|
|
users = users.filter_by_username_or_email(filter)
|
|
else
|
|
users = users.filter_by_username(filter)
|
|
end
|
|
end
|
|
|
|
users = users
|
|
.select("users.*, group_requests.reason, group_requests.created_at requested_at")
|
|
.order(params[:order] == 'requested_at' ? "group_requests.created_at #{dir}" : "")
|
|
.order(username_lower: dir)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
|
|
return render json: {
|
|
members: serialize_data(users, GroupRequesterSerializer),
|
|
meta: {
|
|
total: total,
|
|
limit: limit,
|
|
offset: offset
|
|
}
|
|
}
|
|
end
|
|
|
|
if params[:order] && %w{last_posted_at last_seen_at}.include?(params[:order])
|
|
order = "#{params[:order]} #{dir} NULLS LAST"
|
|
elsif params[:order] == 'added_at'
|
|
order = "group_users.created_at #{dir}"
|
|
end
|
|
|
|
users = group.users.human_users
|
|
total = users.count
|
|
|
|
if (filter = params[:filter]).present?
|
|
filter = filter.split(',') if filter.include?(',')
|
|
|
|
if current_user&.admin
|
|
users = users.filter_by_username_or_email(filter)
|
|
else
|
|
users = users.filter_by_username(filter)
|
|
end
|
|
end
|
|
|
|
users = users.joins(:user_option).select('users.*, user_options.timezone, group_users.created_at as added_at')
|
|
|
|
members = users
|
|
.order('NOT group_users.owner')
|
|
.order(order)
|
|
.order(username_lower: dir)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.includes(:primary_group)
|
|
|
|
owners = users
|
|
.order(order)
|
|
.order(username_lower: dir)
|
|
.where('group_users.owner')
|
|
.includes(:primary_group)
|
|
|
|
render json: {
|
|
members: serialize_data(members, GroupUserSerializer),
|
|
owners: serialize_data(owners, GroupUserSerializer),
|
|
meta: {
|
|
total: total,
|
|
limit: limit,
|
|
offset: offset
|
|
}
|
|
}
|
|
end
|
|
|
|
def add_members
|
|
group = Group.find(params[:id])
|
|
group.public_admission ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
|
users = users_from_params.to_a
|
|
|
|
if group.public_admission
|
|
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
|
|
unless current_user.staff?
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
end
|
|
end
|
|
|
|
emails = []
|
|
if params[:emails]
|
|
params[:emails].split(",").each do |email|
|
|
existing_user = User.find_by_email(email)
|
|
existing_user.present? ? users.push(existing_user) : emails.push(email)
|
|
end
|
|
end
|
|
|
|
if users.empty? && emails.empty?
|
|
raise Discourse::InvalidParameters.new(
|
|
'usernames or emails must be present'
|
|
)
|
|
end
|
|
|
|
if (usernames = group.users.where(id: users.map(&:id)).pluck(:username)).present?
|
|
render_json_error(I18n.t(
|
|
"groups.errors.member_already_exist",
|
|
username: usernames.sort.join(", "),
|
|
count: usernames.size
|
|
))
|
|
else
|
|
uniq_users = users.uniq
|
|
uniq_users.each do |user|
|
|
begin
|
|
group.add(user)
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
group.notify_added_to_group(user) if params[:notify_users]&.to_s == "true"
|
|
rescue ActiveRecord::RecordNotUnique
|
|
# Under concurrency, we might attempt to insert two records quickly and hit a DB
|
|
# constraint. In this case we can safely ignore the error and act as if the user
|
|
# was added to the group.
|
|
end
|
|
end
|
|
|
|
emails.each do |email|
|
|
Invite.invite_by_email(email, current_user, nil, [group.id])
|
|
end
|
|
|
|
render json: success_json.merge!(
|
|
usernames: uniq_users.map(&:username),
|
|
emails: emails
|
|
)
|
|
end
|
|
end
|
|
|
|
def handle_membership_request
|
|
group = Group.find_by(id: params[:id])
|
|
raise Discourse::InvalidParameters.new(:id) if group.blank?
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
user = User.find_by(id: params[:user_id])
|
|
raise Discourse::InvalidParameters.new(:user_id) if user.blank?
|
|
|
|
ActiveRecord::Base.transaction do
|
|
if params[:accept]
|
|
group.add(user)
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
end
|
|
|
|
GroupRequest.where(group_id: group.id, user_id: user.id).delete_all
|
|
end
|
|
|
|
if params[:accept]
|
|
PostCreator.new(current_user,
|
|
title: I18n.t('groups.request_accepted_pm.title', group_name: group.name),
|
|
raw: I18n.t('groups.request_accepted_pm.body', group_name: group.name),
|
|
archetype: Archetype.private_message,
|
|
target_usernames: user.username,
|
|
skip_validations: true
|
|
).create!
|
|
end
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def mentionable
|
|
group = find_group(:group_id, ensure_can_see: false)
|
|
|
|
if group
|
|
render json: { mentionable: Group.mentionable(current_user).where(id: group.id).present? }
|
|
else
|
|
raise Discourse::InvalidAccess.new
|
|
end
|
|
end
|
|
|
|
def messageable
|
|
group = find_group(:group_id, ensure_can_see: false)
|
|
|
|
if group
|
|
render json: { messageable: Group.messageable(current_user).where(id: group.id).present? }
|
|
else
|
|
raise Discourse::InvalidAccess.new
|
|
end
|
|
end
|
|
|
|
def check_name
|
|
group_name = params.require(:group_name)
|
|
checker = UsernameCheckerService.new(allow_reserved_username: true)
|
|
render json: checker.check_username(group_name, nil)
|
|
end
|
|
|
|
def remove_member
|
|
group = Group.find_by(id: params[:id])
|
|
raise Discourse::NotFound unless group
|
|
group.public_exit ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
|
|
|
# Maintain backwards compatibility
|
|
params[:usernames] = params[:username] if params[:username].present?
|
|
params[:user_emails] = params[:user_email] if params[:user_email].present?
|
|
|
|
users = users_from_params
|
|
raise Discourse::InvalidParameters.new(
|
|
'user_ids or usernames or user_emails must be present'
|
|
) if users.empty?
|
|
|
|
if group.public_exit
|
|
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
|
|
unless current_user.staff?
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
end
|
|
end
|
|
|
|
removed_users = []
|
|
skipped_users = []
|
|
|
|
users.each do |user|
|
|
if group.remove(user)
|
|
removed_users << user.username
|
|
GroupActionLogger.new(current_user, group).log_remove_user_from_group(user)
|
|
else
|
|
if group.users.exclude? user
|
|
skipped_users << user.username
|
|
else
|
|
raise Discourse::InvalidParameters
|
|
end
|
|
end
|
|
end
|
|
|
|
render json: success_json.merge!(
|
|
usernames: removed_users,
|
|
skipped_usernames: skipped_users
|
|
)
|
|
end
|
|
|
|
def request_membership
|
|
params.require(:reason) if params[:topic_id].blank?
|
|
|
|
group = find_group(:id)
|
|
|
|
if params[:topic_id] && topic = Topic.find_by_id(params[:topic_id])
|
|
reason = I18n.t("groups.view_hidden_topic_request_reason", group_name: group.name, topic_url: topic.url)
|
|
end
|
|
|
|
reason ||= params[:reason]
|
|
|
|
begin
|
|
GroupRequest.create!(group: group, user: current_user, reason: reason)
|
|
rescue ActiveRecord::RecordNotUnique => e
|
|
return render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")), status: 409
|
|
end
|
|
|
|
usernames = [current_user.username].concat(
|
|
group.users.where('group_users.owner')
|
|
.order("users.last_seen_at DESC")
|
|
.limit(5)
|
|
.pluck("users.username")
|
|
)
|
|
|
|
post = PostCreator.new(current_user,
|
|
title: I18n.t('groups.request_membership_pm.title', group_name: group.name),
|
|
raw: params[:reason],
|
|
archetype: Archetype.private_message,
|
|
target_usernames: usernames.join(','),
|
|
topic_opts: { custom_fields: { requested_group_id: group.id } },
|
|
skip_validations: true
|
|
).create!
|
|
|
|
render json: success_json.merge(relative_url: post.topic.relative_url)
|
|
end
|
|
|
|
def set_notifications
|
|
group = find_group(:id)
|
|
notification_level = params.require(:notification_level)
|
|
|
|
user_id = current_user.id
|
|
if guardian.is_staff?
|
|
user_id = params[:user_id] || user_id
|
|
end
|
|
|
|
GroupUser.where(group_id: group.id)
|
|
.where(user_id: user_id)
|
|
.update_all(notification_level: notification_level)
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def histories
|
|
group = find_group(:group_id)
|
|
guardian.ensure_can_edit!(group) unless current_user.admin
|
|
|
|
page_size = 25
|
|
offset = (params[:offset] && params[:offset].to_i) || 0
|
|
|
|
group_histories = GroupHistory.with_filters(group, params[:filters])
|
|
.limit(page_size)
|
|
.offset(offset * page_size)
|
|
|
|
render_json_dump(
|
|
logs: serialize_data(group_histories, BasicGroupHistorySerializer),
|
|
all_loaded: group_histories.count < page_size
|
|
)
|
|
end
|
|
|
|
def search
|
|
groups = Group.visible_groups(current_user)
|
|
.where("groups.id <> ?", Group::AUTO_GROUPS[:everyone])
|
|
.order(:name)
|
|
|
|
if (term = params[:term]).present?
|
|
groups = groups.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{term}%")
|
|
end
|
|
|
|
if params[:ignore_automatic].to_s == "true"
|
|
groups = groups.where(automatic: false)
|
|
end
|
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
end
|
|
|
|
render_serialized(groups, BasicGroupSerializer)
|
|
end
|
|
|
|
def permissions
|
|
group = find_group(:id)
|
|
category_groups = group.category_groups.select { |category_group| guardian.can_see_category?(category_group.category) }
|
|
render_serialized(category_groups.sort_by { |category_group| category_group.category.name }, CategoryGroupSerializer)
|
|
end
|
|
|
|
private
|
|
|
|
def group_params(automatic: false)
|
|
permitted_params =
|
|
if automatic
|
|
%i{
|
|
visibility_level
|
|
mentionable_level
|
|
messageable_level
|
|
default_notification_level
|
|
bio_raw
|
|
}
|
|
else
|
|
default_params = %i{
|
|
mentionable_level
|
|
messageable_level
|
|
title
|
|
flair_icon
|
|
flair_upload_id
|
|
flair_bg_color
|
|
flair_color
|
|
bio_raw
|
|
public_admission
|
|
public_exit
|
|
allow_membership_requests
|
|
full_name
|
|
default_notification_level
|
|
membership_request_template
|
|
}
|
|
|
|
if current_user.admin
|
|
default_params.push(*[
|
|
:incoming_email,
|
|
:smtp_server,
|
|
:smtp_port,
|
|
:smtp_ssl,
|
|
:imap_server,
|
|
:imap_port,
|
|
:imap_ssl,
|
|
:imap_mailbox_name,
|
|
:email_username,
|
|
:email_password,
|
|
:primary_group,
|
|
:visibility_level,
|
|
:members_visibility_level,
|
|
:name,
|
|
:grant_trust_level,
|
|
:automatic_membership_email_domains,
|
|
:publish_read_state
|
|
])
|
|
|
|
custom_fields = DiscoursePluginRegistry.editable_group_custom_fields
|
|
default_params << { custom_fields: custom_fields } unless custom_fields.blank?
|
|
end
|
|
|
|
default_params
|
|
end
|
|
|
|
if !automatic || current_user.admin
|
|
[:muted, :regular, :tracking, :watching, :watching_first_post].each do |level|
|
|
permitted_params << { "#{level}_category_ids" => [] }
|
|
permitted_params << { "#{level}_tags" => [] }
|
|
end
|
|
end
|
|
|
|
params.require(:group).permit(*permitted_params)
|
|
end
|
|
|
|
def find_group(param_name, ensure_can_see: true)
|
|
name = params.require(param_name)
|
|
group = Group.find_by("LOWER(name) = ?", name.downcase)
|
|
guardian.ensure_can_see!(group) if ensure_can_see
|
|
group
|
|
end
|
|
|
|
def users_from_params
|
|
if params[:usernames].present?
|
|
users = User.where(username_lower: params[:usernames].split(",").map(&:downcase))
|
|
raise Discourse::InvalidParameters.new(:usernames) if users.blank?
|
|
elsif params[:user_id].present?
|
|
users = User.where(id: params[:user_id].to_i)
|
|
raise Discourse::InvalidParameters.new(:user_id) if users.blank?
|
|
elsif params[:user_ids].present?
|
|
users = User.where(id: params[:user_ids].to_s.split(","))
|
|
raise Discourse::InvalidParameters.new(:user_ids) if users.blank?
|
|
elsif params[:user_emails].present?
|
|
users = User.with_email(params[:user_emails].split(","))
|
|
raise Discourse::InvalidParameters.new(:user_emails) if users.blank?
|
|
else
|
|
users = []
|
|
end
|
|
users
|
|
end
|
|
end
|