Files
discourse/app/services/destroy_task.rb
Ted Johansson 42c4427cb1 DEV: Add rake task to bulk delete posts (#31576)
This PR adds a destroy:posts rake task that can be used to hard-delete a list of posts. Useful for dealing with large amounts of spam that has been soft deleted and needs to go.

Notes:

Works on both non-deleted and soft-deleted posts. (We might want to change this to work on only soft-deleted posts?)
Works exclusively on post IDs. We can't mix topic and post IDs as they might clash, and we have no way of resolving that ambiguity.
Accepts either a rake-style array of IDs or, more conveniently, you can pipe the argument in through STDIN.
Added a confirmation step since it's a fairly destructive operation.
2025-03-04 17:29:38 +08:00

149 lines
5.0 KiB
Ruby

# frozen_string_literal: true
require "highline/import"
class DestroyTask
def initialize(io = STDOUT)
@io = io
end
def destroy_topics(category, parent_category = nil, delete_system_topics = false)
c = Category.find_by_slug(category, parent_category)
descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category
return @io.puts "A category with the slug: #{descriptive_slug} could not be found" if c.nil?
if delete_system_topics
topics = Topic.where(category_id: c.id, pinned_at: nil)
else
topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1)
end
@io.puts "There are #{topics.count} topics to delete in #{descriptive_slug} category"
topics.find_each do |topic|
@io.puts "Deleting #{topic.slug}..."
first_post = topic.ordered_posts.first
return @io.puts "Topic.ordered_posts.first was nil" if first_post.nil?
@io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy
end
end
def destroy_topics_in_category(category_id, delete_system_topics = false)
c = Category.find(category_id)
return @io.puts "A category with the id: #{category_id} could not be found" if c.nil?
if delete_system_topics
topics = Topic.where(category_id: c.id, pinned_at: nil)
else
topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1)
end
@io.puts "There are #{topics.count} topics to delete in #{c.slug} category"
topics.find_each do |topic|
first_post = topic.ordered_posts.first
return @io.puts "Topic.ordered_posts.first was nil for topic: #{topic.id}" if first_post.nil?
PostDestroyer.new(Discourse.system_user, first_post).destroy
end
topics = Topic.where(category_id: c.id, pinned_at: nil)
@io.puts "There are #{topics.count} topics that could not be deleted in #{c.slug} category"
end
def destroy_topics_all_categories
categories = Category.all
categories.each { |c| @io.puts destroy_topics(c.slug, c.parent_category&.slug) }
end
def destroy_posts(post_ids, require_confirmation: true)
posts = Post.with_deleted.where(id: post_ids)
@io.puts "There are #{posts.count} posts to delete"
if posts.count < post_ids.size
@io.puts "Couldn't find the following posts:"
@io.puts " #{post_ids.map(&:to_i) - posts.pluck(:id)}"
end
if require_confirmation
confirm_destroy = ask("Are you sure? (Y/n)")
exit 1 if confirm_destroy.downcase != "y"
end
posts.find_each do |post|
@io.puts "Destroying post #{post.id}"
@io.puts PostDestroyer.new(
Discourse.system_user,
post,
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
force_destroy: true,
).destroy
end
end
def destroy_private_messages
Topic
.where(archetype: "private_message")
.find_each do |pm|
@io.puts "Destroying #{pm.slug} pm"
first_post = pm.ordered_posts.first
@io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy
end
end
def destroy_category(category_id, destroy_system_topics = false)
c = Category.find_by_id(category_id)
return @io.puts "A category with the id: #{category_id} could not be found" if c.nil?
subcategories = Category.where(parent_category_id: c.id)
@io.puts "There are #{subcategories.count} subcategories to delete" if subcategories
subcategories.each { |s| category_topic_destroyer(s, destroy_system_topics) }
category_topic_destroyer(c, destroy_system_topics)
end
def destroy_groups
groups = Group.where(automatic: false)
groups.each do |group|
@io.puts "destroying group: #{group.id}"
@io.puts group.destroy
end
end
def destroy_users
User
.human_users
.where(admin: false)
.find_each do |user|
begin
if UserDestroyer.new(Discourse.system_user).destroy(
user,
delete_posts: true,
context: "destroy task",
)
@io.puts "#{user.username} deleted"
else
@io.puts "#{user.username} not deleted"
end
rescue UserDestroyer::PostsExistError
raise Discourse::InvalidAccess.new(
"User #{user.username} has #{user.post_count} posts, so can't be deleted.",
)
rescue NoMethodError
@io.puts "#{user.username} could not be deleted"
rescue Discourse::InvalidAccess => e
@io.puts "#{user.username} #{e.message}"
end
end
end
def destroy_stats
ApplicationRequest.delete_all
IncomingLink.delete_all
UserVisit.delete_all
UserProfileView.delete_all
UserProfile.update_all(views: 0)
PostAction.unscoped.delete_all
EmailLog.delete_all
end
private
def category_topic_destroyer(category, destroy_system_topics = false)
destroy_topics_in_category(category.id, destroy_system_topics)
@io.puts "Destroying #{category.slug} category"
category.destroy
end
end