208 lines
8.5 KiB
Ruby
208 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
|
|
|
|
execute_webhook(category)
|
|
|
|
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 }.to_h,
|
|
count: count,
|
|
},
|
|
})
|
|
end
|
|
|
|
def image
|
|
image = APIImage.search({ md5: params[:id].gsub("-", "") }).first
|
|
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, APICategory.new("Gay Polar Cutie", "chris", 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
|
|
Total: **#{bulk_categories.values.sum}**
|
|
|
|
**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
|