Websites/app/controllers/yiff_rest/api_v2_controller.rb

206 lines
8.5 KiB
Ruby
Raw Normal View History

2024-05-03 03:04:43 +00:00
# frozen_string_literal: true
module YiffRest
class APIV2Controller < APIV2::ApplicationController
2024-05-03 03:04:43 +00:00
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("/", ".") || ""
2024-05-03 06:39:05 +00:00
category = category[3..] if category.start_with?("v2.")
2024-05-03 03:04:43 +00:00
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)
2024-05-03 03:04:43 +00:00
[*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
2024-05-09 02:25:36 +00:00
Total: **#{bulk_categories.values.sum}**
2024-05-03 03:04:43 +00:00
**Categories:**
#{bulk_categories.permit!.to_h.map { |c, l| "- **#{c}**: #{l}" }.join("\n")}
BULK
2024-05-09 02:19:08 +00:00
color = 0xDC143C if bulk_categories.keys.any? { |k| category_info.find { |c| c.db == k }.sfw }
2024-05-09 02:25:36 +00:00
elsif !category_info.find { |c| c.db == category }.sfw
2024-05-03 03:04:43 +00:00
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