forked from e621ng/e621ng
1795 lines
52 KiB
Ruby
1795 lines
52 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Post < ApplicationRecord
|
|
class RevertError < Exception ; end
|
|
class DeletionError < Exception ; end
|
|
class TimeoutError < Exception ; end
|
|
|
|
# Tags to copy when copying notes.
|
|
NOTE_COPY_TAGS = %w[translated partially_translated translation_check translation_request]
|
|
|
|
before_validation :initialize_uploader, :on => :create
|
|
before_validation :merge_old_changes
|
|
before_validation :apply_source_diff
|
|
before_validation :apply_tag_diff, if: :should_process_tags?
|
|
before_validation :normalize_tags, if: :should_process_tags?
|
|
before_validation :tag_count_not_insane, if: :should_process_tags?
|
|
before_validation :strip_source
|
|
before_validation :fix_bg_color
|
|
before_validation :blank_out_nonexistent_parents
|
|
before_validation :remove_parent_loops
|
|
normalizes :description, with: ->(desc) { desc.gsub("\r\n", "\n") }
|
|
validates :md5, uniqueness: { :on => :create, message: ->(obj, data) {"duplicate: #{Post.find_by_md5(obj.md5).id}"} }
|
|
validates :rating, inclusion: { in: %w(s q e), message: "rating must be s, q, or e" }
|
|
validates :bg_color, format: { with: /\A[A-Fa-f0-9]{6}\z/ }, allow_nil: true
|
|
validates :title, length: { maximum: Danbooru.config.post_title_max_size }, if: :title_changed?
|
|
validates :description, length: { maximum: Danbooru.config.post_descr_max_size }, if: :description_changed?
|
|
validates :transcript, length: { maximum: Danbooru.config.post_trasc_max_size }, if: :transcript_changed?
|
|
validate :added_tags_are_valid, if: :should_process_tags?
|
|
validate :removed_tags_are_valid, if: :should_process_tags?
|
|
validate :has_artist_tag, if: :should_process_tags?
|
|
validate :has_enough_tags, if: :should_process_tags?
|
|
validate :post_is_not_its_own_parent
|
|
validate :updater_can_change_rating
|
|
before_save :update_tag_post_counts, if: :should_process_tags?
|
|
before_save :set_tag_counts, if: :should_process_tags?
|
|
after_save :create_post_events
|
|
after_save :create_version
|
|
after_save :update_parent_on_save
|
|
after_save :apply_post_metatags
|
|
after_commit :delete_files, :on => :destroy
|
|
after_commit :remove_iqdb_async, :on => :destroy
|
|
after_commit :update_iqdb_async, :on => :create
|
|
after_commit :generate_video_samples, on: :create, if: :is_video?
|
|
|
|
belongs_to :updater, :class_name => "User", optional: true # this is handled in versions
|
|
belongs_to :approver, class_name: "User", optional: true
|
|
belongs_to :uploader, :class_name => "User"
|
|
user_status_counter :post_count, foreign_key: :uploader_id
|
|
belongs_to :parent, class_name: "Post", optional: true
|
|
has_one :upload, :dependent => :destroy
|
|
has_many :flags, :class_name => "PostFlag", :dependent => :destroy
|
|
has_many :votes, :class_name => "PostVote", :dependent => :destroy
|
|
has_many :notes, :dependent => :destroy
|
|
has_many :comments, -> {includes(:creator, :updater).order("comments.is_sticky DESC, comments.id")}, :dependent => :destroy
|
|
has_many :children, -> {order("posts.id")}, :class_name => "Post", :foreign_key => "parent_id"
|
|
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
|
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
|
has_many :favorites
|
|
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
|
|
|
|
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating,
|
|
:do_not_version_changes, :tag_string_diff, :source_diff, :edit_reason
|
|
|
|
has_many :versions, -> {order("post_versions.id ASC")}, :class_name => "PostVersion", :dependent => :destroy
|
|
|
|
IMAGE_TYPES = %i[original large preview crop]
|
|
|
|
module PostFileMethods
|
|
extend ActiveSupport::Concern
|
|
|
|
module ClassMethods
|
|
def delete_files(post_id, md5, file_ext, force: false)
|
|
if Post.where(md5: md5).exists? && !force
|
|
raise DeletionError.new("Files still in use; skipping deletion.")
|
|
end
|
|
|
|
Danbooru.config.storage_manager.delete_post_files(md5, file_ext)
|
|
end
|
|
end
|
|
|
|
def delete_files
|
|
Post.delete_files(id, md5, file_ext, force: true)
|
|
end
|
|
|
|
def move_files_on_delete
|
|
Danbooru.config.storage_manager.move_file_delete(self)
|
|
end
|
|
|
|
def move_files_on_undelete
|
|
Danbooru.config.storage_manager.move_file_undelete(self)
|
|
end
|
|
|
|
def storage_manager
|
|
Danbooru.config.storage_manager
|
|
end
|
|
|
|
def file(type = :original)
|
|
storage_manager.open_file(self, type)
|
|
end
|
|
|
|
def tagged_large_file_url
|
|
storage_manager.file_url(self, :large)
|
|
end
|
|
|
|
def file_url
|
|
storage_manager.file_url(self, :original)
|
|
end
|
|
|
|
def file_url_ext(ext)
|
|
storage_manager.file_url_ext(self, :original, ext)
|
|
end
|
|
|
|
def scaled_url_ext(scale, ext)
|
|
storage_manager.file_url_ext(self, :scaled, ext, scale: scale)
|
|
end
|
|
|
|
def large_file_url
|
|
return file_url if !has_large?
|
|
storage_manager.file_url(self, :large)
|
|
end
|
|
|
|
def preview_file_url
|
|
storage_manager.file_url(self, :preview)
|
|
end
|
|
|
|
def reverse_image_url
|
|
return large_file_url if has_large?
|
|
preview_file_url
|
|
end
|
|
|
|
def file_path
|
|
storage_manager.file_path(self, file_ext, :original, is_deleted?)
|
|
end
|
|
|
|
def large_file_path
|
|
storage_manager.file_path(self, file_ext, :large, is_deleted?)
|
|
end
|
|
|
|
def preview_file_path
|
|
storage_manager.file_path(self, file_ext, :preview, is_deleted?)
|
|
end
|
|
|
|
def crop_file_url
|
|
storage_manager.file_url(self, :crop)
|
|
end
|
|
|
|
def open_graph_video_url
|
|
if image_height > 720 && has_sample_size?('720p')
|
|
return scaled_url_ext('720p', 'mp4')
|
|
end
|
|
file_url_ext('mp4')
|
|
end
|
|
|
|
def open_graph_image_url
|
|
if is_image?
|
|
if has_large?
|
|
large_file_url
|
|
else
|
|
file_url
|
|
end
|
|
else
|
|
preview_file_url
|
|
end
|
|
end
|
|
|
|
def file_url_for(user)
|
|
if user.default_image_size == "large" && image_width > Danbooru.config.large_image_width
|
|
large_file_url
|
|
else
|
|
file_url
|
|
end
|
|
end
|
|
|
|
def file_url_ext_for(user, ext)
|
|
if user.default_image_size == "large" && is_video? && has_sample_size?('720p')
|
|
scaled_url_ext('720p', ext)
|
|
else
|
|
file_url_ext(ext)
|
|
end
|
|
end
|
|
|
|
def display_class_for(user)
|
|
if user.default_image_size == "original"
|
|
""
|
|
else
|
|
"fit-window"
|
|
end
|
|
end
|
|
|
|
def has_preview?
|
|
is_image? || is_video?
|
|
end
|
|
|
|
def has_dimensions?
|
|
image_width.present? && image_height.present?
|
|
end
|
|
|
|
def preview_dimensions(max_px = Danbooru.config.small_image_width)
|
|
return [max_px, max_px] unless has_dimensions?
|
|
height = width = max_px
|
|
dimension_ratio = image_width.to_f / image_height
|
|
if dimension_ratio > 1
|
|
height = (width / dimension_ratio).to_i
|
|
else
|
|
width = (height * dimension_ratio).to_i
|
|
end
|
|
[height, width]
|
|
end
|
|
|
|
def has_sample_size?(scale)
|
|
(generated_samples || []).include?(scale)
|
|
end
|
|
|
|
def scaled_sample_dimensions(box)
|
|
ratio = [box[0] / image_width.to_f, box[1] / image_height.to_f].min
|
|
width = [([image_width * ratio, 2].max.ceil), box[0]].min & ~1
|
|
height = [([image_height * ratio, 2].max.ceil), box[1]].min & ~1
|
|
[width, height]
|
|
end
|
|
|
|
def generate_video_samples(later: false)
|
|
if later
|
|
PostVideoConversionJob.set(wait: 1.minute).perform_later(id)
|
|
else
|
|
PostVideoConversionJob.perform_later(id)
|
|
end
|
|
end
|
|
|
|
def regenerate_video_samples!
|
|
# force code to assume no samples exist
|
|
update_column(:generated_samples, nil)
|
|
generate_video_samples(later: true)
|
|
end
|
|
|
|
def regenerate_image_samples!
|
|
file = self.file()
|
|
preview_file, crop_file, sample_file = ::PostThumbnailer.generate_resizes(file, image_height, image_width, is_video? ? :video : :image, background_color: bg_color.presence || "000000")
|
|
storage_manager.store_file(sample_file, self, :large) if sample_file.present?
|
|
storage_manager.store_file(preview_file, self, :preview) if preview_file.present?
|
|
storage_manager.store_file(crop_file, self, :crop) if crop_file.present?
|
|
update({has_cropped: crop_file.present?})
|
|
ensure
|
|
file.close
|
|
end
|
|
end
|
|
|
|
module ImageMethods
|
|
def twitter_card_supported?
|
|
image_width.to_i >= 280 && image_height.to_i >= 150
|
|
end
|
|
|
|
def has_large?
|
|
return true if is_video?
|
|
return false if is_gif?
|
|
return false if is_flash?
|
|
return false if has_tag?("animated_gif", "animated_png")
|
|
is_image? && image_width.present? && image_width > Danbooru.config.large_image_width
|
|
end
|
|
|
|
def has_large
|
|
!!has_large?
|
|
end
|
|
|
|
def large_image_width
|
|
if has_large?
|
|
[Danbooru.config.large_image_width, image_width].min
|
|
else
|
|
image_width
|
|
end
|
|
end
|
|
|
|
def large_image_height
|
|
ratio = Danbooru.config.large_image_width.to_f / image_width.to_f
|
|
if has_large? && ratio < 1
|
|
(image_height * ratio).to_i
|
|
else
|
|
image_height
|
|
end
|
|
end
|
|
|
|
def resize_percentage
|
|
100 * large_image_width.to_f / image_width.to_f
|
|
end
|
|
end
|
|
|
|
module ApprovalMethods
|
|
def is_approvable?
|
|
!is_status_locked? && is_pending? && approver.nil?
|
|
end
|
|
|
|
def unflag!
|
|
flags.each(&:resolve!)
|
|
update(is_flagged: false)
|
|
PostEvent.add(id, CurrentUser.user, :flag_removed)
|
|
end
|
|
|
|
def approved_by?(user)
|
|
approver == user || approvals.where(user: user).exists?
|
|
end
|
|
|
|
def unapprove!
|
|
PostEvent.add(id, CurrentUser.user, :unapproved)
|
|
update(approver: nil, is_pending: true)
|
|
end
|
|
|
|
def is_unapprovable?(user)
|
|
# Allow unapproval only by the approver
|
|
return false if approver.present? && approver != user
|
|
# Prevent unapproving self approvals by someone else
|
|
return false if approver.nil? && uploader != user
|
|
# Allow unapproval when the post is not pending anymore and is not at risk of auto deletion
|
|
!is_pending? && !is_deleted? && created_at.after?(PostPruner::DELETION_WINDOW.days.ago)
|
|
end
|
|
|
|
def approve!(approver = CurrentUser.user)
|
|
return if self.approver != nil
|
|
|
|
if uploader == approver
|
|
update(is_pending: false)
|
|
else
|
|
PostEvent.add(id, CurrentUser.user, :approved)
|
|
approvals.create(user: approver)
|
|
update(approver: approver, is_pending: false)
|
|
end
|
|
end
|
|
end
|
|
|
|
module SourceMethods
|
|
def source_array
|
|
return [] if source.blank?
|
|
source.split("\n")
|
|
end
|
|
|
|
def apply_source_diff
|
|
return unless source_diff.present?
|
|
|
|
diff = source_diff.gsub(/\r\n?/, "\n").gsub(/%0A/i, "\n").split(/(?:\r)?\n/)
|
|
to_remove, to_add = diff.partition {|x| x =~ /\A-/i}
|
|
to_remove = to_remove.map {|x| x[1..-1].starts_with?('"') && x.ends_with?('"') ? x[1..-1].delete_prefix('"').delete_suffix('"') : x[1..-1]}
|
|
to_add = to_add.map {|x| x.starts_with?('"') && x.ends_with?('"') ? x.delete_prefix('"').delete_suffix('"') : x}
|
|
|
|
current_sources = source_array
|
|
current_sources += to_add
|
|
current_sources -= to_remove
|
|
self.source = current_sources.join("\n")
|
|
end
|
|
|
|
def strip_source
|
|
self.source = "" if source.blank?
|
|
|
|
self.source.gsub!(/\r\n?/, "\n") # Normalize newlines
|
|
self.source.gsub!(/%0A/i, "\n") # Handle accidentally-encoded %0As from api calls (which would normally insert a literal %0A into the source)
|
|
sources = self.source.split(/(?:\r)?\n/)
|
|
gallery_sources = []
|
|
submission_sources = []
|
|
direct_sources = []
|
|
additional_sources = []
|
|
|
|
alternate_processors = []
|
|
sources.map! do |src|
|
|
src.unicode_normalize!(:nfc)
|
|
src = src.try(:strip)
|
|
alternate = Sources::Alternates.find(src)
|
|
alternate_processors << alternate
|
|
gallery_sources << alternate.gallery_url if alternate.gallery_url
|
|
submission_sources << alternate.submission_url if alternate.submission_url
|
|
direct_sources << alternate.submission_url if alternate.direct_url
|
|
additional_sources += alternate.additional_urls if alternate.additional_urls
|
|
alternate.original_url
|
|
end
|
|
sources = (sources + submission_sources + gallery_sources + direct_sources + additional_sources).compact.reject{ |e| e.strip.empty? }.uniq
|
|
alternate_processors.each do |alt_processor|
|
|
sources = alt_processor.remove_duplicates(sources)
|
|
end
|
|
|
|
self.source = sources.first(10).join("\n")
|
|
end
|
|
|
|
def copy_sources_to_parent
|
|
return unless parent_id.present?
|
|
parent.source += "\n#{self.source}"
|
|
end
|
|
end
|
|
|
|
module PresenterMethods
|
|
def presenter
|
|
@presenter ||= PostPresenter.new(self)
|
|
end
|
|
|
|
def status_flags
|
|
flags = []
|
|
flags << "pending" if is_pending?
|
|
flags << "flagged" if is_flagged?
|
|
flags << "deleted" if is_deleted?
|
|
flags.join(" ")
|
|
end
|
|
|
|
def pretty_rating
|
|
case rating
|
|
when "q"
|
|
"Questionable"
|
|
|
|
when "e"
|
|
"Explicit"
|
|
|
|
when "s"
|
|
"Safe"
|
|
end
|
|
end
|
|
end
|
|
|
|
module TagMethods
|
|
def should_process_tags?
|
|
if @removed_tags.nil?
|
|
@removed_tags = []
|
|
end
|
|
|
|
tag_string_changed? || locked_tags_changed? || tag_string_diff.present? || @removed_tags.length > 0 || added_tags.length > 0
|
|
end
|
|
|
|
def tag_array
|
|
@tag_array ||= TagQuery.scan(tag_string)
|
|
end
|
|
|
|
def tag_array_was
|
|
@tag_array_was ||= TagQuery.scan(tag_string_in_database.presence || tag_string_before_last_save || "")
|
|
end
|
|
|
|
def tags
|
|
Tag.where(name: tag_array)
|
|
end
|
|
|
|
def tags_was
|
|
Tag.where(name: tag_array_was)
|
|
end
|
|
|
|
def added_tags
|
|
tags - tags_was
|
|
end
|
|
|
|
def decrement_tag_post_counts
|
|
Tag.decrement_post_counts(tag_array)
|
|
end
|
|
|
|
def increment_tag_post_counts
|
|
Tag.increment_post_counts(tag_array)
|
|
end
|
|
|
|
def update_tag_post_counts
|
|
return if is_deleted?
|
|
|
|
decrement_tags = tag_array_was - tag_array
|
|
increment_tags = tag_array - tag_array_was
|
|
Tag.increment_post_counts(increment_tags)
|
|
Tag.decrement_post_counts(decrement_tags)
|
|
end
|
|
|
|
def set_tag_count(category, tagcount)
|
|
self.send("tag_count_#{category}=", tagcount)
|
|
end
|
|
|
|
def inc_tag_count(category)
|
|
set_tag_count(category, self.send("tag_count_#{category}") + 1)
|
|
end
|
|
|
|
def set_tag_counts(disable_cache: true)
|
|
self.tag_count = 0
|
|
TagCategory::CATEGORIES.each { |x| set_tag_count(x, 0) }
|
|
categories = Tag.categories_for(tag_array, disable_cache: disable_cache)
|
|
categories.each_value do |category|
|
|
self.tag_count += 1
|
|
inc_tag_count(TagCategory::REVERSE_MAPPING[category])
|
|
end
|
|
end
|
|
|
|
def merge_old_changes
|
|
if old_tag_string
|
|
# If someone else committed changes to this post before we did,
|
|
# then try to merge the tag changes together.
|
|
current_tags = tag_array_was()
|
|
new_tags = tag_array()
|
|
old_tags = TagQuery.scan(old_tag_string)
|
|
|
|
kept_tags = current_tags & new_tags
|
|
@removed_tags = old_tags - kept_tags
|
|
|
|
set_tag_string(((current_tags + new_tags) - old_tags + (current_tags & new_tags)).uniq.sort.join(" "))
|
|
end
|
|
|
|
if old_parent_id == ""
|
|
old_parent_id = nil
|
|
else
|
|
old_parent_id = old_parent_id.to_i
|
|
end
|
|
if old_parent_id == parent_id
|
|
self.parent_id = parent_id_before_last_save || parent_id_was
|
|
end
|
|
|
|
if old_source == source.to_s
|
|
self.source = source_before_last_save || source_was
|
|
end
|
|
|
|
if old_rating == rating
|
|
self.rating = rating_before_last_save || rating_was
|
|
end
|
|
end
|
|
|
|
def apply_tag_diff
|
|
return unless tag_string_diff.present?
|
|
|
|
current_tags = tag_array
|
|
diff = TagQuery.scan(tag_string_diff)
|
|
to_remove, to_add = diff.partition {|x| x =~ /\A-/i}
|
|
to_remove = to_remove.map {|x| x[1..-1]}
|
|
to_remove = TagAlias.to_aliased(to_remove)
|
|
to_add = TagAlias.to_aliased(to_add)
|
|
@removed_tags = to_remove
|
|
current_tags += to_add
|
|
current_tags -= to_remove
|
|
set_tag_string(current_tags.uniq.sort.join(" "))
|
|
end
|
|
|
|
def reset_tag_array_cache
|
|
@tag_array = nil
|
|
@tag_array_was = nil
|
|
end
|
|
|
|
def set_tag_string(string)
|
|
self.tag_string = string
|
|
reset_tag_array_cache
|
|
end
|
|
|
|
def tag_count_not_insane
|
|
return if do_not_version_changes
|
|
|
|
max_count = Danbooru.config.max_tags_per_post
|
|
if TagQuery.scan(tag_string).size > max_count
|
|
self.errors.add(:tag_string, "tag count exceeds maximum of #{max_count}")
|
|
throw :abort
|
|
end
|
|
true
|
|
end
|
|
|
|
def normalize_tags
|
|
if !locked_tags.nil? && locked_tags.strip.blank?
|
|
self.locked_tags = nil
|
|
elsif locked_tags.present?
|
|
locked = TagQuery.scan(locked_tags.downcase)
|
|
to_remove, to_add = locked.partition {|x| x =~ /\A-/i}
|
|
to_remove = to_remove.map {|x| x[1..-1]}
|
|
to_remove = TagAlias.to_aliased(to_remove)
|
|
@locked_to_remove = to_remove + to_remove.map { |tag_name| TagImplication.cached_descendants(tag_name) }.flatten
|
|
@locked_to_add = TagAlias.to_aliased(to_add)
|
|
end
|
|
|
|
normalized_tags = TagQuery.scan(tag_string)
|
|
normalized_tags = apply_casesensitive_metatags(normalized_tags)
|
|
normalized_tags = normalized_tags.map {|tag| tag.downcase}
|
|
normalized_tags = filter_metatags(normalized_tags)
|
|
normalized_tags = remove_negated_tags(normalized_tags)
|
|
normalized_tags = remove_dnp_tags(normalized_tags)
|
|
normalized_tags = TagAlias.to_aliased(normalized_tags)
|
|
normalized_tags = apply_locked_tags(normalized_tags, @locked_to_add, @locked_to_remove)
|
|
normalized_tags = %w[tagme] if normalized_tags.empty?
|
|
normalized_tags = add_automatic_tags(normalized_tags)
|
|
normalized_tags = TagImplication.with_descendants(normalized_tags)
|
|
add_dnp_tags_to_locked(normalized_tags)
|
|
normalized_tags -= @locked_to_remove if @locked_to_remove # Prevent adding locked tags through implications or aliases.
|
|
normalized_tags = normalized_tags.compact.uniq
|
|
normalized_tags = Tag.find_or_create_by_name_list(normalized_tags)
|
|
normalized_tags = remove_invalid_tags(normalized_tags)
|
|
set_tag_string(normalized_tags.map(&:name).uniq.sort.join(" "))
|
|
end
|
|
|
|
# Prevent adding these without an implication
|
|
def remove_dnp_tags(tags)
|
|
locked = locked_tags || ""
|
|
# Don't remove dnp tags here if they would be later added through locked tags
|
|
# to prevent the warning message from appearing when they didn't actually get removed
|
|
if locked.exclude?("avoid_posting")
|
|
tags -= ["avoid_posting"]
|
|
end
|
|
if locked.exclude?("conditional_dnp")
|
|
tags -= ["conditional_dnp"]
|
|
end
|
|
tags
|
|
end
|
|
|
|
def add_dnp_tags_to_locked(tags)
|
|
locked = TagQuery.scan((locked_tags || '').downcase)
|
|
if tags.include? 'avoid_posting'
|
|
locked << 'avoid_posting'
|
|
end
|
|
if tags.include? 'conditional_dnp'
|
|
locked << 'conditional_dnp'
|
|
end
|
|
self.locked_tags = locked.uniq.join(' ') if locked.size > 0
|
|
end
|
|
|
|
def apply_locked_tags(tags, to_add, to_remove)
|
|
if to_remove
|
|
overlap = tags & to_remove
|
|
n = overlap.size
|
|
if n > 0
|
|
self.warnings.add(:base, "Forcefully removed #{n} locked #{n == 1 ? "tag" : "tags"}: #{overlap.join(", ")}")
|
|
end
|
|
tags -= to_remove
|
|
end
|
|
if to_add
|
|
missing = to_add - tags
|
|
n = missing.size
|
|
if n > 0
|
|
self.warnings.add(:base, "Forcefully added #{n} locked #{n == 1 ? "tag" : "tags"}: #{missing.join(", ")}")
|
|
end
|
|
tags += to_add
|
|
end
|
|
tags
|
|
end
|
|
|
|
def remove_invalid_tags(tags)
|
|
tags = tags.reject do |tag|
|
|
if tag.errors.size > 0
|
|
self.warnings.add(:base, "Can't add tag #{tag.name}: #{tag.errors.full_messages.join('; ')}")
|
|
end
|
|
tag.errors.size > 0
|
|
end
|
|
tags
|
|
end
|
|
|
|
def remove_negated_tags(tags)
|
|
@negated_tags, tags = tags.partition {|x| x =~ /\A-/i}
|
|
@negated_tags = @negated_tags.map {|x| x[1..-1]}
|
|
@negated_tags = TagAlias.to_aliased(@negated_tags)
|
|
return tags - @negated_tags
|
|
end
|
|
|
|
def add_automatic_tags(tags)
|
|
return tags if !Danbooru.config.enable_dimension_autotagging?
|
|
|
|
tags -= %w[thumbnail low_res hi_res absurd_res superabsurd_res huge_filesize flash webm mp4 wide_image long_image]
|
|
|
|
if has_dimensions?
|
|
tags << "superabsurd_res" if image_width >= 10_000 && image_height >= 10_000
|
|
tags << "absurd_res" if image_width >= 3200 || image_height >= 2400
|
|
tags << "hi_res" if image_width >= 1600 || image_height >= 1200
|
|
tags << "low_res" if image_width <= 500 && image_height <= 500
|
|
tags << "thumbnail" if image_width <= 250 && image_height <= 250
|
|
|
|
if image_width >= 1024 && image_width.to_f / image_height >= 4
|
|
tags << "wide_image"
|
|
tags << "long_image"
|
|
elsif image_height >= 1024 && image_height.to_f / image_width >= 4
|
|
tags << "tall_image"
|
|
tags << "long_image"
|
|
end
|
|
end
|
|
|
|
if file_size >= 30.megabytes
|
|
tags << "huge_filesize"
|
|
end
|
|
|
|
if is_flash?
|
|
tags << "flash"
|
|
end
|
|
|
|
if is_webm?
|
|
tags << "webm"
|
|
end
|
|
|
|
unless is_gif?
|
|
tags -= ["animated_gif"]
|
|
end
|
|
|
|
unless is_png?
|
|
tags -= ["animated_png"]
|
|
end
|
|
|
|
return tags
|
|
end
|
|
|
|
def apply_casesensitive_metatags(tags)
|
|
casesensitive_metatags, tags = tags.partition {|x| x =~ /\A(?:source):/i}
|
|
#Reuse the following metatags after the post has been saved
|
|
casesensitive_metatags += tags.select {|x| x =~ /\A(?:newpool):/i}
|
|
if casesensitive_metatags.length > 0
|
|
case casesensitive_metatags[-1]
|
|
when /^source:none$/i
|
|
self.source = ""
|
|
|
|
when /^source:"(.*)"$/i
|
|
self.source = $1
|
|
|
|
when /^source:(.*)$/i
|
|
self.source = $1
|
|
|
|
when /^newpool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
if pool.nil?
|
|
pool = Pool.create(name: $1, description: "")
|
|
end
|
|
end
|
|
end
|
|
return tags
|
|
end
|
|
|
|
def filter_metatags(tags)
|
|
@bad_type_changes = []
|
|
@pre_metatags, tags = tags.partition {|x| x =~ /\A(?:rating|parent|-parent|-?locked):/i}
|
|
tags = apply_categorization_metatags(tags)
|
|
@post_metatags, tags = tags.partition {|x| x =~ /\A(?:-pool|pool|newpool|-set|set|fav|-fav|child|-child|upvote|downvote):/i}
|
|
apply_pre_metatags
|
|
if @bad_type_changes.size > 0
|
|
bad_tags = @bad_type_changes.map {|x| "[[#{x}]]"}
|
|
self.warnings.add(:base, "Failed to update the tag category for the following tags: #{bad_tags.join(', ')}. You can not edit the tag category of existing tags using prefixes. Please review usage of the tags, and if you are sure that the tag categories should be changed, then you can change them using the \"Tags\":/tags section of the website")
|
|
end
|
|
tags
|
|
end
|
|
|
|
def apply_categorization_metatags(tags)
|
|
prefixed, unprefixed = tags.partition {|x| x =~ Tag.categories.regexp}
|
|
prefixed = Tag.find_or_create_by_name_list(prefixed)
|
|
prefixed.map! do |tag|
|
|
@bad_type_changes << tag.name if tag.errors.include? :category
|
|
tag.name
|
|
end
|
|
prefixed + unprefixed
|
|
end
|
|
|
|
def apply_post_metatags
|
|
return unless @post_metatags
|
|
|
|
@post_metatags.each do |tag|
|
|
case tag
|
|
when /^-pool:(\d+)$/i
|
|
pool = Pool.find_by(id: $1.to_i)
|
|
if pool
|
|
pool.remove!(self)
|
|
if pool.errors.any?
|
|
errors.add(:base, pool.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^-pool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
if pool
|
|
pool.remove!(self)
|
|
if pool.errors.any?
|
|
errors.add(:base, pool.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^pool:(\d+)$/i
|
|
pool = Pool.find_by(id: $1.to_i)
|
|
if pool
|
|
pool.add!(self)
|
|
if pool.errors.any?
|
|
errors.add(:base, pool.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^(?:new)?pool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
if pool
|
|
pool.add!(self)
|
|
if pool.errors.any?
|
|
errors.add(:base, pool.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^set:(\d+)$/i
|
|
set = PostSet.find_by(id: $1.to_i)
|
|
if set&.can_edit_posts?(CurrentUser.user)
|
|
set.add!(self)
|
|
if set.errors.any?
|
|
errors.add(:base, set.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^-set:(\d+)$/i
|
|
set = PostSet.find_by(id: $1.to_i)
|
|
if set&.can_edit_posts?(CurrentUser.user)
|
|
set.remove!(self)
|
|
if set.errors.any?
|
|
errors.add(:base, set.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^set:(.+)$/i
|
|
set = PostSet.find_by(shortname: $1)
|
|
if set&.can_edit_posts?(CurrentUser.user)
|
|
set.add!(self)
|
|
if set.errors.any?
|
|
errors.add(:base, set.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^-set:(.+)$/i
|
|
set = PostSet.find_by(shortname: $1)
|
|
if set&.can_edit_posts?(CurrentUser.user)
|
|
set.remove!(self)
|
|
if set.errors.any?
|
|
errors.add(:base, set.errors.full_messages.join("; "))
|
|
end
|
|
end
|
|
|
|
when /^child:none$/i
|
|
children.each do |post|
|
|
post.update!(parent_id: nil)
|
|
end
|
|
|
|
when /^-child:(.+)$/i
|
|
children.numeric_attribute_matches(:id, $1).each do |post|
|
|
post.update!(parent_id: nil)
|
|
end
|
|
|
|
when /^child:(.+)$/i
|
|
Post.numeric_attribute_matches(:id, $1).where.not(id: id).limit(10).each do |post|
|
|
post.update!(parent_id: id)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
def apply_pre_metatags
|
|
return unless @pre_metatags
|
|
|
|
@pre_metatags.each do |tag|
|
|
case tag
|
|
when /^parent:none$/i, /^parent:0$/i
|
|
self.parent_id = nil
|
|
|
|
when /^-parent:(\d+)$/i
|
|
if parent_id == $1.to_i
|
|
self.parent_id = nil
|
|
end
|
|
|
|
when /^parent:(\d+)$/i
|
|
if $1.to_i != id && Post.exists?(["id = ?", $1.to_i])
|
|
self.parent_id = $1.to_i
|
|
remove_parent_loops
|
|
end
|
|
|
|
when /^rating:([qse])/i
|
|
self.rating = $1
|
|
|
|
when /^(-?)locked:notes?$/i
|
|
self.is_note_locked = ($1 != "-") if CurrentUser.is_janitor?
|
|
|
|
when /^(-?)locked:rating$/i
|
|
self.is_rating_locked = ($1 != "-") if CurrentUser.is_janitor?
|
|
|
|
when /^(-?)locked:status$/i
|
|
self.is_status_locked = ($1 != "-") if CurrentUser.is_admin?
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
def has_tag?(*)
|
|
TagQuery.has_tag?(tag_array, *)
|
|
end
|
|
|
|
def fetch_tags(*)
|
|
TagQuery.fetch_tags(tag_array, *)
|
|
end
|
|
|
|
def ad_tag_string
|
|
TagQuery.ad_tag_string(tag_array)
|
|
end
|
|
|
|
def add_tag(tag)
|
|
set_tag_string("#{tag_string} #{tag}")
|
|
end
|
|
|
|
def remove_tag(tag)
|
|
set_tag_string((tag_array - Array(tag)).join(" "))
|
|
end
|
|
|
|
def inject_tag_categories(tag_cats)
|
|
@tag_categories = tag_cats
|
|
@typed_tags = tag_array.group_by do |tag_name|
|
|
@tag_categories[tag_name]
|
|
end
|
|
end
|
|
|
|
def tag_categories
|
|
@tag_categories ||= Tag.categories_for(tag_array)
|
|
end
|
|
|
|
def typed_tags(category_id)
|
|
@typed_tags ||= {}
|
|
@typed_tags[category_id] ||= begin
|
|
tag_array.select do |tag|
|
|
tag_categories[tag] == category_id
|
|
end
|
|
end
|
|
end
|
|
|
|
def copy_tags_to_parent
|
|
return unless parent_id.present?
|
|
parent.tag_string += " #{tag_string}"
|
|
end
|
|
end
|
|
|
|
|
|
module FavoriteMethods
|
|
def clean_fav_string!
|
|
array = fav_string.split.uniq
|
|
self.fav_string = array.join(" ")
|
|
self.fav_count = array.size
|
|
end
|
|
|
|
def favorited_by?(user_id = CurrentUser.id)
|
|
!!(fav_string =~ /(?:\A| )fav:#{user_id}(?:\Z| )/)
|
|
end
|
|
|
|
alias_method :is_favorited?, :favorited_by?
|
|
|
|
def append_user_to_fav_string(user_id)
|
|
self.fav_string = (fav_string + " fav:#{user_id}").strip
|
|
clean_fav_string!
|
|
end
|
|
|
|
def delete_user_from_fav_string(user_id)
|
|
self.fav_string = fav_string.gsub(/(?:\A| )fav:#{user_id}(?:\Z| )/, " ").strip
|
|
clean_fav_string!
|
|
end
|
|
|
|
# users who favorited this post, ordered by users who favorited it first
|
|
def favorited_users
|
|
favorited_user_ids = fav_string.scan(/\d+/).map(&:to_i)
|
|
visible_users = User.find(favorited_user_ids).reject(&:hide_favorites?)
|
|
ordered_users = visible_users.index_by(&:id).slice(*favorited_user_ids).values
|
|
ordered_users
|
|
end
|
|
|
|
def remove_from_favorites
|
|
Favorite.where(post_id: id).delete_all
|
|
user_ids = fav_string.scan(/\d+/)
|
|
UserStatus.where(:user_id => user_ids).update_all("favorite_count = favorite_count - 1")
|
|
end
|
|
end
|
|
|
|
module UploaderMethods
|
|
def initialize_uploader
|
|
if uploader_id.blank?
|
|
self.uploader_id = CurrentUser.id
|
|
self.uploader_ip_addr = CurrentUser.ip_addr
|
|
end
|
|
end
|
|
|
|
def uploader_name
|
|
if association(:uploader).loaded?
|
|
return uploader&.name || "Anonymous"
|
|
end
|
|
User.id_to_name(uploader_id)
|
|
end
|
|
end
|
|
|
|
module SetMethods
|
|
def set_ids
|
|
pool_string.scan(/set\:(\d+)/).map {|set| set[0].to_i}
|
|
end
|
|
|
|
def post_sets
|
|
@post_sets ||= begin
|
|
return PostSet.none if pool_string.blank?
|
|
PostSet.where(id: set_ids)
|
|
end
|
|
end
|
|
|
|
def belongs_to_post_set(set)
|
|
pool_string =~ /(?:\A| )set:#{set.id}(?:\z| )/
|
|
end
|
|
|
|
def add_set!(set, force = false)
|
|
return if belongs_to_post_set(set) && !force
|
|
with_lock do
|
|
self.pool_string = "#{pool_string} set:#{set.id}".strip
|
|
end
|
|
end
|
|
|
|
def remove_set!(set)
|
|
with_lock do
|
|
self.pool_string = (pool_string.split(' ') - ["set:#{set.id}"]).join(' ').strip
|
|
end
|
|
end
|
|
|
|
def give_post_sets_to_parent
|
|
transaction do
|
|
post_sets.find_each do |set|
|
|
begin
|
|
set.remove([id])
|
|
set.add([parent.id]) if parent_id.present? && set.transfer_on_delete
|
|
set.save!
|
|
rescue
|
|
#Ignore set errors due to things like set post count
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_from_post_sets
|
|
post_sets.find_each do |set|
|
|
set.remove!(self)
|
|
end
|
|
end
|
|
end
|
|
|
|
module PoolMethods
|
|
def pool_ids
|
|
pool_string.scan(/pool\:(\d+)/).map {|pool| pool[0].to_i}
|
|
end
|
|
|
|
def pools
|
|
@pools ||= begin
|
|
return Pool.none if pool_string.blank?
|
|
Pool.where(id: pool_ids).series_first
|
|
end
|
|
end
|
|
|
|
def has_active_pools?
|
|
pools.any?
|
|
end
|
|
|
|
def belongs_to_pool?(pool)
|
|
pool_string =~ /(?:\A| )pool:#{pool.id}(?:\Z| )/
|
|
end
|
|
|
|
def add_pool!(pool)
|
|
return if belongs_to_pool?(pool)
|
|
|
|
with_lock do
|
|
self.pool_string = "#{pool_string} pool:#{pool.id}".strip
|
|
end
|
|
end
|
|
|
|
def remove_pool!(pool)
|
|
return unless belongs_to_pool?(pool)
|
|
return unless CurrentUser.user.can_remove_from_pools?
|
|
|
|
with_lock do
|
|
self.pool_string = pool_string.gsub(/(?:\A| )pool:#{pool.id}(?:\Z| )/, " ").strip
|
|
end
|
|
end
|
|
|
|
def remove_from_all_pools
|
|
pools.find_each do |pool|
|
|
pool.remove!(self)
|
|
end
|
|
end
|
|
end
|
|
|
|
module VoteMethods
|
|
def own_vote(user = CurrentUser.user)
|
|
return nil unless user
|
|
votes.where('user_id = ?', user.id).first
|
|
end
|
|
end
|
|
|
|
module CountMethods
|
|
def fast_count(tags = "", enable_safe_mode: CurrentUser.safe_mode?)
|
|
tags = tags.to_s
|
|
tags += " rating:s" if enable_safe_mode
|
|
tags += " -status:deleted" unless TagQuery.has_metatag?(tags, "status", "-status")
|
|
tags = TagQuery.normalize(tags)
|
|
|
|
cache_key = "pfc:#{tags}"
|
|
count = Cache.fetch(cache_key)
|
|
if count.nil?
|
|
count = Post.tag_match(tags).count_only
|
|
expiry = count.seconds.clamp(3.minutes, 20.hours).to_i
|
|
Cache.write(cache_key, count, expires_in: expiry)
|
|
end
|
|
count
|
|
rescue TagQuery::CountExceededError
|
|
0
|
|
end
|
|
end
|
|
|
|
module ParentMethods
|
|
# A parent has many children. A child belongs to a parent.
|
|
# A parent cannot have a parent.
|
|
#
|
|
# After expunging a child:
|
|
# - Move favorites to parent.
|
|
# - Does the parent have any children?
|
|
# - Yes: Done.
|
|
# - No: Update parent's has_children flag to false.
|
|
#
|
|
# After expunging a parent:
|
|
# - Move favorites to the first child.
|
|
# - Reparent all children to the first child.
|
|
|
|
def update_has_children_flag
|
|
update(has_children: children.exists?, has_active_children: children.undeleted.exists?)
|
|
end
|
|
|
|
def blank_out_nonexistent_parents
|
|
if parent_id.present? && parent.nil?
|
|
self.parent_id = nil
|
|
end
|
|
end
|
|
|
|
def remove_parent_loops
|
|
if parent.present? && parent.parent_id.present? && parent.parent_id == id
|
|
parent.parent_id = nil
|
|
parent.save
|
|
end
|
|
end
|
|
|
|
def update_parent_on_destroy
|
|
parent.update_has_children_flag if parent
|
|
end
|
|
|
|
def update_children_on_destroy
|
|
return unless children.present?
|
|
|
|
eldest = children[0]
|
|
siblings = children[1..-1]
|
|
|
|
eldest.update(parent_id: nil)
|
|
Post.where(id: siblings).find_each {|p| p.update(parent_id: eldest.id)}
|
|
# Post.where(id: siblings).update(parent_id: eldest.id) # XXX rails 5
|
|
end
|
|
|
|
def update_parent_on_save
|
|
return unless saved_change_to_parent_id? || saved_change_to_is_deleted?
|
|
|
|
parent.update_has_children_flag if parent.present?
|
|
Post.find(parent_id_before_last_save).update_has_children_flag if parent_id_before_last_save.present?
|
|
end
|
|
|
|
def give_favorites_to_parent
|
|
TransferFavoritesJob.perform_later(id, CurrentUser.id)
|
|
end
|
|
|
|
def give_favorites_to_parent!
|
|
return if parent.nil?
|
|
|
|
FavoriteManager.give_to_parent!(self)
|
|
PostEvent.add(id, CurrentUser.user, :favorites_moved, { parent_id: parent_id })
|
|
PostEvent.add(parent_id, CurrentUser.user, :favorites_received, { child_id: id })
|
|
end
|
|
|
|
def parent_exists?
|
|
Post.exists?(parent_id)
|
|
end
|
|
|
|
def has_visible_children?
|
|
return true if has_active_children?
|
|
return true if has_children? && CurrentUser.is_approver?
|
|
return true if has_children? && is_deleted?
|
|
return false
|
|
end
|
|
|
|
def has_visible_children
|
|
has_visible_children?
|
|
end
|
|
|
|
def inject_children(ids)
|
|
@children_ids = ids.map(&:id).join(' ')
|
|
end
|
|
|
|
def children_ids
|
|
if has_children?
|
|
@children_ids ||= children.map {|p| p.id}.join(' ')
|
|
end
|
|
end
|
|
end
|
|
|
|
module DeletionMethods
|
|
def backup_post_data_destroy(reason: "")
|
|
post_data = {
|
|
id: id,
|
|
title: title,
|
|
description: description,
|
|
transcript: transcript,
|
|
md5: md5,
|
|
tags: tag_string,
|
|
height: image_height,
|
|
width: image_width,
|
|
file_size: file_size,
|
|
sources: source,
|
|
approver_id: approver_id,
|
|
locked_tags: locked_tags,
|
|
rating: rating,
|
|
parent_id: parent_id,
|
|
change_seq: change_seq,
|
|
is_deleted: is_deleted,
|
|
is_pending: is_pending,
|
|
duration: duration,
|
|
fav_count: fav_count,
|
|
comment_count: comment_count
|
|
}
|
|
DestroyedPost.create!(post_id: id, post_data: post_data, md5: md5,
|
|
uploader_ip_addr: uploader_ip_addr, uploader_id: uploader_id,
|
|
destroyer_id: CurrentUser.id, destroyer_ip_addr: CurrentUser.ip_addr,
|
|
upload_date: created_at, reason: reason || "")
|
|
end
|
|
|
|
def expunge!(reason: "")
|
|
if is_status_locked?
|
|
self.errors.add(:is_status_locked, "; cannot delete post")
|
|
return false
|
|
end
|
|
|
|
transaction do
|
|
backup_post_data_destroy(reason: reason)
|
|
end
|
|
|
|
transaction do
|
|
Post.without_timeout do
|
|
PostEvent.add(id, CurrentUser.user, :expunged)
|
|
|
|
update_children_on_destroy
|
|
decrement_tag_post_counts
|
|
remove_from_all_pools
|
|
remove_from_post_sets
|
|
remove_from_favorites
|
|
destroy
|
|
update_parent_on_destroy
|
|
end
|
|
end
|
|
end
|
|
|
|
def protect_file?
|
|
is_deleted?
|
|
end
|
|
|
|
def delete!(reason, options = {})
|
|
if is_status_locked? && !options.fetch(:force, false)
|
|
self.errors.add(:is_status_locked, "; cannot delete post")
|
|
return false
|
|
end
|
|
|
|
if reason.blank?
|
|
if pending_flag.blank?
|
|
errors.add(:base, "Cannot delete with given reason when no active flag exists.")
|
|
return
|
|
end
|
|
if pending_flag.reason =~ /uploading_guidelines/
|
|
errors.add(:base, "Cannot delete with given reason when the flag is for uploading guidelines.")
|
|
return
|
|
end
|
|
reason = pending_flag.reason
|
|
end
|
|
|
|
force_flag = options.fetch(:force, false)
|
|
Post.with_timeout(30_000) do
|
|
transaction do
|
|
flag = flags.create(reason: reason, reason_name: 'deletion', is_resolved: false, is_deletion: true, force_flag: force_flag)
|
|
|
|
if flag.errors.any?
|
|
raise PostFlag::Error.new(flag.errors.full_messages.join("; "))
|
|
end
|
|
|
|
update(
|
|
is_deleted: true,
|
|
is_pending: false,
|
|
is_flagged: false
|
|
)
|
|
decrement_tag_post_counts
|
|
move_files_on_delete
|
|
PostEvent.add(id, CurrentUser.user, :deleted, { reason: reason })
|
|
end
|
|
end
|
|
|
|
# XXX This must happen *after* the `is_deleted` flag is set to true (issue #3419).
|
|
# We don't care if these fail per-se so they are outside the transaction.
|
|
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count + 1")
|
|
give_favorites_to_parent if options[:move_favorites]
|
|
give_post_sets_to_parent if options[:move_favorites]
|
|
reject_pending_replacements
|
|
end
|
|
|
|
def reject_pending_replacements
|
|
replacements.where(status: 'pending').update_all(status: 'rejected')
|
|
end
|
|
|
|
def undelete!(options = {})
|
|
if is_status_locked? && !options.fetch(:force, false)
|
|
errors.add(:is_status_locked, "; cannot undelete post")
|
|
return
|
|
end
|
|
|
|
if !CurrentUser.is_admin? && uploader_id == CurrentUser.id
|
|
raise User::PrivilegeError, "You cannot undelete a post you uploaded"
|
|
end
|
|
|
|
if !is_deleted
|
|
errors.add(:base, "Post is not deleted")
|
|
return
|
|
end
|
|
|
|
transaction do
|
|
self.is_deleted = false
|
|
self.is_pending = false
|
|
self.approver_id = CurrentUser.id
|
|
flags.each { |x| x.resolve! }
|
|
increment_tag_post_counts
|
|
save
|
|
approvals.create(user: CurrentUser.user)
|
|
PostEvent.add(id, CurrentUser.user, :undeleted)
|
|
end
|
|
move_files_on_undelete
|
|
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count - 1")
|
|
end
|
|
|
|
def deletion_flag
|
|
flags.order(id: :desc).first
|
|
end
|
|
|
|
def pending_flag
|
|
flags.unresolved.order(id: :desc).first
|
|
end
|
|
end
|
|
|
|
module VersionMethods
|
|
def create_version(force = false)
|
|
return if do_not_version_changes == true
|
|
if new_record? || saved_change_to_watched_attributes? || force
|
|
create_new_version
|
|
end
|
|
end
|
|
|
|
def saved_change_to_watched_attributes?
|
|
saved_change_to_rating? || saved_change_to_source? || saved_change_to_parent_id? || saved_change_to_tag_string? || saved_change_to_locked_tags? || saved_change_to_title? || saved_change_to_description? || saved_change_to_transcript?
|
|
end
|
|
|
|
def create_new_version
|
|
# This function name is misleading, this directly creates the version.
|
|
# Previously there was a queue involved, now there isn't.
|
|
PostVersion.queue(self)
|
|
end
|
|
|
|
def revert_to(target)
|
|
if id != target.post_id
|
|
raise RevertError.new("You cannot revert to a previous version of another post.")
|
|
end
|
|
|
|
self.tag_string = target.tags
|
|
self.rating = target.rating
|
|
self.source = target.source
|
|
self.parent_id = target.parent_id
|
|
self.title = target.title
|
|
self.description = target.description
|
|
self.transcript = target.transcript
|
|
self.edit_reason = "Revert to version #{target.version}"
|
|
end
|
|
|
|
def revert_to!(target)
|
|
revert_to(target)
|
|
save!
|
|
end
|
|
end
|
|
|
|
module NoteMethods
|
|
def has_notes?
|
|
last_noted_at.present?
|
|
end
|
|
|
|
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
|
|
transaction do
|
|
if id == other_post.id
|
|
errors.add :base, "Source and destination posts are the same"
|
|
return false
|
|
end
|
|
unless has_notes?
|
|
errors.add :post, "has no notes"
|
|
return false
|
|
end
|
|
|
|
notes.active.each do |note|
|
|
note.copy_to(other_post)
|
|
end
|
|
|
|
dummy = Note.new
|
|
if notes.active.length == 1
|
|
dummy.body = "Copied 1 note from post ##{id}."
|
|
else
|
|
dummy.body = "Copied #{notes.active.length} notes from post ##{id}."
|
|
end
|
|
dummy.is_active = false
|
|
dummy.post_id = other_post.id
|
|
dummy.x = dummy.y = dummy.width = dummy.height = 0
|
|
dummy.save
|
|
|
|
copy_tags.each do |tag|
|
|
other_post.remove_tag(tag)
|
|
other_post.add_tag(tag) if has_tag?(tag)
|
|
end
|
|
|
|
other_post.save
|
|
end
|
|
end
|
|
end
|
|
|
|
module ApiMethods
|
|
def hidden_attributes
|
|
list = super + [:pool_string, :fav_string]
|
|
if !visible?
|
|
list += [:md5, :file_ext]
|
|
end
|
|
super + list
|
|
end
|
|
|
|
def method_attributes
|
|
list = super + [:has_large, :has_visible_children, :children_ids, :pool_ids, :is_favorited?]
|
|
if visible?
|
|
list += [:file_url, :large_file_url, :preview_file_url]
|
|
end
|
|
list
|
|
end
|
|
|
|
def thumbnail_attributes
|
|
attributes = {
|
|
id: id,
|
|
flags: status_flags,
|
|
tags: tag_string,
|
|
rating: rating,
|
|
file_ext: file_ext,
|
|
|
|
width: image_width,
|
|
height: image_height,
|
|
size: file_size,
|
|
|
|
created_at: created_at,
|
|
uploader: uploader_name,
|
|
uploader_id: uploader_id,
|
|
|
|
score: score,
|
|
fav_count: fav_count,
|
|
is_favorited: favorited_by?(CurrentUser.user.id),
|
|
|
|
pools: pool_ids,
|
|
}
|
|
|
|
if visible?
|
|
attributes[:md5] = md5
|
|
attributes[:preview_url] = preview_file_url
|
|
attributes[:large_url] = large_file_url
|
|
attributes[:file_url] = file_url
|
|
attributes[:preview_width] = preview_dimensions[1]
|
|
attributes[:preview_height] = preview_dimensions[0]
|
|
attributes[:large_width] = large_image_width
|
|
attributes[:large_height] = large_image_height
|
|
end
|
|
|
|
attributes
|
|
end
|
|
|
|
def status
|
|
if is_pending?
|
|
"pending"
|
|
elsif is_deleted?
|
|
"deleted"
|
|
elsif is_flagged?
|
|
"flagged"
|
|
else
|
|
"active"
|
|
end
|
|
end
|
|
end
|
|
|
|
module SearchMethods
|
|
# returns one single post
|
|
def random
|
|
key = Digest::MD5.hexdigest(Time.now.to_f.to_s)
|
|
random_up(key) || random_down(key)
|
|
end
|
|
|
|
def random_up(key)
|
|
where("md5 < ?", key).reorder("md5 desc").first
|
|
end
|
|
|
|
def random_down(key)
|
|
where("md5 >= ?", key).reorder("md5 asc").first
|
|
end
|
|
|
|
def sample(query, sample_size)
|
|
tag_match_system("#{query} order:random", free_tags_count: 1).limit(sample_size).relation
|
|
end
|
|
|
|
# unflattens the tag_string into one tag per row.
|
|
def with_unflattened_tags
|
|
joins("CROSS JOIN unnest(string_to_array(tag_string, ' ')) AS tag")
|
|
end
|
|
|
|
def pending
|
|
where(is_pending: true)
|
|
end
|
|
|
|
def flagged
|
|
where(is_flagged: true)
|
|
end
|
|
|
|
def pending_or_flagged
|
|
pending.or(flagged)
|
|
end
|
|
|
|
def undeleted
|
|
where("is_deleted = ?", false)
|
|
end
|
|
|
|
def deleted
|
|
where("is_deleted = ?", true)
|
|
end
|
|
|
|
def has_notes
|
|
where("last_noted_at is not null")
|
|
end
|
|
|
|
def for_user(user_id)
|
|
where("uploader_id = ?", user_id)
|
|
end
|
|
|
|
def sql_raw_tag_match(tag)
|
|
where("string_to_array(posts.tag_string, ' ') @> ARRAY[?]", tag)
|
|
end
|
|
|
|
def tag_match_system(query, free_tags_count: 0)
|
|
tag_match(query, free_tags_count: free_tags_count, enable_safe_mode: false, always_show_deleted: true)
|
|
end
|
|
|
|
def tag_match(query, resolve_aliases: true, free_tags_count: 0, enable_safe_mode: CurrentUser.safe_mode?, always_show_deleted: false)
|
|
ElasticPostQueryBuilder.new(
|
|
query,
|
|
resolve_aliases: resolve_aliases,
|
|
free_tags_count: free_tags_count,
|
|
enable_safe_mode: enable_safe_mode,
|
|
always_show_deleted: always_show_deleted,
|
|
).search
|
|
end
|
|
|
|
def tag_match_sql(query)
|
|
PostQueryBuilder.new(query).search
|
|
end
|
|
end
|
|
|
|
module IqdbMethods
|
|
extend ActiveSupport::Concern
|
|
|
|
module ClassMethods
|
|
def remove_iqdb(post_id)
|
|
if IqdbProxy.enabled?
|
|
IqdbRemoveJob.perform_later(post_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_iqdb_async
|
|
if IqdbProxy.enabled? && has_preview?
|
|
IqdbUpdateJob.perform_later(id)
|
|
end
|
|
end
|
|
|
|
def remove_iqdb_async
|
|
Post.remove_iqdb(id)
|
|
end
|
|
end
|
|
|
|
module PostEventMethods
|
|
def create_post_events
|
|
if saved_change_to_is_rating_locked?
|
|
action = is_rating_locked? ? :rating_locked : :rating_unlocked
|
|
PostEvent.add(id, CurrentUser.user, action)
|
|
end
|
|
if saved_change_to_is_status_locked?
|
|
action = is_status_locked? ? :status_locked : :status_unlocked
|
|
PostEvent.add(id, CurrentUser.user, action)
|
|
end
|
|
if saved_change_to_is_note_locked?
|
|
action = is_note_locked? ? :note_locked : :note_unlocked
|
|
PostEvent.add(id, CurrentUser.user, action)
|
|
end
|
|
if saved_change_to_is_comment_locked?
|
|
action = is_comment_locked? ? :comment_locked : :comment_unlocked
|
|
PostEvent.add(id, CurrentUser.user, action)
|
|
end
|
|
if saved_change_to_is_comment_disabled?
|
|
action = is_comment_disabled? ? :comment_disabled : :comment_enabled
|
|
PostEvent.add(id, CurrentUser.user, action)
|
|
end
|
|
if saved_change_to_bg_color?
|
|
PostEvent.add(id, CurrentUser.user, :changed_bg_color, { bg_color: bg_color })
|
|
end
|
|
end
|
|
end
|
|
|
|
module ValidationMethods
|
|
def fix_bg_color
|
|
if bg_color.blank?
|
|
self.bg_color = nil
|
|
end
|
|
end
|
|
|
|
def post_is_not_its_own_parent
|
|
if !new_record? && id == parent_id
|
|
errors.add(:base, "Post cannot have itself as a parent")
|
|
false
|
|
end
|
|
end
|
|
|
|
def updater_can_change_rating
|
|
if rating_changed? && is_rating_locked?
|
|
# Don't forbid changes if the rating lock was just now set in the same update.
|
|
if !is_rating_locked_changed?
|
|
errors.add(:rating, "is locked and cannot be changed. Unlock the post first.")
|
|
end
|
|
end
|
|
end
|
|
|
|
def added_tags_are_valid
|
|
# Load this only once since it isn't cached
|
|
added = added_tags
|
|
added_invalid_tags = added.select { |t| t.category == Tag.categories.invalid }
|
|
new_tags = added.select { |t| t.post_count <= 0 }
|
|
new_general_tags = new_tags.select { |t| t.category == Tag.categories.general }
|
|
new_artist_tags = new_tags.select { |t| t.category == Tag.categories.artist }
|
|
# See https://github.com/e621ng/e621ng/issues/494
|
|
# If the tag is fresh it's save to assume it was created with a prefix
|
|
repopulated_tags = new_tags.select { |t| t.category != Tag.categories.general && t.category != Tag.categories.meta && t.created_at < 10.seconds.ago }
|
|
|
|
if added_invalid_tags.present?
|
|
n = added_invalid_tags.size
|
|
tag_wiki_links = added_invalid_tags.map { |tag| "[[#{tag.name}]]" }
|
|
warnings.add(:base, "Added #{n} invalid #{'tag'.pluralize(n)}. See the wiki page for each tag for help on resolving these: #{tag_wiki_links.join(', ')}")
|
|
end
|
|
|
|
if new_general_tags.present?
|
|
n = new_general_tags.size
|
|
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
|
|
warnings.add(:base, "Created #{n} new #{'tag'.pluralize(n)}: #{tag_wiki_links.join(', ')}")
|
|
end
|
|
|
|
if repopulated_tags.present?
|
|
n = repopulated_tags.size
|
|
tag_wiki_links = repopulated_tags.map { |tag| "[[#{tag.name}]]" }
|
|
warnings.add(:base, "Repopulated #{n} old #{'tag'.pluralize(n)}: #{tag_wiki_links.join(', ')}")
|
|
end
|
|
|
|
new_artist_tags.each do |tag|
|
|
if tag.artist.blank?
|
|
warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]")
|
|
end
|
|
end
|
|
end
|
|
|
|
def removed_tags_are_valid
|
|
attempted_removed_tags = @removed_tags + @negated_tags
|
|
unremoved_tags = tag_array & attempted_removed_tags
|
|
|
|
if unremoved_tags.present?
|
|
unremoved_tags_list = unremoved_tags.map {|t| "[[#{t}]]"}.to_sentence
|
|
self.warnings.add(:base, "#{unremoved_tags_list} could not be removed. Check for implications and locked tags and try again")
|
|
end
|
|
|
|
@removed_tags = []
|
|
end
|
|
|
|
def has_artist_tag
|
|
return if !new_record?
|
|
return if tags.any? { |t| t.category == Tag.categories.artist }
|
|
|
|
self.warnings.add(:base, 'Artist tag is required. "Click here":/help/tags#catchange if you need help changing the category of an tag. Ask on the forum if you need naming help')
|
|
end
|
|
|
|
def has_enough_tags
|
|
return if !new_record?
|
|
|
|
if tags.count {|t| t.category == Tag.categories.general} < 10
|
|
self.warnings.add(:base, "Uploads must have at least 10 general tags. Read [[help:tags]] for guidelines on tagging your uploads")
|
|
end
|
|
end
|
|
end
|
|
|
|
include PostFileMethods
|
|
include FileMethods
|
|
include ImageMethods
|
|
include ApprovalMethods
|
|
include SourceMethods
|
|
include PresenterMethods
|
|
include TagMethods
|
|
include FavoriteMethods
|
|
include UploaderMethods
|
|
include PoolMethods
|
|
include SetMethods
|
|
include VoteMethods
|
|
extend CountMethods
|
|
include ParentMethods
|
|
include DeletionMethods
|
|
include VersionMethods
|
|
include NoteMethods
|
|
include ApiMethods
|
|
extend SearchMethods
|
|
include IqdbMethods
|
|
include ValidationMethods
|
|
include PostEventMethods
|
|
include Danbooru::HasBitFlags
|
|
include DocumentStore::Model
|
|
include PostIndex
|
|
|
|
BOOLEAN_ATTRIBUTES = %w(
|
|
_has_embedded_notes
|
|
has_cropped
|
|
hide_from_anonymous
|
|
hide_from_search_engines
|
|
)
|
|
has_bit_flags BOOLEAN_ATTRIBUTES
|
|
|
|
def safeblocked?
|
|
return true if Danbooru.config.safe_mode? && rating != "s"
|
|
CurrentUser.safe_mode? && (rating != "s" || has_tag?(*Danbooru.config.safeblocked_tags))
|
|
end
|
|
|
|
def deleteblocked?
|
|
!Danbooru.config.can_user_see_post?(CurrentUser.user, self)
|
|
end
|
|
|
|
def loginblocked?
|
|
CurrentUser.is_anonymous? && (hide_from_anonymous? || Danbooru.config.user_needs_login_for_post?(self))
|
|
end
|
|
|
|
def visible?
|
|
return false if loginblocked?
|
|
return false if safeblocked?
|
|
return false if deleteblocked?
|
|
return true
|
|
end
|
|
|
|
def allow_sample_resize?
|
|
!is_flash?
|
|
end
|
|
|
|
def force_original_size?
|
|
is_flash?
|
|
end
|
|
|
|
def reload(options = nil)
|
|
super
|
|
reset_tag_array_cache
|
|
@locked_to_add = nil
|
|
@locked_to_remove = nil
|
|
@pools = nil
|
|
@post_sets = nil
|
|
@tag_categories = nil
|
|
@typed_tags = nil
|
|
self
|
|
end
|
|
|
|
def mark_as_translated(params)
|
|
add_tag("translation_check") if params["translation_check"].to_s.truthy?
|
|
remove_tag("translation_check") if params["translation_check"].to_s.falsy?
|
|
|
|
add_tag("partially_translated") if params["partially_translated"].to_s.truthy?
|
|
remove_tag("partially_translated") if params["partially_translated"].to_s.falsy?
|
|
|
|
if has_tag?("translation_check", "partially_translated")
|
|
add_tag("translation_request")
|
|
remove_tag("translated")
|
|
else
|
|
add_tag("translated")
|
|
remove_tag("translation_request")
|
|
end
|
|
|
|
save
|
|
end
|
|
|
|
def artist_tags
|
|
tags.select { |t| t.category == Tag.categories.artist }
|
|
end
|
|
|
|
def uploader_linked_artists
|
|
artist_tags.filter_map(&:artist).select { |artist| artist.linked_user_id == uploader_id }
|
|
end
|
|
|
|
def flaggable_for_guidelines?
|
|
!has_tag?("grandfathered_content") && created_at.after?("2015-01-01")
|
|
end
|
|
|
|
def visible_comment_count(user)
|
|
if user.is_moderator? || !is_comment_disabled?
|
|
comment_count
|
|
else
|
|
comments.visible(user).count
|
|
end
|
|
end
|
|
|
|
def avoid_posting_artists
|
|
AvoidPosting.active.joins(:artist).where("artists.name": artist_tags.map(&:name))
|
|
end
|
|
end
|