Websites/app/controllers/yiff_rest/api_v2_controller.rb
Donovan Daniels b1c702e3cd
Add image management ui
poorly tested but it worked well enough, I'm sure I'll be patching bugs over the next few weeks
Also remove turbo because it sucks
Also changed the way we handle hosts in dev
2024-05-06 03:25:17 -05:00

205 lines
8.5 KiB
Ruby

# frozen_string_literal: true
module YiffRest
class APIV2Controller < APIV2::ApplicationController
include ::ApplicationController::ReadonlyMethods
before_action :handle_ratelimit, except: %i[robots]
before_action :validate_images_access, only: %i[index]
before_action :validate_images_bulk_access, only: %i[bulk]
before_action -> { track_usage("images") }, only: %i[index categories category image]
def index
return render_error(YiffyAPIErrorCodes::IMAGES_IMAGE_RESPONSE_DISABLED, error: "Image response has been disabled. Please use the json response.") if params[:category].ends_with?("/image")
category = params[:category]&.downcase&.gsub("/", ".") || ""
category = category[3..] if category.start_with?("v2.")
return redirect_to("https://yiff.rest") if category.blank?
limit = params[:amount]&.to_i || 1
return render_error(YiffyAPIErrorCodes::IMAGES_AMOUNT_LT_ONE, error: "Amount must be 1 or more.") if limit < 1
return render_error(YiffyAPIErrorCodes::IMAGES_AMOUNT_LT_ONE, error: "Amount must be 5 or less.") if limit > 5
size_limit = params[:sizeLimit].blank? ? nil : Filesize.from(params[:sizeLimit].to_s).to_i
return render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Category not found.") unless api_categories.include?(category)
images = APIImage.random(category, limit, size_limit)
return render_error(YiffyAPIErrorCodes::IMAGES_NO_RESULTS, error: "No results were found. Try changing your search parameters.") if images.empty?
Cache.redis.multi do |r|
r.incr("yiffy2:stats:images:#{category}")
r.incr("yiffy2:stats:images:ip:#{request.remote_ip}")
r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}")
r.incr("yiffy2:stats:images:total")
r.incr("yiffy2:stats:images:total:#{category}")
if @apikey.present? && !@apikey&.is_anon?
r.incr("yiffy2:stats:images:key:#{@apikey.id}")
r.incr("yiffy2:stats:images:key:#{@apikey.id}:#{category}")
end
end
render(json: {
"$schema": "https://schema.yiff.rest/V2.json",
images: images,
success: true,
notes: notes_for_request,
})
end
def robots
render(plain: <<~ROBOTS)
User-agent: *
Disallow: /
ROBOTS
end
def stats
render(json: {
success: true,
data: APIKey.stats(ip: request.remote_ip, key: @apikey),
})
end
def categories
render(json: {
success: true,
data: APIImage.categories,
})
end
def category
category = params[:category]&.downcase&.gsub("/", ".") || ""
return render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Category not found.") unless api_categories.include?(category)
count = APIImage.cached_count(category)
render(json: {
success: true,
data: {
**APIImage.categories.find { |c| c[:db] == category },
count: count,
},
})
end
def image
image = APIImage.find_by(id: params[:id].gsub("-", ""))
return render_error(YiffyAPIErrorCodes::IMAGES_NOT_FOUND, error: "Image not found.") unless image
render(json: {
success: true,
data: {
**image.as_json,
category: image.category,
},
})
end
def bulk
req = params[:api_v2] || {}
return render_error(YiffyAPIErrorCodes::BULK_IMAGES_INVALID_BODY, error: "Invalid body, or no categories specified.") if req.blank?
size_limit = params[:sizeLimit].blank? ? nil : Filesize.from(params[:sizeLimit].to_s).to_i
total = 0
valid = true
req.each do |category, amount|
total += amount.to_i
next if api_categories.include?(category)
render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Invalid category specified: #{category}")
valid = false
break
end
return unless valid
return render_error(YiffyAPIErrorCodes::BULK_IMAGES_NUMBER_GT_MAX, error: "Total amount of images requested is greater than #{@apikey.bulk_limit} (#{total}).") if total > @apikey.bulk_limit
images = APIImage.bulk(req, size_limit)
return render_error(YiffyAPIErrorCodes::IMAGES_NO_RESULTS, error: "No results were found. Try changing your search parameters.") if images.empty?
Cache.redis.multi do |r|
r.incrby("yiffy2:stats:images:ip:#{request.remote_ip}", total)
r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:bulk")
r.incrby("yiffy2:stats:images:total", total)
r.incr("yiffy2:stats:images:total:bulk")
r.incrby("yiffy2:stats:images:key:#{@apikey.id}", total)
r.incr("yiffy2:stats:images:key:#{@apikey.id}:bulk")
req.each do |category, amount|
r.incrby("yiffy2:stats:images:#{category}", amount.to_i)
r.incr("yiffy2:stats:images:#{category}:bulk")
r.incrby("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}", amount.to_i)
r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}:bulk")
r.incrby("yiffy2:stats:images:total:#{category}", amount.to_i)
r.incr("yiffy2:stats:images:total:#{category}:bulk")
r.incrby("yiffy2:stats:images:key:#{@apikey.id}:#{category}", amount.to_i)
r.incr("yiffy2:stats:images:key:#{@apikey.id}:#{category}:bulk")
end
end
execute_webhook("bulk", req)
render(json: {
success: true,
data: images,
})
end
private
def api_categories
categories = APIImage.categories.map(&:db)
[*categories, "chris"] # gay little polar cutie
end
def category_info
[*APIImage.categories, { name: "Gay Polar Cutie", db: "chris", sfw: true }] # gay little polar cutie
end
def notes_for_request
return [] if params[:notes].to_s.downcase == "disabled"
notes = []
# notes << { id: 1, content: "This api host (api.furry.bot) is being removed on June 9th, 2021. Please migrate to https://yiff.rest." } if headers["Host"] == "api.furry.bot"
notes << { id: 2, content: "We've moved to using subdomains for api versioning! e.g. https://v2.yiff.rest. They have the same functionality, just without the version in the path. The /V2 route will not be removed, but v3 and forward will only use the subdomain." } if request.path.start_with?("/V2")
notes << { id: 3, content: "Hey, we see you aren't using an api key. They're free! To get one, visit https://yiff.rest/apikeys." } if headers["Authorization"].blank?
# notes << { id: 4, content: "WARNING! This list is STATIC, it can be inaccurate! We recommended parsing the dot notation in https://v2.yiff.rest/categories instead of this!" }
# notes << { id: 5, content: "Since images are getting bigger, we're adding a size limit parameter. Add ?sizeLimit=<size> to limit the size of images we provide you." } if params[:sizeLimit].blank?
notes << { id: 6, content: "You can now hide these notes by setting the notes parameter to disabled. (ex: ?notes=disabled)" }
notes << { id: 7, content: "We now have proper documentation: https://docs.yiff.rest" }
notes << { id: 8, content: "We have a new service available, a thumbnailer for e621. You can see its documentation at https://docs.yiff.rest/thumbnails." }
end
def execute_webhook(category, bulk_categories = nil)
bulk = ""
color = 0x008000
if bulk_categories.present?
bulk = <<~BULK
**Categories:**
#{bulk_categories.permit!.to_h.map { |c, l| "- **#{c}**: #{l}" }.join("\n")}
BULK
color = 0xDC143C if bulk_categories.keys.any? { |k| category_info.find { |c| c[:db] == k }[:sfw] }
elsif !(category_info.find { |c| c[:db] == category }[:sfw])
color = 0xDC143C
end
Websites.config.yiffyapi_usage_webhook.execute(embeds: [
{
title: "V2 API Request",
description: <<~DESC.strip,
Host: **#{request.host}**
Path: **#{request.path}**
Category: `#{category}`
Auth: #{@apikey.nil? || @apikey.is_anon? ? '**No**' : "**Yes** (##{@apikey.id})"}
Size Limit: **#{params[:sizeLimit] || 'None'}**
User Agent: **#{request.user_agent}**
IP: **#{request.remote_ip}**
#{bulk}
DESC
color: color,
timestamp: Time.now.iso8601,
},
])
end
def allowed_readonly_actions
super + %w[bulk]
end
end
end