Websites/app/models/api_image.rb

323 lines
8.9 KiB
Ruby

# frozen_string_literal: true
class APIImage < ApplicationRecord
belongs_to_creator
attr_accessor :file, :exception, :deletion_reason, :no_webhook_messages
validates :category, presence: true, inclusion: { in: -> { APIImage.categories.map(&:db) } }
validates :id, uniqueness: true, on: :file
validate on: :file do |image|
ext, mime = file_header_info(file.path)
image.errors.add(:file, "type is invalid (#{mime})") if ext == mime
end
validate do |image|
image.errors.add(:base, exception.message) if exception
end
after_create :invalidate_cache
after_create :send_created, unless: :no_webhook_messages
after_create :initialize_short_url
after_update :update_files, if: :saved_change_to_category?
after_update :send_updated, unless: :no_webhook_messages
after_destroy :delete_files
after_destroy :send_deleted, unless: :no_webhook_messages
def delete_files
Websites.config.yiffy2_storage.delete(path)
Websites.config.yiffy2_backup_storage.delete(path)
invalidate_cache
end
def update_files
file = Websites.config.yiffy2_storage.get(path_before_last_save)
Websites.config.yiffy2_storage.put(path, file)
Websites.config.yiffy2_storage.delete(path_before_last_save)
Websites.config.yiffy2_backup_storage.put(path, file)
Websites.config.yiffy2_backup_storage.delete(path_before_last_save)
invalidate_cache
end
def file_header_info(file_path)
File.open(file_path) do |bin|
mime_type = Marcel::MimeType.for(bin)
ext = case mime_type
when "image/jpeg"
"jpg"
when "image/gif"
"gif"
when "image/png"
"png"
else
mime_type
end
[mime_type, ext]
end
end
def invalidate_cache
Cache.redis.del("yiffy2:images:#{category_before_last_save}")
Cache.redis.del("yiffy2:images:#{category}")
end
module SearchMethods
def random(category, limit, size_limit = nil)
q = where(category: category)
q = q.where(file_size: ..size_limit) if size_limit
q.ids.sample(limit).map(&method(:find))
end
def bulk(category_map, size_limit = nil)
data = {}
category_map.each do |category, limit|
data[category] = random(category, limit, size_limit)
end
data
end
def search(params)
q = super
q = q.attribute_matches(:category, params[:category])
q = q.attribute_matches(:original_url, params[:original_url])
q = q.attribute_matches(:artists, params[:artists])
q.order(created_at: :desc)
end
end
extend SearchMethods
def md5
id.gsub("-", "")
end
alias stripped_md5 md5
def path_before_last_save
"/#{category_before_last_save.tr('.', '/')}/#{md5}.#{file_ext}"
end
def path
"/#{category.tr('.', '/')}/#{md5}.#{file_ext}"
end
def url
Websites.config.yiffy2_storage.url_for(self)
end
def short_url
ShortUrl.override(md5, url).shorturl
end
def initialize_short_url
ShortUrl.override(md5, url)
end
def sources_string
sources.join("\n")
end
def sources_string=(str)
self.sources = str.split("\n").map(&:strip)
end
def artists_string
artists.join("\n")
end
def artists_string=(str)
self.artists = str.split("\n").map(&:strip)
end
def serializable_hash(*)
{
artists: artists,
sources: sources,
width: width,
height: height,
url: url,
type: mime_type,
name: "#{md5}.#{file_ext}",
id: md5,
ext: file_ext,
size: file_size,
reportURL: nil,
shortURL: short_url,
}
end
def self.categories
sfw = %w[animals.birb animals.blep animals.dikdik furry.boop furry.cuddle furry.flop furry.fursuit furry.hold furry.howl furry.hug furry.kiss furry.lick furry.propose]
nsfw = %w[furry.butts furry.bulge furry.yiff.andromorph furry.yiff.gay furry.yiff.gynomorph furry.yiff.lesbian furry.yiff.straight]
titles = {
**sfw.index_with { |c| c.split(".").map(&:capitalize).join(" > ") },
"animals.dikdik" => "Animals > Dik Dik",
**nsfw.index_with { |c| c.split(".").map(&:capitalize).join(" > ") },
}
sfw.map { |cat| APICategory.new(titles[cat], cat, true) }
.concat(nsfw.map { |cat| APICategory.new(titles[cat], cat, false) })
end
def self.category_title(db)
categories.find { |k| k == db }.try(:db) || db.split(".").map(&:capitalize).join(" > ")
end
def self.cached_count(category)
count = Cache.redis.get("yiffy2:images:#{category}")
if count.nil?
count = APIImage.where(category: category).count
# images aren't changing so we really don't need any expiry, but I still want an expiry just in case
Cache.redis.set("yiffy2:images:#{category}", count.to_s, ex: 60 * 60 * 24 * 7)
end
count.to_i
end
def self.state
data = group(:category).count.map do |k, v|
{
count: v,
name: category_title(k),
category: k,
state: v < 5 ? "red" : v < 20 ? "yellow" : "green",
}
end
[*data, {
count: data.pluck(:count).reduce(:+),
name: "Total",
category: "total",
state: "total",
},]
end
def self.sync_all
sync_e621
end
# TODO
def self.sync_e621
images = joins("CROSS JOIN unnest(sources) AS source").where("source ILIKE ?", "%e621.net%").distinct.limit(300)
ids = []
md5s = []
images.each do |img|
img.sources.each do |source|
next unless source.include?("e621.net")
id = source.split("/").last
ids << id.to_i if id.to_i.to_s == id
end
md5s << img.md5
end
Requests::E621.get_all_posts(ids: ids, md5s: md5s) => { posts:, missing: }
Rails.logger.info("Failed to find #{missing[:ids].length} posts") unless missing[:ids].empty?
mismatch = images.select { |img| posts.none? { |post| post["file"]["md5"] == img.md5 } }
Rails.logger.info("Found #{mismatch.length} mismatched images") unless mismatch.empty?
posts
end
def sync
APIImageSyncJob.perform_later(self)
end
module WebhookMethods
GREEN = 0x008000
YELLOW = 0xFFA500
RED = 0xFF0000
GREEN_TICK = "<:GreenTick:1235058920762904576>"
RED_TICK = "<:RedTick:1235058898549870724>"
def execute(content)
Websites.config.yiffyapi_image_logs_webhook.execute({
embeds: [content],
})
end
def send_created
execute({
title: "Image Uploaded",
description: <<~DESC,
ID: `#{md5}`
Category: `#{category}`
Artists: `#{artists.join(', ')}`
Sources:
#{sources.map { |s| "* #{s}" }.join("\n")}
Upload Type: #{original_url.present? ? "[url](#{original_url})" : 'file'}
Uploaded By: <@#{creator.id}> (`#{creator.name}`)
Created At: <t:#{created_at.to_i}>
Updated At: <t:#{updated_at.to_i}>
DESC
color: GREEN,
timestamp: Time.now.iso8601,
})
end
def send_deleted
execute({
title: "Image Deleted",
description: <<~DESC,
ID: `#{md5}`
Category: `#{category}`
Artists: `#{artists.join(', ')}`
Sources:
#{sources.map { |s| "* #{s}" }.join("\n")}
Upload Type: #{original_url.present? ? "[url](#{original_url})" : 'file'}
Uploaded By: <@#{creator.id}> (`#{creator.name}`)
Reason: #{deletion_reason || 'None Provided'}
Created At: <t:#{created_at.to_i}>
Updated At: <t:#{updated_at.to_i}>
DESC
color: RED,
timestamp: Time.now.iso8601,
})
end
def check_change(attr, changes)
changes << "#{attr.to_s.titleize}: **#{send("#{attr}_before_last_save")}** -> **#{send(attr)}**" if send("saved_change_to_#{attr}?")
end
def send_updated
changes = []
check_change(:artists, changes)
check_change(:category, changes)
if sources != sources_before_last_save
diff = []
sources_before_last_save.each do |source|
diff << "- #{source}" unless sources.include?(source)
end
sources.each do |source|
diff << "+ #{source}" unless sources_before_last_save.include?(source)
end
changes << "Sources:\n```diff\n#{diff.join("\n")}\n```"
end
return if changes.empty?
changes << "Blame: #{blame}"
execute({
title: "Image Updated",
description: <<~DESC,
ID: `#{md5}`
#{changes.join("\n")}
DESC
color: YELLOW,
timestamp: Time.now.iso8601,
})
end
def blame
if CurrentUser.user.present?
"<@#{CurrentUser.id}> (`#{CurrentUser.name}`)"
elsif Rails.const_defined?("Console")
"**Console**"
elsif CurrentUser.ip_addr
"**#{CurrentUser.ip_addr}**"
else
"**Unknown**"
end
end
end
include WebhookMethods
end