Files
danbooru/app/models/forum_post.rb
evazion 56722df753 forum: delete posts when topic is deleted.
Fix it so that when a forum topic is deleted, all posts in the topic are
deleted too. Also make it so that when a forum topic is undeleted, all
posts in it are undeleted too.

Before when a topic was deleted, only the topic itself was marked as
deleted, not the posts inside the topic. This meant that when a spam
topic was deleted, the OP wouldn't be marked as deleted, so any
modreports against it wouldn't be marked as handled.

Also change it so that it's not possible to undelete a post in a deleted
topic, or to delete the OP of a topic without deleting the topic itself.

Finally, add a fix script to delete all active posts in deleted topics,
and to undelete all deleted OPs in active topics.
2022-01-21 22:35:20 -06:00

198 lines
6.2 KiB
Ruby

# frozen_string_literal: true
class ForumPost < ApplicationRecord
attr_readonly :topic_id
belongs_to :creator, class_name: "User"
belongs_to_updater
belongs_to :topic, class_name: "ForumTopic", inverse_of: :forum_posts
has_many :moderation_reports, as: :model
has_many :pending_moderation_reports, -> { pending }, as: :model, class_name: "ModerationReport"
has_many :votes, class_name: "ForumPostVote"
has_one :tag_alias
has_one :tag_implication
has_one :bulk_update_request
validates :body, presence: true, length: { maximum: 200_000 }, if: :body_changed?
validate :validate_deletion_of_original_post
validate :validate_undeletion_of_post
before_create :autoreport_spam
before_save :handle_reports_on_deletion
after_create :update_topic_updated_at_on_create
after_update :update_topic_updated_at_on_update_for_original_posts
after_destroy :update_topic_updated_at_on_destroy
after_update(:if => ->(rec) {rec.updater_id != rec.creator_id}) do |rec|
ModAction.log("#{CurrentUser.user.name} updated forum ##{rec.id}", :forum_post_update)
end
after_destroy(:if => ->(rec) {rec.updater_id != rec.creator_id}) do |rec|
ModAction.log("#{CurrentUser.user.name} deleted forum ##{rec.id}", :forum_post_delete)
end
after_create_commit :async_send_discord_notification
deletable
has_dtext_links :body
mentionable(
message_field: :body,
title: ->(_user_name) {%{#{creator.name} mentioned you in topic ##{topic_id} (#{topic.title})}},
body: ->(user_name) {%{@#{creator.name} mentioned you in topic ##{topic_id} ("#{topic.title}":[#{Routes.forum_topic_path(topic, page: forum_topic_page)}]):\n\n[quote]\n#{DText.extract_mention(body, "@#{user_name}")}\n[/quote]\n}}
)
module SearchMethods
def visible(user)
where(topic_id: ForumTopic.visible(user))
end
def not_visible(user)
where.not(topic_id: ForumTopic.visible(user))
end
def wiki_link_matches(title)
dtext_links = DtextLink.forum_post.wiki_link.where(link_target: WikiPage.normalize_title(title)).select(:model_id)
bur_links = BulkUpdateRequest.where_array_includes_any(:tags, title).select(:forum_post_id)
where(id: dtext_links).or(where(id: bur_links))
end
def search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :body, :creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request)
q = q.text_attribute_matches(:body, params[:body_matches])
if params[:linked_to].present?
q = q.wiki_link_matches(params[:linked_to])
end
q.apply_default_order(params)
end
end
extend SearchMethods
def self.new_reply(params)
if params[:topic_id]
new(:topic_id => params[:topic_id])
elsif params[:post_id]
forum_post = ForumPost.find(params[:post_id])
forum_post.build_response
else
new
end
end
def voted?(user, score)
votes.exists?(creator_id: user.id, score: score)
end
def validate_deletion_of_original_post
if is_original_post? && is_deleted? && !topic.is_deleted?
errors.add(:base, "Can't delete original post without deleting the topic first")
end
end
def validate_undeletion_of_post
if topic.is_deleted? && !is_deleted?
errors.add(:base, "Can't undelete post without undeleting the topic first")
end
end
def autoreport_spam
if SpamDetector.new(self, user_ip: CurrentUser.ip_addr).spam?
moderation_reports << ModerationReport.new(creator: User.system, reason: "Spam.")
end
end
def update_topic_updated_at_on_create
if topic
# need to do this to bypass the topic's original post from getting touched
ForumTopic.where(:id => topic.id).update_all(["updater_id = ?, response_count = response_count + 1, updated_at = ?", creator.id, Time.now])
topic.response_count += 1
end
end
def update_topic_updated_at_on_update_for_original_posts
if is_original_post?
topic.touch
end
end
def delete!
update(is_deleted: true)
update_topic_updated_at_on_delete
end
def undelete!
update(is_deleted: false)
update_topic_updated_at_on_undelete
end
def update_topic_updated_at_on_delete
max = ForumPost.where(:topic_id => topic.id, :is_deleted => false).order("updated_at desc").first
if max
ForumTopic.where(:id => topic.id).update_all(["updated_at = ?, updater_id = ?", max.updated_at, max.updater_id])
end
end
def update_topic_updated_at_on_undelete
if topic
ForumTopic.where(:id => topic.id).update_all(["updater_id = ?, updated_at = ?", CurrentUser.id, Time.now])
end
end
def update_topic_updated_at_on_destroy
max = ForumPost.where(:topic_id => topic.id, :is_deleted => false).order("updated_at desc").first
if max
ForumTopic.where(:id => topic.id).update_all(["response_count = response_count - 1, updated_at = ?, updater_id = ?", max.updated_at, max.updater_id])
else
ForumTopic.where(:id => topic.id).update_all("response_count = response_count - 1")
end
topic.response_count -= 1
end
def quoted_response
DText.quote(body, creator.name)
end
def forum_topic_page
(ForumPost.where("topic_id = ? and created_at <= ?", topic_id, created_at).count / Danbooru.config.posts_per_page.to_f).ceil
end
def is_original_post?(original_post_id = nil)
if original_post_id
id == original_post_id
else
ForumPost.exists?(["id = ? and id = (select _.id from forum_posts _ where _.topic_id = ? order by _.id asc limit 1)", id, topic_id])
end
end
def handle_reports_on_deletion
return unless moderation_reports.pending.present? && is_deleted_change == [false, true]
moderation_reports.pending.update!(status: :handled, updater: updater)
end
def async_send_discord_notification
DiscordNotificationJob.perform_later(forum_post: self)
end
def send_discord_notification
return unless policy(User.anonymous).show?
DiscordWebhookService.new.post_message(self)
end
def build_response
dup.tap do |x|
x.body = x.quoted_response
end
end
def dtext_shortlink(**_options)
"forum ##{id}"
end
def self.available_includes
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
end
end