2024-02-25 12:15:55 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-06-14 22:27:53 -04:00
|
|
|
class PostReplacement < ApplicationRecord
|
2020-06-11 20:55:37 -04:00
|
|
|
self.table_name = 'post_replacements2'
|
2017-05-12 18:46:36 -04:00
|
|
|
belongs_to :post
|
|
|
|
belongs_to :creator, class_name: "User"
|
2020-06-11 20:55:37 -04:00
|
|
|
belongs_to :approver, class_name: "User", optional: true
|
2021-06-26 08:01:07 -04:00
|
|
|
belongs_to :uploader_on_approve, class_name: "User", foreign_key: :uploader_id_on_approve, optional: true
|
2024-07-25 13:26:05 -04:00
|
|
|
attr_accessor :replacement_file, :replacement_url, :tags, :is_backup, :as_pending, :is_destroyed_reupload
|
2020-05-10 18:07:43 -04:00
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
validate :user_is_not_limited, on: :create
|
|
|
|
validate :post_is_valid, on: :create
|
2020-05-10 18:07:43 -04:00
|
|
|
validate :set_file_name, on: :create
|
|
|
|
validate :fetch_source_file, on: :create
|
|
|
|
validate :update_file_attributes, on: :create
|
2022-05-08 14:47:11 -04:00
|
|
|
validate on: :create do |replacement|
|
|
|
|
FileValidator.new(replacement, replacement_file.path).validate
|
2022-05-09 13:52:44 -04:00
|
|
|
throw :abort if errors.any?
|
2022-05-08 14:47:11 -04:00
|
|
|
end
|
2020-06-11 20:55:37 -04:00
|
|
|
validate :no_pending_duplicates, on: :create
|
2020-05-10 18:07:43 -04:00
|
|
|
validate :write_storage_file, on: :create
|
2022-08-20 11:21:42 -04:00
|
|
|
validates :reason, length: { in: 5..150 }, presence: true, on: :create
|
2020-05-10 18:07:43 -04:00
|
|
|
|
2021-10-05 15:14:00 -04:00
|
|
|
after_create -> { post.update_index }
|
2020-05-10 18:07:43 -04:00
|
|
|
before_destroy :remove_files
|
2021-10-05 15:14:00 -04:00
|
|
|
after_destroy -> { post.update_index }
|
2020-05-10 18:07:43 -04:00
|
|
|
|
2022-09-25 07:05:33 -04:00
|
|
|
TAGS_TO_REMOVE_AFTER_ACCEPT = ["better_version_at_source"]
|
|
|
|
HIGHLIGHTED_TAGS = ["better_version_at_source", "avoid_posting", "conditional_dnp"]
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
def replacement_url_parsed
|
|
|
|
return nil unless replacement_url =~ %r!\Ahttps?://!i
|
|
|
|
Addressable::URI.heuristic_parse(replacement_url) rescue nil
|
2017-05-12 18:46:36 -04:00
|
|
|
end
|
|
|
|
|
2024-07-13 20:05:50 -04:00
|
|
|
def notify_reupload
|
|
|
|
return unless is_destroyed_reupload
|
|
|
|
if (destroyed_post = DestroyedPost.find_by(md5: md5))
|
|
|
|
destroyed_post.notify_reupload(creator, replacement_post_id: post_id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
module PostMethods
|
|
|
|
def post_is_valid
|
|
|
|
if post.is_deleted?
|
|
|
|
self.errors.add(:post, "is deleted")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def no_pending_duplicates
|
2020-11-09 03:37:22 -05:00
|
|
|
return true if is_backup
|
2022-10-19 18:13:52 -04:00
|
|
|
|
2024-07-13 20:05:50 -04:00
|
|
|
if DestroyedPost.find_by(md5: md5)
|
|
|
|
errors.add(:base, "That image had been deleted from our site, and cannot be re-uploaded")
|
|
|
|
self.is_destroyed_reupload = true
|
2022-10-19 18:13:52 -04:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
post = Post.where(md5: md5).first
|
|
|
|
if post
|
|
|
|
self.errors.add(:md5, "duplicate of existing post ##{post.id}")
|
|
|
|
return false
|
|
|
|
end
|
2020-11-05 15:05:37 -05:00
|
|
|
replacements = PostReplacement.where(status: 'pending', md5: md5)
|
2020-06-11 20:55:37 -04:00
|
|
|
replacements.each do |replacement|
|
|
|
|
self.errors.add(:md5, "duplicate of pending replacement on post ##{replacement.post_id}")
|
|
|
|
end
|
|
|
|
replacements.size == 0
|
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
def user_is_not_limited
|
|
|
|
return true if status == 'original'
|
|
|
|
uploadable = creator.can_upload_with_reason
|
|
|
|
if uploadable != true
|
|
|
|
self.errors.add(:creator, User.upload_reason_string(uploadable))
|
2022-03-06 07:05:09 -05:00
|
|
|
throw :abort
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
2020-06-11 20:55:37 -04:00
|
|
|
|
|
|
|
# Janitor bypass replacement limits
|
|
|
|
return true if creator.is_janitor?
|
|
|
|
|
2020-11-29 21:50:38 -05:00
|
|
|
if post.replacements.where(creator_id: creator.id).where('created_at > ?', 1.day.ago).count >= Danbooru.config.post_replacement_per_day_limit
|
2020-11-29 01:01:40 -05:00
|
|
|
self.errors.add(:creator, 'has already suggested too many replacements for this post today')
|
2022-03-06 07:05:09 -05:00
|
|
|
throw :abort
|
2020-06-11 20:55:37 -04:00
|
|
|
end
|
2020-11-29 21:50:38 -05:00
|
|
|
if post.replacements.where(creator_id: creator.id).count >= Danbooru.config.post_replacement_per_post_limit
|
2020-06-11 20:55:37 -04:00
|
|
|
self.errors.add(:creator, 'has already suggested too many total replacements for this post')
|
2022-03-06 07:05:09 -05:00
|
|
|
throw :abort
|
2020-06-11 20:55:37 -04:00
|
|
|
end
|
2020-05-10 18:07:43 -04:00
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2021-03-21 17:40:10 -04:00
|
|
|
def source_list
|
|
|
|
source.split("\n").uniq.reject(&:blank?)
|
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
module StorageMethods
|
|
|
|
def remove_files
|
2022-01-08 19:47:06 -05:00
|
|
|
PostEvent.add(post_id, CurrentUser.user, :replacement_deleted, { replacement_id: id, md5: md5, storage_id: storage_id})
|
2020-05-10 18:07:43 -04:00
|
|
|
Danbooru.config.storage_manager.delete_replacement(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
def fetch_source_file
|
|
|
|
return if replacement_file.present?
|
|
|
|
|
2022-02-05 07:52:25 -05:00
|
|
|
valid, reason = UploadWhitelist.is_whitelisted?(replacement_url_parsed)
|
|
|
|
if !valid
|
|
|
|
self.errors.add(:replacement_url, "is not whitelisted: #{reason}")
|
|
|
|
throw :abort
|
|
|
|
end
|
|
|
|
|
2022-03-16 12:39:58 -04:00
|
|
|
download = Downloads::File.new(replacement_url_parsed)
|
2022-03-16 12:15:45 -04:00
|
|
|
file = download.download!
|
2020-05-10 18:07:43 -04:00
|
|
|
|
|
|
|
self.replacement_file = file
|
2021-10-24 07:07:53 -04:00
|
|
|
self.source = "#{self.source}\n" + replacement_url
|
2021-03-21 17:40:10 -04:00
|
|
|
rescue Downloads::File::Error
|
|
|
|
self.errors.add(:replacement_url, "failed to fetch file")
|
|
|
|
throw :abort
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def update_file_attributes
|
2022-05-08 13:05:12 -04:00
|
|
|
self.file_ext = file_header_to_file_ext(replacement_file.path)
|
2020-05-10 18:07:43 -04:00
|
|
|
self.file_size = replacement_file.size
|
|
|
|
self.md5 = Digest::MD5.file(replacement_file.path).hexdigest
|
2022-05-08 13:05:12 -04:00
|
|
|
width, height = calculate_dimensions(replacement_file.path)
|
|
|
|
self.image_width = width
|
|
|
|
self.image_height = height
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def set_file_name
|
|
|
|
if replacement_file.present?
|
|
|
|
self.file_name = replacement_file.try(:original_filename) || File.basename(replacement_file.path)
|
|
|
|
else
|
2022-02-05 07:52:25 -05:00
|
|
|
if replacement_url_parsed.blank? && replacement_url.present?
|
|
|
|
self.errors.add(:replacement_url, "is invalid")
|
|
|
|
throw :abort
|
|
|
|
end
|
2020-06-11 20:55:37 -04:00
|
|
|
if replacement_url_parsed.blank?
|
2022-02-05 07:52:25 -05:00
|
|
|
self.errors.add(:base, "No file or replacement URL provided")
|
2020-06-11 20:55:37 -04:00
|
|
|
throw :abort
|
|
|
|
end
|
2020-05-10 18:07:43 -04:00
|
|
|
self.file_name = replacement_url_parsed.basename
|
|
|
|
end
|
|
|
|
end
|
2017-05-12 18:46:36 -04:00
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
def write_storage_file
|
|
|
|
self.storage_id = SecureRandom.hex(16)
|
|
|
|
Danbooru.config.storage_manager.store_replacement(replacement_file, self, :original)
|
|
|
|
thumbnail_file = PostThumbnailer.generate_thumbnail(replacement_file, is_video? ? :video : :image)
|
2020-11-29 01:01:40 -05:00
|
|
|
Danbooru.config.storage_manager.store_replacement(thumbnail_file, self, :preview)
|
2020-05-10 18:07:43 -04:00
|
|
|
ensure
|
|
|
|
thumbnail_file.try(:close!)
|
|
|
|
end
|
|
|
|
|
2020-11-29 01:01:40 -05:00
|
|
|
def replacement_file_path
|
|
|
|
Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)
|
|
|
|
end
|
|
|
|
|
|
|
|
def replacement_thumb_path
|
|
|
|
Danbooru.config.storage_manager.replacement_path(self, file_ext, :preview)
|
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
def replacement_file_url
|
|
|
|
Danbooru.config.storage_manager.replacement_url(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
def replacement_thumb_url
|
2020-11-29 01:01:40 -05:00
|
|
|
Danbooru.config.storage_manager.replacement_url(self, :preview)
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
module ApiMethods
|
|
|
|
def hidden_attributes
|
2021-10-18 11:53:07 -04:00
|
|
|
super + %i[storage_id protected uploader_id_on_approve penalize_uploader_on_approve]
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
module ProcessingMethods
|
2021-06-26 07:01:26 -04:00
|
|
|
def approve!(penalize_current_uploader:)
|
2021-06-26 09:05:44 -04:00
|
|
|
unless ["pending", "original"].include? status
|
|
|
|
errors.add(:status, "must be pending or original to approve")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-03-30 13:34:31 -04:00
|
|
|
processor = UploadService::Replacer.new(post: post, replacement: self)
|
|
|
|
processor.process!(penalize_current_uploader: penalize_current_uploader)
|
|
|
|
PostEvent.add(post.id, CurrentUser.user, :replacement_accepted, { replacement_id: id, old_md5: post.md5, new_md5: md5 })
|
2021-10-05 15:14:00 -04:00
|
|
|
post.update_index
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
|
2021-06-26 08:01:07 -04:00
|
|
|
def toggle_penalize!
|
2021-06-26 09:05:44 -04:00
|
|
|
if status != "approved"
|
|
|
|
errors.add(:status, "must be approved to penalize")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2021-06-26 08:01:07 -04:00
|
|
|
if penalize_uploader_on_approve
|
|
|
|
UserStatus.for_user(uploader_on_approve).update_all("own_post_replaced_penalize_count = own_post_replaced_penalize_count - 1")
|
|
|
|
else
|
|
|
|
UserStatus.for_user(uploader_on_approve).update_all("own_post_replaced_penalize_count = own_post_replaced_penalize_count + 1")
|
|
|
|
end
|
|
|
|
update_attribute(:penalize_uploader_on_approve, !penalize_uploader_on_approve)
|
|
|
|
end
|
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
def promote!
|
2021-06-26 09:05:44 -04:00
|
|
|
if status != "pending"
|
|
|
|
errors.add(:status, "must be pending to promote")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-01-31 01:58:33 -05:00
|
|
|
upload = transaction do
|
2020-06-11 20:55:37 -04:00
|
|
|
processor = UploadService.new(new_upload_params)
|
2021-06-26 09:05:44 -04:00
|
|
|
new_upload = processor.start!
|
2022-09-12 15:18:41 -04:00
|
|
|
if new_upload.valid? && new_upload.post&.valid?
|
|
|
|
update_attribute(:status, "promoted")
|
2023-02-03 13:47:52 -05:00
|
|
|
PostEvent.add(new_upload.post.id, CurrentUser.user, :replacement_promoted, { source_post_id: post.id })
|
2022-09-12 15:18:41 -04:00
|
|
|
end
|
2021-06-26 09:05:44 -04:00
|
|
|
new_upload
|
2020-06-11 20:55:37 -04:00
|
|
|
end
|
2021-10-05 15:14:00 -04:00
|
|
|
post.update_index
|
2022-01-31 01:58:33 -05:00
|
|
|
upload
|
2020-06-11 20:55:37 -04:00
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
def reject!
|
2021-06-26 09:05:44 -04:00
|
|
|
if status != "pending"
|
|
|
|
errors.add(:status, "must be pending to reject")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-01-08 19:47:06 -05:00
|
|
|
PostEvent.add(post.id, CurrentUser.user, :replacement_rejected, { replacement_id: id })
|
2020-05-10 18:07:43 -04:00
|
|
|
update_attribute(:status, 'rejected')
|
2021-06-26 07:01:26 -04:00
|
|
|
UserStatus.for_user(creator_id).update_all("post_replacement_rejected_count = post_replacement_rejected_count + 1")
|
2021-10-05 15:14:00 -04:00
|
|
|
post.update_index
|
2020-05-10 18:07:43 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
module PromotionMethods
|
|
|
|
def new_upload_params
|
|
|
|
{
|
|
|
|
uploader_id: creator_id,
|
|
|
|
uploader_ip_addr: creator_ip_addr,
|
|
|
|
file: Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)),
|
|
|
|
tag_string: post.tag_string,
|
|
|
|
rating: post.rating,
|
2021-03-21 17:40:10 -04:00
|
|
|
source: "#{self.source}\n" + post.source,
|
2020-06-11 20:55:37 -04:00
|
|
|
parent_id: post.id,
|
2024-10-30 19:52:53 -04:00
|
|
|
title: post.title,
|
2020-06-11 20:55:37 -04:00
|
|
|
description: post.description,
|
2024-11-10 14:58:15 -05:00
|
|
|
transcript: post.transcript,
|
2020-06-11 20:55:37 -04:00
|
|
|
locked_tags: post.locked_tags,
|
|
|
|
replacement_id: self.id
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
concerning :Search do
|
|
|
|
class_methods do
|
2023-06-02 10:15:17 -04:00
|
|
|
def search(params)
|
2018-06-09 16:38:06 -04:00
|
|
|
q = super
|
2017-06-09 00:53:52 -04:00
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
q = q.attribute_exact_matches(:file_ext, params[:file_ext])
|
|
|
|
q = q.attribute_exact_matches(:md5, params[:md5])
|
|
|
|
q = q.attribute_exact_matches(:status, params[:status])
|
2018-08-31 20:23:25 -04:00
|
|
|
|
2023-08-03 16:01:53 -04:00
|
|
|
q = q.where_user(:creator_id, :creator, params)
|
|
|
|
q = q.where_user(:approver_id, :approver, params)
|
2023-08-11 10:19:34 -04:00
|
|
|
q = q.where_user(:uploader_id_on_approve, %i[uploader_name_on_approve uploader_id_on_approve], params)
|
2021-06-26 07:01:26 -04:00
|
|
|
|
2018-06-09 16:38:06 -04:00
|
|
|
if params[:post_id].present?
|
2021-06-27 13:04:25 -04:00
|
|
|
q = q.where("post_id in (?)", params[:post_id].split(",").first(100).map(&:to_i))
|
2018-06-09 16:38:06 -04:00
|
|
|
end
|
2017-05-14 19:49:57 -04:00
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
q.order(Arel.sql("CASE status WHEN 'pending' THEN 0 ELSE 1 END ASC, id DESC"))
|
2017-05-14 19:49:57 -04:00
|
|
|
end
|
2020-05-10 18:07:43 -04:00
|
|
|
|
|
|
|
def pending
|
|
|
|
where(status: 'pending')
|
|
|
|
end
|
|
|
|
|
|
|
|
def rejected
|
|
|
|
where(status: 'rejected')
|
|
|
|
end
|
|
|
|
|
|
|
|
def approved
|
|
|
|
where(status: 'approved')
|
|
|
|
end
|
|
|
|
|
|
|
|
def for_user(id)
|
|
|
|
where(creator_id: id.to_i)
|
|
|
|
end
|
2020-06-11 20:55:37 -04:00
|
|
|
|
2021-06-26 18:21:01 -04:00
|
|
|
def for_uploader_on_approve(id)
|
|
|
|
where(uploader_id_on_approve: id.to_i)
|
|
|
|
end
|
|
|
|
|
|
|
|
def penalized
|
|
|
|
where(penalize_uploader_on_approve: true)
|
|
|
|
end
|
|
|
|
|
|
|
|
def not_penalized
|
|
|
|
where(penalize_uploader_on_approve: false)
|
|
|
|
end
|
|
|
|
|
2020-06-11 20:55:37 -04:00
|
|
|
def visible(user)
|
|
|
|
return where('status != ?', 'rejected') if user.is_anonymous?
|
|
|
|
return all if user.is_janitor?
|
|
|
|
where('creator_id = ? or status != ?', user.id, 'rejected')
|
|
|
|
end
|
2017-05-14 19:49:57 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-11 16:23:12 -04:00
|
|
|
def original_file_visible_to?(user)
|
|
|
|
user.is_janitor?
|
2018-06-09 16:38:06 -04:00
|
|
|
end
|
|
|
|
|
2024-07-25 13:26:05 -04:00
|
|
|
def upload_as_pending?
|
|
|
|
as_pending.to_s.truthy?
|
|
|
|
end
|
|
|
|
|
2020-05-10 18:07:43 -04:00
|
|
|
include ApiMethods
|
|
|
|
include StorageMethods
|
|
|
|
include FileMethods
|
|
|
|
include ProcessingMethods
|
2020-06-11 20:55:37 -04:00
|
|
|
include PromotionMethods
|
|
|
|
include PostMethods
|
2017-05-12 18:46:36 -04:00
|
|
|
end
|