mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 01:25:54 +08:00
600f2854c7
Previously, we did not log any topic slow mode changes. This allowed some malicious (or just careless) TL4 users to delete slow modes created by moderators at will. Administrators could not see who changed the slow mode unless they had SQL knowledge and used Data Explorer. This commit enables logging who turns slow mode on, off, or changes it. Related meta topic: https://meta.discourse.org/t/why-is-there-no-record-of-who-added-or-removed-slow-mode/316354
1458 lines
42 KiB
Ruby
1458 lines
42 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class TopicsController < ApplicationController
|
|
requires_login only: %i[
|
|
timings
|
|
destroy_timings
|
|
update
|
|
update_shared_draft
|
|
destroy
|
|
recover
|
|
status
|
|
invite
|
|
mute
|
|
unmute
|
|
set_notifications
|
|
move_posts
|
|
merge_topic
|
|
clear_pin
|
|
re_pin
|
|
status_update
|
|
timer
|
|
bulk
|
|
reset_new
|
|
change_post_owners
|
|
change_timestamps
|
|
archive_message
|
|
move_to_inbox
|
|
convert_topic
|
|
bookmark
|
|
publish
|
|
reset_bump_date
|
|
set_slow_mode
|
|
]
|
|
|
|
before_action :consider_user_for_promotion, only: :show
|
|
|
|
skip_before_action :check_xhr, only: %i[show feed]
|
|
|
|
def id_for_slug
|
|
topic = Topic.find_by_slug(params[:slug])
|
|
guardian.ensure_can_see!(topic)
|
|
raise Discourse::NotFound unless topic
|
|
render json: { slug: topic.slug, topic_id: topic.id, url: topic.url }
|
|
end
|
|
|
|
def show_by_external_id
|
|
topic = Topic.find_by(external_id: params[:external_id])
|
|
raise Discourse::NotFound unless topic
|
|
guardian.ensure_can_see!(topic)
|
|
redirect_to_correct_topic(topic, params[:post_number])
|
|
end
|
|
|
|
def show
|
|
flash["referer"] ||= request.referer[0..255] if request.referer
|
|
|
|
# TODO: We'd like to migrate the wordpress feed to another url. This keeps up backwards
|
|
# compatibility with existing installs.
|
|
return wordpress if params[:best].present?
|
|
|
|
# work around people somehow sending in arrays,
|
|
# arrays are not supported
|
|
params[:page] = begin
|
|
params[:page].to_i
|
|
rescue StandardError
|
|
1
|
|
end
|
|
|
|
opts =
|
|
params.slice(
|
|
:username_filters,
|
|
:filter,
|
|
:page,
|
|
:post_number,
|
|
:show_deleted,
|
|
:replies_to_post_number,
|
|
:filter_upwards_post_id,
|
|
:filter_top_level_replies,
|
|
)
|
|
username_filters = opts[:username_filters]
|
|
|
|
opts[:print] = true if params[:print] == "true"
|
|
opts[:username_filters] = username_filters.split(",") if username_filters.is_a?(String)
|
|
|
|
# Special case: a slug with a number in front should look by slug first before looking
|
|
# up that particular number
|
|
if params[:id] && params[:id] =~ /\A\d+[^\d\\]+\z/
|
|
topic = Topic.find_by_slug(params[:id])
|
|
return redirect_to_correct_topic(topic, opts[:post_number]) if topic
|
|
end
|
|
|
|
if opts[:print]
|
|
raise Discourse::InvalidAccess if SiteSetting.max_prints_per_hour_per_user.zero?
|
|
begin
|
|
unless @guardian.is_admin?
|
|
RateLimiter.new(
|
|
current_user,
|
|
"print-topic-per-hour",
|
|
SiteSetting.max_prints_per_hour_per_user,
|
|
1.hour,
|
|
).performed!
|
|
end
|
|
rescue RateLimiter::LimitExceeded
|
|
return render_json_error I18n.t("rate_limiter.slow_down")
|
|
end
|
|
end
|
|
|
|
begin
|
|
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
|
|
rescue Discourse::NotFound => ex
|
|
if params[:id]
|
|
topic = Topic.find_by_slug(params[:id])
|
|
return redirect_to_correct_topic(topic, opts[:post_number]) if topic
|
|
end
|
|
|
|
raise ex
|
|
rescue Discourse::NotLoggedIn => ex
|
|
raise(SiteSetting.detailed_404 ? ex : Discourse::NotFound)
|
|
rescue Discourse::InvalidAccess => ex
|
|
# If the user can't see the topic, clean up notifications for it.
|
|
Notification.remove_for(current_user.id, params[:topic_id]) if current_user
|
|
|
|
deleted =
|
|
guardian.can_see_topic?(ex.obj, false) ||
|
|
(!guardian.can_see_topic?(ex.obj) && ex.obj&.access_topic_via_group && ex.obj&.deleted_at)
|
|
|
|
if SiteSetting.detailed_404
|
|
if deleted
|
|
raise Discourse::NotFound.new(
|
|
"deleted topic",
|
|
custom_message: "deleted_topic",
|
|
status: 410,
|
|
check_permalinks: true,
|
|
original_path: ex.obj.relative_url,
|
|
)
|
|
elsif !guardian.can_see_topic?(ex.obj) && group = ex.obj&.access_topic_via_group
|
|
raise Discourse::InvalidAccess.new(
|
|
"not in group",
|
|
ex.obj,
|
|
custom_message: "not_in_group.title_topic",
|
|
custom_message_params: {
|
|
group: group.name,
|
|
},
|
|
group: serialize_data(group, BasicGroupSerializer, root: false),
|
|
)
|
|
end
|
|
|
|
raise ex
|
|
else
|
|
raise Discourse::NotFound.new(
|
|
nil,
|
|
check_permalinks: deleted,
|
|
original_path: ex.obj.relative_url,
|
|
)
|
|
end
|
|
end
|
|
|
|
page = params[:page]
|
|
if (page < 0) || ((page - 1) * @topic_view.chunk_size >= @topic_view.topic.highest_post_number)
|
|
raise Discourse::NotFound
|
|
end
|
|
|
|
discourse_expires_in 1.minute
|
|
|
|
if slugs_do_not_match || (!request.format.json? && params[:slug].nil?)
|
|
redirect_to_correct_topic(@topic_view.topic, opts[:post_number])
|
|
return
|
|
end
|
|
|
|
track_visit_to_topic
|
|
|
|
if should_track_visit_to_topic?
|
|
@topic_view.draft = Draft.get(current_user, @topic_view.draft_key, @topic_view.draft_sequence)
|
|
end
|
|
|
|
response.headers["X-Robots-Tag"] = "noindex" unless @topic_view.topic.visible
|
|
|
|
canonical_url UrlHelper.absolute_without_cdn(@topic_view.canonical_path)
|
|
|
|
# provide hint to crawlers only for now
|
|
# we would like to give them a bit more signal about age of data
|
|
if use_crawler_layout?
|
|
if last_modified = @topic_view.posts&.map { |p| p.updated_at }&.max&.httpdate
|
|
response.headers["Last-Modified"] = last_modified
|
|
end
|
|
end
|
|
|
|
perform_show_response
|
|
end
|
|
|
|
def publish
|
|
params.permit(:id, :destination_category_id)
|
|
|
|
topic = Topic.find(params[:id])
|
|
category = Category.find(params[:destination_category_id])
|
|
|
|
raise Discourse::InvalidParameters if category.id == SiteSetting.shared_drafts_category.to_i
|
|
|
|
guardian.ensure_can_publish_topic!(topic, category)
|
|
topic = TopicPublisher.new(topic, current_user, category.id).publish!
|
|
|
|
render_serialized(topic.reload, BasicTopicSerializer)
|
|
end
|
|
|
|
def wordpress
|
|
params.require(:best)
|
|
params.require(:topic_id)
|
|
params.permit(
|
|
:min_trust_level,
|
|
:min_score,
|
|
:min_replies,
|
|
:bypass_trust_level_score,
|
|
:only_moderator_liked,
|
|
)
|
|
|
|
begin
|
|
opts = {
|
|
best: params[:best].to_i,
|
|
min_trust_level: params[:min_trust_level] ? params[:min_trust_level].to_i : 1,
|
|
min_score: params[:min_score].to_i,
|
|
min_replies: params[:min_replies].to_i,
|
|
bypass_trust_level_score: params[:bypass_trust_level_score].to_i, # safe cause 0 means ignore
|
|
only_moderator_liked: params[:only_moderator_liked].to_s == "true",
|
|
exclude_hidden: true,
|
|
}
|
|
rescue NoMethodError
|
|
raise Discourse::InvalidParameters
|
|
end
|
|
|
|
@topic_view = TopicView.new(params[:topic_id], current_user, opts)
|
|
discourse_expires_in 1.minute
|
|
|
|
wordpress_serializer =
|
|
TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
|
|
render_json_dump(wordpress_serializer)
|
|
end
|
|
|
|
def post_ids
|
|
params.require(:topic_id)
|
|
params.permit(:post_number, :username_filters, :filter)
|
|
|
|
options = {
|
|
filter_post_number: params[:post_number],
|
|
filter: params[:filter],
|
|
skip_limit: true,
|
|
asc: true,
|
|
skip_custom_fields: true,
|
|
}
|
|
|
|
fetch_topic_view(options)
|
|
render_json_dump(post_ids: @topic_view.posts.pluck(:id))
|
|
end
|
|
|
|
def posts
|
|
params.require(:topic_id)
|
|
params.permit(:post_ids, :post_number, :username_filters, :filter, :include_suggested)
|
|
|
|
include_suggested = params[:include_suggested] == "true"
|
|
|
|
options = {
|
|
filter_post_number: params[:post_number],
|
|
post_ids: params[:post_ids],
|
|
asc: ActiveRecord::Type::Boolean.new.deserialize(params[:asc]),
|
|
filter: params[:filter],
|
|
include_suggested: include_suggested,
|
|
include_related: include_suggested,
|
|
}
|
|
|
|
fetch_topic_view(options)
|
|
|
|
render_json_dump(
|
|
TopicViewPostsSerializer.new(
|
|
@topic_view,
|
|
scope: guardian,
|
|
root: false,
|
|
include_raw: !!params[:include_raw],
|
|
),
|
|
)
|
|
end
|
|
|
|
def excerpts
|
|
params.require(:topic_id)
|
|
params.require(:post_ids)
|
|
|
|
unless Array === params[:post_ids]
|
|
render_json_error("Expecting post_ids to contain a list of posts ids")
|
|
return
|
|
end
|
|
post_ids = params[:post_ids].map(&:to_i)
|
|
|
|
if post_ids.length > 100
|
|
render_json_error("Requested a chunk that is too big")
|
|
return
|
|
end
|
|
|
|
@topic = Topic.with_deleted.where(id: params[:topic_id]).first
|
|
guardian.ensure_can_see!(@topic)
|
|
|
|
@posts =
|
|
Post
|
|
.where(hidden: false, deleted_at: nil, topic_id: @topic.id)
|
|
.where("posts.id in (?)", post_ids)
|
|
.joins("LEFT JOIN users u on u.id = posts.user_id")
|
|
.pluck(:id, :cooked, :username, :action_code, :created_at)
|
|
.map do |post_id, cooked, username, action_code, created_at|
|
|
attrs = {
|
|
post_id: post_id,
|
|
username: username,
|
|
excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true),
|
|
}
|
|
|
|
if action_code
|
|
attrs[:action_code] = action_code
|
|
attrs[:created_at] = created_at
|
|
end
|
|
|
|
attrs
|
|
end
|
|
|
|
render json: @posts.to_json
|
|
end
|
|
|
|
def destroy_timings
|
|
topic_id = params[:topic_id].to_i
|
|
|
|
if params[:last].to_s == "1"
|
|
PostTiming.destroy_last_for(current_user, topic_id: topic_id)
|
|
else
|
|
PostTiming.destroy_for(current_user.id, [topic_id])
|
|
end
|
|
|
|
last_notification =
|
|
Notification
|
|
.where(user_id: current_user.id, topic_id: topic_id)
|
|
.order(created_at: :desc)
|
|
.limit(1)
|
|
.first
|
|
|
|
last_notification.update!(read: false) if last_notification
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def update_shared_draft
|
|
topic = Topic.find_by(id: params[:id])
|
|
guardian.ensure_can_edit!(topic)
|
|
|
|
category = Category.find_by(id: params[:category_id].to_i)
|
|
guardian.ensure_can_publish_topic!(topic, category)
|
|
|
|
row_count = SharedDraft.where(topic_id: topic.id).update_all(category_id: category.id)
|
|
SharedDraft.create(topic_id: topic.id, category_id: category.id) if row_count == 0
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def update
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
guardian.ensure_can_edit!(topic)
|
|
|
|
if params[:category_id] && (params[:category_id].to_i != topic.category_id.to_i)
|
|
if topic.shared_draft
|
|
topic.shared_draft.update(category_id: params[:category_id])
|
|
params.delete(:category_id)
|
|
else
|
|
category = Category.find_by(id: params[:category_id])
|
|
|
|
if category || (params[:category_id].to_i == 0)
|
|
begin
|
|
guardian.ensure_can_move_topic_to_category!(category)
|
|
rescue Discourse::InvalidAccess
|
|
return(
|
|
render_json_error I18n.t("category.errors.move_topic_to_category_disallowed"),
|
|
status: :forbidden
|
|
)
|
|
end
|
|
else
|
|
return render_json_error(I18n.t("category.errors.not_found"))
|
|
end
|
|
|
|
if category &&
|
|
topic_tags = (params[:tags] || topic.tags.pluck(:name)).reject { |c| c.empty? }
|
|
if topic_tags.present?
|
|
allowed_tags =
|
|
DiscourseTagging.filter_allowed_tags(guardian, category: category).map(&:name)
|
|
|
|
invalid_tags = topic_tags - allowed_tags
|
|
|
|
# Do not raise an error on a topic's hidden tags when not modifying tags
|
|
if params[:tags].blank?
|
|
invalid_tags.each do |tag_name|
|
|
if DiscourseTagging.hidden_tag_names.include?(tag_name)
|
|
invalid_tags.delete(tag_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
invalid_tags = Tag.where_name(invalid_tags).pluck(:name)
|
|
|
|
if !invalid_tags.empty?
|
|
if (invalid_tags & DiscourseTagging.hidden_tag_names).present?
|
|
return render_json_error(I18n.t("category.errors.disallowed_tags_generic"))
|
|
else
|
|
return(
|
|
render_json_error(
|
|
I18n.t("category.errors.disallowed_topic_tags", tags: invalid_tags.join(", ")),
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
changes = {}
|
|
|
|
PostRevisor.tracked_topic_fields.each_key { |f| changes[f] = params[f] if params.has_key?(f) }
|
|
|
|
changes.delete(:title) if topic.title == changes[:title]
|
|
changes.delete(:category_id) if topic.category_id.to_i == changes[:category_id].to_i
|
|
|
|
if Tag.include_tags?
|
|
topic_tags = topic.tags.map(&:name).sort
|
|
changes.delete(:tags) if changes[:tags]&.sort == topic_tags
|
|
end
|
|
|
|
success = true
|
|
|
|
if changes.length > 0
|
|
bypass_bump = should_bypass_bump?(changes)
|
|
|
|
first_post = topic.ordered_posts.first
|
|
success =
|
|
PostRevisor.new(first_post, topic).revise!(
|
|
current_user,
|
|
changes,
|
|
validate_post: false,
|
|
bypass_bump: bypass_bump,
|
|
keep_existing_draft: params[:keep_existing_draft].to_s == "true",
|
|
)
|
|
|
|
topic.errors.add(:base, :unable_to_update) if !success && topic.errors.blank?
|
|
end
|
|
|
|
# this is used to return the title to the client as it may have been changed by "TextCleaner"
|
|
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
|
|
end
|
|
|
|
def update_tags
|
|
params.require(:tags)
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
guardian.ensure_can_edit_tags!(topic)
|
|
|
|
success =
|
|
PostRevisor.new(topic.first_post, topic).revise!(
|
|
current_user,
|
|
{ tags: params[:tags] },
|
|
validate_post: false,
|
|
)
|
|
|
|
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
|
|
end
|
|
|
|
def feature_stats
|
|
params.require(:category_id)
|
|
category_id = params[:category_id].to_i
|
|
|
|
visible_topics = Topic.listable_topics.visible
|
|
|
|
render json: {
|
|
pinned_in_category_count:
|
|
visible_topics
|
|
.where(category_id: category_id)
|
|
.where(pinned_globally: false)
|
|
.where.not(pinned_at: nil)
|
|
.count,
|
|
pinned_globally_count:
|
|
visible_topics.where(pinned_globally: true).where.not(pinned_at: nil).count,
|
|
banner_count: Topic.listable_topics.where(archetype: Archetype.banner).count,
|
|
}
|
|
end
|
|
|
|
def status
|
|
params.require(:status)
|
|
params.require(:enabled)
|
|
params.permit(:until)
|
|
|
|
status = params[:status]
|
|
topic_id = params[:topic_id].to_i
|
|
enabled = params[:enabled] == "true"
|
|
|
|
check_for_status_presence(:status, status)
|
|
@topic =
|
|
if params[:category_id]
|
|
Topic.find_by(id: topic_id, category_id: params[:category_id].to_i)
|
|
else
|
|
Topic.find_by(id: topic_id)
|
|
end
|
|
|
|
status_opts = { until: params[:until].presence }
|
|
|
|
if status == "visible"
|
|
status_opts[:visibility_reason_id] = (
|
|
if enabled
|
|
Topic.visibility_reasons[:manually_relisted]
|
|
else
|
|
Topic.visibility_reasons[:manually_unlisted]
|
|
end
|
|
)
|
|
end
|
|
|
|
case status
|
|
when "closed"
|
|
guardian.ensure_can_close_topic!(@topic)
|
|
when "archived"
|
|
guardian.ensure_can_archive_topic!(@topic)
|
|
when "visible"
|
|
guardian.ensure_can_toggle_topic_visibility!(@topic)
|
|
when "pinned"
|
|
guardian.ensure_can_pin_unpin_topic!(@topic)
|
|
else
|
|
guardian.ensure_can_moderate!(@topic)
|
|
end
|
|
|
|
@topic.update_status(status, enabled, current_user, status_opts)
|
|
|
|
render json:
|
|
success_json.merge!(
|
|
topic_status_update:
|
|
TopicTimerSerializer.new(TopicTimer.find_by(topic: @topic), root: false),
|
|
)
|
|
end
|
|
|
|
def mute
|
|
toggle_mute
|
|
end
|
|
|
|
def unmute
|
|
toggle_mute
|
|
end
|
|
|
|
def timer
|
|
params.permit(:time, :based_on_last_post, :category_id)
|
|
params.require(:status_type)
|
|
|
|
status_type =
|
|
begin
|
|
TopicTimer.types.fetch(params[:status_type].to_sym)
|
|
rescue StandardError
|
|
invalid_param(:status_type)
|
|
end
|
|
based_on_last_post = params[:based_on_last_post]
|
|
params.require(:duration_minutes) if based_on_last_post
|
|
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
guardian.ensure_can_moderate!(topic)
|
|
|
|
guardian.ensure_can_delete!(topic) if TopicTimer.destructive_types.values.include?(status_type)
|
|
|
|
options = { by_user: current_user, based_on_last_post: based_on_last_post }
|
|
|
|
options.merge!(category_id: params[:category_id]) if !params[:category_id].blank?
|
|
if params[:duration_minutes].present?
|
|
options.merge!(duration_minutes: params[:duration_minutes].to_i)
|
|
end
|
|
options.merge!(duration: params[:duration].to_i) if params[:duration].present?
|
|
|
|
begin
|
|
topic_timer = topic.set_or_create_timer(status_type, params[:time], **options)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
return render_json_error(e.message)
|
|
end
|
|
|
|
if topic.save
|
|
render json:
|
|
success_json.merge!(
|
|
execute_at: topic_timer&.execute_at,
|
|
duration_minutes: topic_timer&.duration_minutes,
|
|
based_on_last_post: topic_timer&.based_on_last_post,
|
|
closed: topic.closed,
|
|
category_id: topic_timer&.category_id,
|
|
)
|
|
else
|
|
render_json_error(topic)
|
|
end
|
|
end
|
|
|
|
def make_banner
|
|
topic = Topic.find_by(id: params[:topic_id].to_i)
|
|
guardian.ensure_can_banner_topic!(topic)
|
|
|
|
topic.make_banner!(current_user)
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def remove_banner
|
|
topic = Topic.find_by(id: params[:topic_id].to_i)
|
|
guardian.ensure_can_banner_topic!(topic)
|
|
|
|
topic.remove_banner!(current_user)
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def remove_bookmarks
|
|
topic = Topic.find(params[:topic_id].to_i)
|
|
BookmarkManager.new(current_user).destroy_for_topic(topic)
|
|
render body: nil
|
|
end
|
|
|
|
def archive_message
|
|
toggle_archive_message(true)
|
|
end
|
|
|
|
def move_to_inbox
|
|
toggle_archive_message(false)
|
|
end
|
|
|
|
def toggle_archive_message(archive)
|
|
topic = Topic.find(params[:id].to_i)
|
|
|
|
group_id = nil
|
|
|
|
group_ids = current_user.groups.pluck(:id)
|
|
if group_ids.present?
|
|
allowed_groups =
|
|
topic.allowed_groups.where("topic_allowed_groups.group_id IN (?)", group_ids).pluck(:id)
|
|
|
|
allowed_groups.each do |id|
|
|
if archive
|
|
GroupArchivedMessage.archive!(id, topic, acting_user_id: current_user.id)
|
|
|
|
group_id = id
|
|
else
|
|
GroupArchivedMessage.move_to_inbox!(id, topic, acting_user_id: current_user.id)
|
|
end
|
|
end
|
|
end
|
|
|
|
if topic.allowed_users.include?(current_user)
|
|
if archive
|
|
UserArchivedMessage.archive!(current_user.id, topic)
|
|
else
|
|
UserArchivedMessage.move_to_inbox!(current_user.id, topic)
|
|
end
|
|
end
|
|
|
|
if group_id
|
|
name = Group.find_by(id: group_id).try(:name)
|
|
render_json_dump(group_name: name)
|
|
else
|
|
render body: nil
|
|
end
|
|
end
|
|
|
|
def bookmark
|
|
topic = Topic.find(params[:topic_id].to_i)
|
|
|
|
bookmark_manager = BookmarkManager.new(current_user)
|
|
bookmark_manager.create_for(bookmarkable_id: topic.id, bookmarkable_type: "Topic")
|
|
|
|
return render_json_error(bookmark_manager, status: 400) if bookmark_manager.errors.any?
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def destroy
|
|
topic = Topic.with_deleted.find_by(id: params[:id])
|
|
force_destroy = ActiveModel::Type::Boolean.new.cast(params[:force_destroy])
|
|
|
|
if force_destroy
|
|
if !topic
|
|
raise Discourse::InvalidAccess
|
|
elsif !guardian.can_permanently_delete?(topic)
|
|
return render_json_error topic.cannot_permanently_delete_reason(current_user), status: 403
|
|
end
|
|
else
|
|
guardian.ensure_can_delete!(topic)
|
|
end
|
|
|
|
PostDestroyer.new(
|
|
current_user,
|
|
topic.ordered_posts.with_deleted.first,
|
|
context: params[:context],
|
|
force_destroy: force_destroy,
|
|
).destroy
|
|
|
|
render body: nil
|
|
rescue Discourse::InvalidAccess
|
|
render_json_error I18n.t("delete_topic_failed")
|
|
end
|
|
|
|
def recover
|
|
topic = Topic.where(id: params[:topic_id]).with_deleted.first
|
|
guardian.ensure_can_recover_topic!(topic)
|
|
|
|
first_post = topic.posts.with_deleted.order(:post_number).first
|
|
PostDestroyer.new(current_user, first_post, context: params[:context]).recover
|
|
|
|
render body: nil
|
|
end
|
|
|
|
def excerpt
|
|
render body: nil
|
|
end
|
|
|
|
def remove_allowed_user
|
|
params.require(:username)
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
raise Discourse::NotFound unless topic
|
|
user = User.find_by(username: params[:username])
|
|
raise Discourse::NotFound unless user
|
|
|
|
guardian.ensure_can_remove_allowed_users!(topic, user)
|
|
|
|
if topic.remove_allowed_user(current_user, user)
|
|
render json: success_json
|
|
else
|
|
render json: failed_json, status: 422
|
|
end
|
|
end
|
|
|
|
def remove_allowed_group
|
|
params.require(:name)
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
guardian.ensure_can_remove_allowed_users!(topic)
|
|
|
|
if topic.remove_allowed_group(current_user, params[:name])
|
|
render json: success_json
|
|
else
|
|
render json: failed_json, status: 422
|
|
end
|
|
end
|
|
|
|
def invite_group
|
|
group = Group.find_by(name: params[:group])
|
|
raise Discourse::NotFound if !group
|
|
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
raise Discourse::NotFound if !topic
|
|
|
|
if !pm_has_slots?(topic)
|
|
return(
|
|
render_json_error(
|
|
I18n.t(
|
|
"pm_reached_recipients_limit",
|
|
recipients_limit: SiteSetting.max_allowed_message_recipients,
|
|
),
|
|
)
|
|
)
|
|
end
|
|
|
|
if topic.private_message?
|
|
guardian.ensure_can_invite_group_to_private_message!(group, topic)
|
|
topic.invite_group(current_user, group)
|
|
render_json_dump BasicGroupSerializer.new(group, scope: guardian, root: "group")
|
|
else
|
|
render json: failed_json, status: 422
|
|
end
|
|
end
|
|
|
|
def invite
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
raise Discourse::NotFound if !topic
|
|
|
|
return render_json_error(I18n.t("topic_invite.not_pm")) if !topic.private_message?
|
|
|
|
if !pm_has_slots?(topic)
|
|
return(
|
|
render_json_error(
|
|
I18n.t(
|
|
"pm_reached_recipients_limit",
|
|
recipients_limit: SiteSetting.max_allowed_message_recipients,
|
|
),
|
|
)
|
|
)
|
|
end
|
|
|
|
guardian.ensure_can_invite_to!(topic)
|
|
|
|
username_or_email = params[:user] ? fetch_username : fetch_email
|
|
group_ids =
|
|
Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names]).pluck(
|
|
:id,
|
|
)
|
|
|
|
begin
|
|
if topic.invite(current_user, username_or_email, group_ids, params[:custom_message])
|
|
if user = User.find_by_username_or_email(username_or_email)
|
|
render_json_dump BasicUserSerializer.new(user, scope: guardian, root: "user")
|
|
else
|
|
render json: success_json
|
|
end
|
|
else
|
|
json = failed_json
|
|
|
|
unless topic.private_message?
|
|
group_names =
|
|
topic
|
|
.category
|
|
.visible_group_names(current_user)
|
|
.where(automatic: false)
|
|
.pluck(:name)
|
|
.join(", ")
|
|
|
|
if group_names.present?
|
|
json.merge!(errors: [I18n.t("topic_invite.failed_to_invite", group_names: group_names)])
|
|
end
|
|
end
|
|
|
|
render json: json, status: 422
|
|
end
|
|
rescue Topic::UserExists, Topic::NotAllowed => e
|
|
render json: { errors: [e.message] }, status: 422
|
|
end
|
|
end
|
|
|
|
def set_notifications
|
|
user =
|
|
if is_api? && @guardian.is_admin? &&
|
|
(params[:username].present? || params[:external_id].present?)
|
|
fetch_user_from_params
|
|
else
|
|
current_user
|
|
end
|
|
|
|
topic = Topic.find(params[:topic_id].to_i)
|
|
TopicUser.change(user, topic.id, notification_level: params[:notification_level].to_i)
|
|
render json: success_json
|
|
end
|
|
|
|
def merge_topic
|
|
topic_id = params.require(:topic_id)
|
|
destination_topic_id = params.require(:destination_topic_id)
|
|
params.permit(:participants)
|
|
params.permit(:chronological_order)
|
|
params.permit(:archetype)
|
|
|
|
raise Discourse::InvalidAccess if params[:archetype] == "private_message" && !guardian.is_staff?
|
|
|
|
topic = Topic.find_by(id: topic_id)
|
|
guardian.ensure_can_move_posts!(topic)
|
|
|
|
destination_topic = Topic.find_by(id: destination_topic_id)
|
|
guardian.ensure_can_create_post_on_topic!(destination_topic)
|
|
|
|
args = {}
|
|
args[:destination_topic_id] = destination_topic_id.to_i
|
|
args[:chronological_order] = params[:chronological_order] == "true"
|
|
|
|
if params[:archetype].present?
|
|
args[:archetype] = params[:archetype]
|
|
args[:participants] = params[:participants] if params[:participants].present? &&
|
|
params[:archetype] == "private_message"
|
|
end
|
|
|
|
destination_topic = topic.move_posts(current_user, topic.posts.pluck(:id), args)
|
|
render_topic_changes(destination_topic)
|
|
end
|
|
|
|
def move_posts
|
|
post_ids = params.require(:post_ids)
|
|
topic_id = params.require(:topic_id)
|
|
params.permit(:category_id)
|
|
params.permit(:tags)
|
|
params.permit(:participants)
|
|
params.permit(:chronological_order)
|
|
params.permit(:archetype)
|
|
|
|
raise Discourse::InvalidAccess if params[:archetype] == "private_message" && !guardian.is_staff?
|
|
|
|
topic = Topic.with_deleted.find_by(id: topic_id)
|
|
guardian.ensure_can_move_posts!(topic)
|
|
|
|
if params[:title].present?
|
|
# when creating a new topic, ensure the 1st post is a regular post
|
|
if Post.where(topic: topic, id: post_ids).order(:post_number).pick(:post_type) !=
|
|
Post.types[:regular]
|
|
return(
|
|
render_json_error(
|
|
"When moving posts to a new topic, the first post must be a regular post.",
|
|
)
|
|
)
|
|
end
|
|
|
|
if params[:category_id].present?
|
|
guardian.ensure_can_create_topic_on_category!(params[:category_id])
|
|
end
|
|
end
|
|
|
|
destination_topic = move_posts_to_destination(topic)
|
|
render_topic_changes(destination_topic)
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => ex
|
|
render_json_error(ex)
|
|
end
|
|
|
|
def change_post_owners
|
|
params.require(:post_ids)
|
|
params.require(:topic_id)
|
|
params.require(:username)
|
|
|
|
guardian.ensure_can_change_post_owner!
|
|
|
|
begin
|
|
PostOwnerChanger.new(
|
|
post_ids: params[:post_ids].to_a,
|
|
topic_id: params[:topic_id].to_i,
|
|
new_owner: User.find_by(username: params[:username]),
|
|
acting_user: current_user,
|
|
).change_owner!
|
|
render json: success_json
|
|
rescue ArgumentError
|
|
render json: failed_json, status: 422
|
|
end
|
|
end
|
|
|
|
def change_timestamps
|
|
topic_id = params.require(:topic_id).to_i
|
|
timestamp = params.require(:timestamp).to_f
|
|
|
|
guardian.ensure_can_change_post_timestamps!
|
|
|
|
topic = Topic.with_deleted.find(topic_id)
|
|
previous_timestamp = topic.first_post.created_at
|
|
|
|
begin
|
|
TopicTimestampChanger.new(topic: topic, timestamp: timestamp).change!
|
|
|
|
StaffActionLogger.new(current_user).log_topic_timestamps_changed(
|
|
topic,
|
|
Time.zone.at(timestamp),
|
|
previous_timestamp,
|
|
)
|
|
|
|
render json: success_json
|
|
rescue ActiveRecord::RecordInvalid, TopicTimestampChanger::InvalidTimestampError
|
|
render json: failed_json, status: 422
|
|
end
|
|
end
|
|
|
|
def clear_pin
|
|
topic = Topic.find_by(id: params[:topic_id].to_i)
|
|
guardian.ensure_can_see!(topic)
|
|
topic.clear_pin_for(current_user)
|
|
render body: nil
|
|
end
|
|
|
|
def re_pin
|
|
topic = Topic.find_by(id: params[:topic_id].to_i)
|
|
guardian.ensure_can_see!(topic)
|
|
topic.re_pin_for(current_user)
|
|
render body: nil
|
|
end
|
|
|
|
def timings
|
|
allowed_params = topic_params
|
|
|
|
topic_id = allowed_params[:topic_id].to_i
|
|
topic_time = allowed_params[:topic_time].to_i
|
|
timings = allowed_params[:timings].to_h || {}
|
|
|
|
# ensure we capture current user for the block
|
|
user = current_user
|
|
|
|
hijack do
|
|
PostTiming.process_timings(
|
|
user,
|
|
topic_id,
|
|
topic_time,
|
|
timings.map { |post_number, t| [post_number.to_i, t.to_i] },
|
|
mobile: view_context.mobile_view?,
|
|
)
|
|
render body: nil
|
|
end
|
|
end
|
|
|
|
def feed
|
|
raise Discourse::NotFound if !Post.exists?(topic_id: params[:topic_id])
|
|
|
|
begin
|
|
@topic_view = TopicView.new(params[:topic_id])
|
|
rescue Discourse::NotLoggedIn
|
|
raise Discourse::NotFound
|
|
rescue Discourse::InvalidAccess => ex
|
|
deleted =
|
|
guardian.can_see_topic?(ex.obj, false) ||
|
|
(!guardian.can_see_topic?(ex.obj) && ex.obj&.access_topic_via_group && ex.obj&.deleted_at)
|
|
|
|
raise Discourse::NotFound.new(
|
|
nil,
|
|
check_permalinks: deleted,
|
|
original_path: ex.obj.relative_url,
|
|
)
|
|
end
|
|
|
|
discourse_expires_in 1.minute
|
|
|
|
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
|
render "topics/show", formats: [:rss]
|
|
end
|
|
|
|
def bulk
|
|
if params[:topic_ids].present?
|
|
unless Array === params[:topic_ids]
|
|
raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids")
|
|
end
|
|
topic_ids = params[:topic_ids].map { |t| t.to_i }
|
|
elsif params[:filter] == "unread"
|
|
topic_ids = bulk_unread_topic_ids
|
|
else
|
|
raise ActionController::ParameterMissing.new(:topic_ids)
|
|
end
|
|
|
|
operation =
|
|
params
|
|
.require(:operation)
|
|
.permit(
|
|
:type,
|
|
:group,
|
|
:category_id,
|
|
:notification_level_id,
|
|
:message,
|
|
*DiscoursePluginRegistry.permitted_bulk_action_parameters,
|
|
tags: [],
|
|
)
|
|
.to_h
|
|
.symbolize_keys
|
|
|
|
raise ActionController::ParameterMissing.new(:operation_type) if operation[:type].blank?
|
|
operator = TopicsBulkAction.new(current_user, topic_ids, operation, group: operation[:group])
|
|
changed_topic_ids = operator.perform!
|
|
render_json_dump topic_ids: changed_topic_ids
|
|
end
|
|
|
|
def private_message_reset_new
|
|
topic_query = TopicQuery.new(current_user, limit: false)
|
|
|
|
if params[:topic_ids].present?
|
|
unless Array === params[:topic_ids]
|
|
raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids")
|
|
end
|
|
|
|
topic_scope =
|
|
topic_query.private_messages_for(current_user, :all).where(
|
|
"topics.id IN (?)",
|
|
params[:topic_ids].map(&:to_i),
|
|
)
|
|
else
|
|
params.require(:inbox)
|
|
inbox = params[:inbox].to_s
|
|
filter = private_message_filter(topic_query, inbox)
|
|
topic_scope = topic_query.filter_private_message_new(current_user, filter)
|
|
end
|
|
|
|
topic_ids =
|
|
TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform!
|
|
|
|
render json: success_json.merge(topic_ids: topic_ids)
|
|
end
|
|
|
|
def reset_new
|
|
topic_scope =
|
|
if current_user.new_new_view_enabled?
|
|
if (params[:dismiss_topics] && params[:dismiss_posts])
|
|
TopicQuery.new(current_user).new_and_unread_results(limit: false)
|
|
elsif params[:dismiss_topics]
|
|
TopicQuery.new(current_user).new_results(limit: false)
|
|
elsif params[:dismiss_posts]
|
|
TopicQuery.new(current_user).unread_results(limit: false)
|
|
else
|
|
Topic.none
|
|
end
|
|
else
|
|
TopicQuery.new(current_user).new_results(limit: false)
|
|
end
|
|
if tag_name = params[:tag_id]
|
|
tag_name = DiscourseTagging.visible_tags(guardian).where(name: tag_name).pluck(:name).first
|
|
end
|
|
|
|
topic_scope =
|
|
if params[:category_id].present?
|
|
category_id = params[:category_id].to_i
|
|
|
|
category_ids =
|
|
if ActiveModel::Type::Boolean.new.cast(params[:include_subcategories])
|
|
Category.subcategory_ids(category_id)
|
|
else
|
|
[category_id]
|
|
end
|
|
|
|
category_ids &= guardian.allowed_category_ids
|
|
if category_ids.blank?
|
|
scope = topic_scope.none
|
|
else
|
|
scope = topic_scope.where(category_id: category_ids)
|
|
scope = scope.joins(:tags).where(tags: { name: tag_name }) if tag_name
|
|
end
|
|
scope
|
|
elsif tag_name.present?
|
|
topic_scope.joins(:tags).where(tags: { name: tag_name })
|
|
else
|
|
if params[:tracked].to_s == "true"
|
|
TopicQuery.tracked_filter(topic_scope, current_user.id)
|
|
else
|
|
current_user.user_stat.update_column(:new_since, Time.zone.now)
|
|
topic_scope
|
|
end
|
|
end
|
|
|
|
if params[:topic_ids].present?
|
|
unless Array === params[:topic_ids]
|
|
raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids")
|
|
end
|
|
|
|
topic_ids = params[:topic_ids].map(&:to_i)
|
|
topic_scope = topic_scope.where(id: topic_ids)
|
|
end
|
|
|
|
dismissed_topic_ids = []
|
|
dismissed_post_topic_ids = []
|
|
|
|
if !current_user.new_new_view_enabled? || params[:dismiss_topics]
|
|
dismissed_topic_ids =
|
|
TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform!
|
|
end
|
|
|
|
if params[:dismiss_posts]
|
|
if params[:untrack]
|
|
dismissed_post_topic_ids =
|
|
TopicsBulkAction.new(
|
|
current_user,
|
|
topic_scope.pluck(:id),
|
|
type: "change_notification_level",
|
|
notification_level_id: NotificationLevels.topic_levels[:regular],
|
|
).perform!
|
|
else
|
|
dismissed_post_topic_ids =
|
|
TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_posts").perform!
|
|
end
|
|
end
|
|
|
|
render_json_dump topic_ids: dismissed_topic_ids.concat(dismissed_post_topic_ids).uniq
|
|
end
|
|
|
|
def convert_topic
|
|
params.require(:id)
|
|
params.require(:type)
|
|
|
|
topic = Topic.find_by(id: params[:id])
|
|
guardian.ensure_can_convert_topic!(topic)
|
|
|
|
topic =
|
|
if params[:type] == "public"
|
|
topic.convert_to_public_topic(current_user, category_id: params[:category_id])
|
|
else
|
|
topic.convert_to_private_message(current_user)
|
|
end
|
|
|
|
topic.valid? ? render_topic_changes(topic) : render_json_error(topic)
|
|
end
|
|
|
|
def reset_bump_date
|
|
params.require(:id)
|
|
params.permit(:post_id)
|
|
|
|
guardian.ensure_can_update_bumped_at!
|
|
|
|
topic = Topic.find_by(id: params[:id])
|
|
raise Discourse::NotFound.new unless topic
|
|
|
|
topic.reset_bumped_at(params[:post_id])
|
|
render body: nil
|
|
end
|
|
|
|
def set_slow_mode
|
|
topic = Topic.find(params[:topic_id])
|
|
slow_mode_type = TopicTimer.types[:clear_slow_mode]
|
|
timer = TopicTimer.find_by(topic: topic, status_type: slow_mode_type)
|
|
|
|
guardian.ensure_can_moderate!(topic)
|
|
topic.update!(slow_mode_seconds: params[:seconds])
|
|
enabled = params[:seconds].to_i > 0
|
|
|
|
time = enabled && params[:enabled_until].present? ? params[:enabled_until] : nil
|
|
|
|
topic.set_or_create_timer(slow_mode_type, time, by_user: timer&.user)
|
|
|
|
StaffActionLogger.new(current_user).log_topic_slow_mode(
|
|
topic,
|
|
enabled:,
|
|
seconds: params[:seconds],
|
|
until: time,
|
|
)
|
|
|
|
head :ok
|
|
end
|
|
|
|
private
|
|
|
|
def topic_params
|
|
params.permit(:topic_id, :topic_time, timings: {})
|
|
end
|
|
|
|
def fetch_topic_view(options)
|
|
if (username_filters = params[:username_filters]).present?
|
|
options[:username_filters] = username_filters.split(",")
|
|
end
|
|
|
|
@topic_view = TopicView.new(params[:topic_id], current_user, options)
|
|
end
|
|
|
|
def toggle_mute
|
|
@topic = Topic.find_by(id: params[:topic_id].to_i)
|
|
guardian.ensure_can_see!(@topic)
|
|
|
|
@topic.toggle_mute(current_user)
|
|
render body: nil
|
|
end
|
|
|
|
def consider_user_for_promotion
|
|
Promotion.new(current_user).review if current_user.present?
|
|
end
|
|
|
|
def should_bypass_bump?(changes)
|
|
(changes[:category_id].present? && SiteSetting.disable_category_edit_notifications) ||
|
|
(changes[:tags].present? && SiteSetting.disable_tags_edit_notifications)
|
|
end
|
|
|
|
def slugs_do_not_match
|
|
if SiteSetting.slug_generation_method != "encoded"
|
|
params[:slug] && @topic_view.topic.slug != params[:slug]
|
|
else
|
|
params[:slug] && CGI.unescape(@topic_view.topic.slug) != params[:slug]
|
|
end
|
|
end
|
|
|
|
def redirect_to_correct_topic(topic, post_number = nil)
|
|
begin
|
|
guardian.ensure_can_see!(topic)
|
|
rescue Discourse::InvalidAccess => ex
|
|
raise(SiteSetting.detailed_404 ? ex : Discourse::NotFound)
|
|
end
|
|
|
|
# Allow plugins to append allowed query parameters, so they aren't scrubbed on redirect to proper topic URL
|
|
additional_allowed_query_parameters =
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
:redirect_to_correct_topic_additional_query_parameters,
|
|
[],
|
|
)
|
|
|
|
opts =
|
|
params.slice(
|
|
*%i[page print filter_top_level_replies preview_theme_id].concat(
|
|
additional_allowed_query_parameters,
|
|
),
|
|
)
|
|
opts.delete(:page) if params[:page] == 0
|
|
|
|
url = topic.relative_url
|
|
url << "/#{post_number}" if post_number.to_i > 0
|
|
url << ".json" if request.format.json?
|
|
|
|
opts.each do |k, v|
|
|
s = url.include?("?") ? "&" : "?"
|
|
url << "#{s}#{k}=#{v}"
|
|
end
|
|
|
|
redirect_to url, status: 301
|
|
end
|
|
|
|
def track_visit_to_topic
|
|
topic_id = @topic_view.topic.id
|
|
ip = request.remote_ip
|
|
user_id = (current_user.id if current_user)
|
|
|
|
if !request.format.json?
|
|
hash = {
|
|
referer: request.referer || flash[:referer],
|
|
host: request.host,
|
|
current_user: current_user,
|
|
topic_id: @topic_view.topic.id,
|
|
post_number: @topic_view.current_post_number,
|
|
username: request["u"],
|
|
ip_address: request.remote_ip,
|
|
}
|
|
# defer this way so we do not capture the whole controller
|
|
# in the closure
|
|
TopicsController.defer_add_incoming_link(hash)
|
|
end
|
|
|
|
TopicsController.defer_track_visit(topic_id, user_id) if should_track_visit_to_topic?
|
|
end
|
|
|
|
def self.defer_track_visit(topic_id, user_id)
|
|
Scheduler::Defer.later "Track Visit" do
|
|
TopicUser.track_visit!(topic_id, user_id)
|
|
end
|
|
end
|
|
|
|
def self.defer_topic_view(topic_id, ip, user_id = nil)
|
|
Scheduler::Defer.later "Topic View" do
|
|
topic = Topic.find_by(id: topic_id)
|
|
return if topic.blank?
|
|
|
|
# We need to make sure that we aren't allowing recording
|
|
# random topic views against topics the user cannot see.
|
|
user = User.find_by(id: user_id) if user_id.present?
|
|
return if user_id.present? && user.blank?
|
|
return if !Guardian.new(user).can_see_topic?(topic)
|
|
|
|
TopicViewItem.add(topic_id, ip, user_id)
|
|
end
|
|
end
|
|
|
|
def self.defer_add_incoming_link(hash)
|
|
Scheduler::Defer.later "Track Link" do
|
|
IncomingLink.add(hash)
|
|
end
|
|
end
|
|
|
|
def should_track_visit_to_topic?
|
|
!!((!request.format.json? || params[:track_visit]) && current_user)
|
|
end
|
|
|
|
def perform_show_response
|
|
if request.head?
|
|
head :ok
|
|
return
|
|
end
|
|
|
|
topic_view_serializer =
|
|
TopicViewSerializer.new(
|
|
@topic_view,
|
|
scope: guardian,
|
|
root: false,
|
|
include_raw: !!params[:include_raw],
|
|
exclude_suggested_and_related:
|
|
!!params[:replies_to_post_number] || !!params[:filter_upwards_post_id] ||
|
|
!!params[:filter_top_level_replies],
|
|
)
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
@tags = SiteSetting.tagging_enabled ? @topic_view.topic.tags.visible(guardian) : []
|
|
@breadcrumbs = helpers.categories_breadcrumb(@topic_view.topic) || []
|
|
@description_meta =
|
|
@topic_view.topic.excerpt.present? ? @topic_view.topic.excerpt : @topic_view.summary
|
|
store_preloaded("topic_#{@topic_view.topic.id}", MultiJson.dump(topic_view_serializer))
|
|
render :show
|
|
end
|
|
|
|
format.json { render_json_dump(topic_view_serializer) }
|
|
end
|
|
end
|
|
|
|
def render_topic_changes(dest_topic)
|
|
if dest_topic.present?
|
|
render json: { success: true, url: dest_topic.relative_url }
|
|
else
|
|
render json: { success: false }
|
|
end
|
|
end
|
|
|
|
def move_posts_to_destination(topic)
|
|
args = {}
|
|
args[:title] = params[:title] if params[:title].present?
|
|
args[:destination_topic_id] = params[:destination_topic_id].to_i if params[
|
|
:destination_topic_id
|
|
].present?
|
|
args[:tags] = params[:tags] if params[:tags].present?
|
|
args[:chronological_order] = params[:chronological_order] == "true"
|
|
|
|
if params[:archetype].present?
|
|
args[:archetype] = params[:archetype]
|
|
args[:participants] = params[:participants] if params[:participants].present? &&
|
|
params[:archetype] == "private_message"
|
|
else
|
|
args[:category_id] = params[:category_id].to_i if params[:category_id].present?
|
|
end
|
|
|
|
topic.move_posts(current_user, post_ids_including_replies, args)
|
|
end
|
|
|
|
def check_for_status_presence(key, attr)
|
|
invalid_param(key) if %w[pinned pinned_globally visible closed archived].exclude?(attr)
|
|
end
|
|
|
|
def invalid_param(key)
|
|
raise Discourse::InvalidParameters.new(key.to_sym)
|
|
end
|
|
|
|
def fetch_username
|
|
params.require(:user)
|
|
params[:user]
|
|
end
|
|
|
|
def fetch_email
|
|
params.require(:email)
|
|
params[:email]
|
|
end
|
|
|
|
def pm_has_slots?(pm)
|
|
guardian.is_staff? || !pm.reached_recipients_limit?
|
|
end
|
|
|
|
def bulk_unread_topic_ids
|
|
topic_query = TopicQuery.new(current_user)
|
|
|
|
if inbox = params[:private_message_inbox]
|
|
filter = private_message_filter(topic_query, inbox)
|
|
topic_query.options[:limit] = false
|
|
topics = topic_query.filter_private_messages_unread(current_user, filter)
|
|
else
|
|
topics =
|
|
TopicQuery.unread_filter(
|
|
topic_query.joined_topic_user,
|
|
whisperer: guardian.is_whisperer?,
|
|
).listable_topics
|
|
|
|
topics = TopicQuery.tracked_filter(topics, current_user.id) if params[:tracked].to_s == "true"
|
|
|
|
if params[:category_id]
|
|
category_ids =
|
|
if params[:include_subcategories]
|
|
Category.subcategory_ids(params[:category_id].to_i)
|
|
else
|
|
params[:category_id]
|
|
end
|
|
|
|
topics = topics.where(category_id: category_ids)
|
|
end
|
|
|
|
if params[:tag_name].present?
|
|
topics = topics.joins(:tags).where("tags.name": params[:tag_name])
|
|
end
|
|
end
|
|
|
|
topics.pluck(:id)
|
|
end
|
|
|
|
def private_message_filter(topic_query, inbox)
|
|
case inbox
|
|
when "group"
|
|
group_name = params[:group_name]
|
|
group = Group.find_by("lower(name) = ?", group_name)
|
|
raise Discourse::NotFound if !group
|
|
raise Discourse::NotFound if !guardian.can_see_group_messages?(group)
|
|
topic_query.options[:group_name] = group_name
|
|
:group
|
|
when "user"
|
|
:user
|
|
else
|
|
:all
|
|
end
|
|
end
|
|
end
|