0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 11:52:32 +08:00
discourse/app/services/destroy_task.rb
Alan Guo Xiang Tan 395077a745
PERF: Use daily rollups for DAU/MAU reports (#41958)
DAU/MAU reports previously calculated every requested date from the full
user visit history, so dashboard requests became slower as that history
grew.

Persist the daily DAU and trailing-30-day MAU values and have the shared
report read only those rollups. The scheduled job builds all existing
history on its first successful run, then refreshes yesterday and today
every three hours. Replacements are atomic, so a failed calculation
leaves the previous report data intact.
2026-07-24 09:52:40 +08:00

179 lines
6.1 KiB
Ruby
Vendored

# frozen_string_literal: true
require "highline"
require "io/console"
class DestroyTask
def initialize(io = STDOUT)
@io = io
@tty = IO.console
@hl = HighLine.new(@tty, @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,
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
).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,
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
).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)
if !SiteSetting.can_permanently_delete
@io.puts "The can_permanently_delete site setting needs to be enabled to destroy posts."
return
end
posts = Post.with_deleted.where(id: post_ids)
@io.puts(<<~NOTICE)
There are #{posts.count} posts to delete.
Note: If a post is the first post in a topic, all posts
in that topic will be deleted.
NOTICE
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 = @hl.ask("Are you sure? (Y/n)")
exit 1 if confirm_destroy.downcase != "y"
end
# Do first posts before the rest. The PostDestroyer will destroy
# all posts following a first post automatically, and we want to
# avoid duplicate audit trail entries by trying to delete them
# again. Note: Rails batching will order by pkey (post ID in this
# case) internally, and can not accept additional ordering options.
[posts.where(post_number: 1), posts].each do |scope|
scope.find_each(order: :desc) 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
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,
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
).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|
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
def destroy_stats
ApplicationRequest.delete_all
IncomingLink.delete_all
UserVisit.delete_all
UserVisitDailyRollup.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