393 lines
11 KiB
Ruby
393 lines
11 KiB
Ruby
# typed: true
|
|
class Domain::Fa::ApiController < ApplicationController
|
|
skip_before_action :authenticate_user!
|
|
before_action :validate_api_token!
|
|
|
|
skip_before_action :verify_authenticity_token,
|
|
only: %i[enqueue_objects object_statuses]
|
|
|
|
skip_before_action :validate_api_token!,
|
|
only: %i[search_user_names object_statuses]
|
|
|
|
def search_user_names
|
|
name = params[:name]
|
|
limit = (params[:limit] || 5).to_i.clamp(0, 15)
|
|
users = users_for_name(name, limit: limit)
|
|
if !Rails.env.production? && name == "error"
|
|
render status: 500, json: { error: "an error!" }
|
|
else
|
|
render json: { users: users }
|
|
end
|
|
end
|
|
|
|
def object_statuses
|
|
fa_ids = (params[:fa_ids] || []).reject(&:blank?).map(&:to_i)
|
|
url_names = (params[:url_names] || []).reject(&:blank?)
|
|
|
|
url_name_to_user =
|
|
Domain::User::FaUser
|
|
.where(url_name: url_names)
|
|
.map { |user| [T.must(user.url_name), user] }
|
|
.to_h
|
|
|
|
fa_id_to_post =
|
|
Domain::Post::FaPost
|
|
.includes(:file)
|
|
.where(fa_id: fa_ids)
|
|
.map { |post| [T.must(post.fa_id), post] }
|
|
.to_h
|
|
|
|
posts_response = {}
|
|
users_response = {}
|
|
|
|
fa_ids.each do |fa_id|
|
|
post = fa_id_to_post[fa_id]
|
|
|
|
if post
|
|
post_state =
|
|
if post.file.present?
|
|
"have_file"
|
|
elsif post.scanned_at?
|
|
"scanned_post"
|
|
else
|
|
post.state
|
|
end
|
|
|
|
post_response = {
|
|
state: post_state,
|
|
seen_at: time_ago_or_never(post.created_at),
|
|
object_url: request.base_url + helpers.domain_post_path(post),
|
|
post_scan: {
|
|
last_at: time_ago_or_never(post.scanned_at),
|
|
due_for_scan: !post.scanned_at?,
|
|
},
|
|
file_scan: {
|
|
last_at: time_ago_or_never(post.file&.created_at),
|
|
due_for_scan: !post.file&.created_at?,
|
|
},
|
|
}
|
|
else
|
|
post_response = { state: "not_seen" }
|
|
end
|
|
|
|
posts_response[fa_id] = post_response
|
|
end
|
|
|
|
url_names.each do |url_name|
|
|
user = url_name_to_user[url_name]
|
|
|
|
if user
|
|
user_response = {
|
|
created_at: time_ago_or_never(user.created_at),
|
|
state: user.state,
|
|
object_url: request.base_url + helpers.domain_user_path(user),
|
|
page_scan: {
|
|
last_at: time_ago_or_never(user.scanned_page_at),
|
|
due_for_scan: user.due_for_page_scan?,
|
|
},
|
|
gallery_scan: {
|
|
last_at: time_ago_or_never(user.scanned_gallery_at),
|
|
due_for_scan: user.due_for_gallery_scan?,
|
|
},
|
|
favs_scan: {
|
|
last_at: time_ago_or_never(user.scanned_favs_at),
|
|
due_for_scan: user.due_for_favs_scan?,
|
|
},
|
|
}
|
|
else
|
|
user_response = { state: "not_seen" }
|
|
end
|
|
users_response[url_name] = user_response
|
|
end
|
|
|
|
render json: { posts: posts_response, users: users_response }
|
|
end
|
|
|
|
def enqueue_objects
|
|
@enqueue_counts ||= Hash.new { |h, k| h[k] = 0 }
|
|
|
|
fa_ids = (params[:fa_ids] || []).map(&:to_i)
|
|
url_names = (params[:url_names] || [])
|
|
url_names_to_enqueue = Set.new(params[:url_names_to_enqueue] || [])
|
|
|
|
fa_id_to_post =
|
|
Domain::Fa::Post
|
|
.includes(:file)
|
|
.where(fa_id: fa_ids)
|
|
.map { |post| [post.fa_id, post] }
|
|
.to_h
|
|
|
|
url_name_to_user =
|
|
Domain::Fa::User
|
|
.where(url_name: url_names)
|
|
.map { |user| [user.url_name, user] }
|
|
.to_h
|
|
|
|
fa_ids.each do |fa_id|
|
|
post = fa_id_to_post[fa_id]
|
|
defer_post_scan(post, fa_id)
|
|
end
|
|
|
|
url_names.each do |url_name|
|
|
user = url_name_to_user[url_name]
|
|
defer_user_scan(user, url_name, url_names_to_enqueue.include?(url_name))
|
|
end
|
|
|
|
enqueue_deferred!
|
|
|
|
render json: {
|
|
post_scans: @enqueue_counts[Domain::Fa::Job::ScanPostJob],
|
|
post_files: @enqueue_counts[Domain::Fa::Job::ScanFileJob],
|
|
user_pages: @enqueue_counts[Domain::Fa::Job::UserPageJob],
|
|
user_galleries: @enqueue_counts[Domain::Fa::Job::UserGalleryJob],
|
|
}
|
|
end
|
|
|
|
def similar_users
|
|
url_name = params[:url_name]
|
|
exclude_url_name = params[:exclude_url_name]
|
|
|
|
user = Domain::User::FaUser.find_by(url_name: url_name)
|
|
if user.nil?
|
|
render status: 404,
|
|
json: {
|
|
error: "user '#{url_name}' not found",
|
|
error_type: "user_not_found",
|
|
}
|
|
return
|
|
end
|
|
|
|
all_similar_users = helpers.similar_users_by_followed(user, limit: 10)
|
|
|
|
if all_similar_users.nil?
|
|
render status: 500,
|
|
json: {
|
|
error:
|
|
"user '#{url_name}' has not had recommendations computed yet",
|
|
error_type: "recs_not_computed",
|
|
}
|
|
return
|
|
end
|
|
|
|
all_similar_users = users_list_to_similar_list(all_similar_users)
|
|
|
|
not_followed_similar_users = nil
|
|
if exclude_url_name
|
|
exclude_folowed_by_user =
|
|
Domain::User::FaUser.find_by(url_name: exclude_url_name)
|
|
not_followed_similar_users =
|
|
if exclude_folowed_by_user.nil?
|
|
# TODO - enqueue a manual UserFollowsJob for this user and have client
|
|
# re-try the request later
|
|
render status: 500,
|
|
json: {
|
|
error: "user '#{exclude_url_name}' not found",
|
|
error_type: "exclude_user_not_found",
|
|
}
|
|
elsif exclude_folowed_by_user.scanned_follows_at.nil?
|
|
render status: 500,
|
|
json: {
|
|
error:
|
|
"user '#{exclude_url_name}' followers list hasn't been scanned",
|
|
error_type: "exclude_user_not_scanned",
|
|
}
|
|
else
|
|
users_list_to_similar_list(
|
|
helpers.similar_users_by_followed(
|
|
user,
|
|
limit: 10,
|
|
exclude_followed_by: exclude_folowed_by_user,
|
|
),
|
|
)
|
|
end
|
|
end
|
|
|
|
render json: {
|
|
all: all_similar_users,
|
|
not_followed: not_followed_similar_users,
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def get_best_user_page_http_log_entry_for(user)
|
|
for_path =
|
|
proc do |uri_path|
|
|
HttpLogEntry
|
|
.where(
|
|
uri_scheme: "https",
|
|
uri_host: "www.furaffinity.net",
|
|
uri_path: uri_path,
|
|
)
|
|
.order(created_at: :desc)
|
|
.first
|
|
&.response
|
|
end
|
|
|
|
for_hle_id =
|
|
proc { |hle_id| hle_id && HttpLogEntry.find_by(id: hle_id)&.response }
|
|
|
|
# older versions don't end in a trailing slash
|
|
hle_id = user.log_entry_detail && user.log_entry_detail["last_user_page_id"]
|
|
for_hle_id.call(hle_id) || for_path.call("/user/#{user.url_name}/") ||
|
|
for_path.call("/user/#{user.url_name}")
|
|
end
|
|
|
|
def defer_post_scan(post, fa_id)
|
|
if !post || !post.scanned?
|
|
defer_manual(Domain::Fa::Job::ScanPostJob, { fa_id: fa_id }, -17)
|
|
end
|
|
|
|
if post && post.file_uri && !post.file.present?
|
|
return(
|
|
defer_manual(
|
|
Domain::Fa::Job::ScanFileJob,
|
|
{ post: post },
|
|
-15,
|
|
"static_file",
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
def defer_user_scan(user, url_name, highpri)
|
|
if !user || user.due_for_page_scan?
|
|
defer_manual(
|
|
Domain::Fa::Job::UserPageJob,
|
|
{ url_name: url_name },
|
|
highpri ? -16 : -6,
|
|
)
|
|
return
|
|
end
|
|
|
|
if !user || user.due_for_gallery_scan?
|
|
defer_manual(
|
|
Domain::Fa::Job::UserGalleryJob,
|
|
{ url_name: url_name },
|
|
highpri ? -14 : -4,
|
|
)
|
|
return
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
def defer_manual(klass, args, priority, queue = "manual")
|
|
@@enqueue_deduper ||= Set.new
|
|
return unless @@enqueue_deduper.add?([klass, args, priority])
|
|
|
|
@deferred_jobs ||= []
|
|
@deferred_jobs << [klass, args, priority, queue]
|
|
@enqueue_counts[klass] += 1
|
|
end
|
|
|
|
def enqueue_deferred!
|
|
GoodJob::Bulk.enqueue do
|
|
while job = (@deferred_jobs || []).shift
|
|
klass, args, priority, queue = job
|
|
klass.set(priority: priority, queue: queue).perform_later(args)
|
|
end
|
|
end
|
|
end
|
|
|
|
def time_ago_or_never(time)
|
|
if time
|
|
helpers.time_ago_in_words(time, include_seconds: true) + " ago"
|
|
else
|
|
"never"
|
|
end
|
|
end
|
|
|
|
def users_for_name(name, limit: 10)
|
|
users =
|
|
Domain::Fa::User
|
|
.where(
|
|
[
|
|
"(name ilike :name) OR (url_name ilike :name)",
|
|
{ name: "#{ReduxApplicationRecord.sanitize_sql_like(name)}%" },
|
|
],
|
|
)
|
|
.includes(:avatar)
|
|
.select(:id, :state, :state_detail, :log_entry_detail, :name, :url_name)
|
|
.select(
|
|
"(SELECT COUNT(*) FROM domain_fa_posts WHERE creator_id = domain_fa_users.id) as num_posts",
|
|
)
|
|
.order(name: :asc)
|
|
.limit(limit)
|
|
|
|
users.map do |user|
|
|
{
|
|
id: user.id,
|
|
name: user.name,
|
|
url_name: user.url_name,
|
|
thumb: helpers.fa_user_avatar_path(user, thumb: "64-avatar"),
|
|
show_path: domain_fa_user_path(user.url_name),
|
|
# `num_posts` is a manually added column, so we need to use T.unsafe to
|
|
# access it
|
|
num_posts: T.unsafe(user).num_posts,
|
|
}
|
|
end
|
|
end
|
|
|
|
sig do
|
|
params(users_list: T::Array[Domain::User::FaUser]).returns(
|
|
T::Array[T::Hash[Symbol, T.untyped]],
|
|
)
|
|
end
|
|
def users_list_to_similar_list(users_list)
|
|
users_list.map do |user|
|
|
profile_thumb_url = user.avatar&.log_entry&.uri_str
|
|
profile_thumb_url ||
|
|
begin
|
|
profile_page_response = get_best_user_page_http_log_entry_for(user)
|
|
if profile_page_response
|
|
parser =
|
|
Domain::Fa::Parser::Page.new(
|
|
profile_page_response.contents,
|
|
require_logged_in: false,
|
|
)
|
|
profile_thumb_url = parser.user_page.profile_thumb_url
|
|
else
|
|
if user.due_for_follows_scan?
|
|
Domain::Fa::Job::UserFollowsJob.set(
|
|
{ priority: -20 },
|
|
).perform_later({ user: user })
|
|
end
|
|
if user.due_for_page_scan?
|
|
Domain::Fa::Job::UserPageJob.set({ priority: -20 }).perform_later(
|
|
{ user: user },
|
|
)
|
|
end
|
|
end
|
|
rescue StandardError
|
|
logger.error("error getting profile_thumb_url: #{$!.message}")
|
|
end
|
|
|
|
{
|
|
name: user.name,
|
|
url_name: user.url_name,
|
|
profile_thumb_url: profile_thumb_url,
|
|
external_url: "https://www.furaffinity.net/user/#{user.url_name}/",
|
|
refurrer_url: request.base_url + helpers.domain_user_path(user),
|
|
}
|
|
end
|
|
end
|
|
|
|
API_TOKENS = {
|
|
"a4eb03ac-b33c-439c-9b51-a834d1c5cf48" => "dymk",
|
|
"56cc81fe-8c00-4436-8981-4580eab00e66" => "taargus",
|
|
"9c38727f-f11d-41de-b775-0effd86d520c" => "xjal",
|
|
"e38c568f-a24d-4f26-87f0-dfcd898a359d" => "fyacin",
|
|
"41fa1144-d4cd-11ed-afa1-0242ac120002" => "soft_fox_lad",
|
|
"9b3cf444-5913-4efb-9935-bf26501232ff" => "syfaro",
|
|
}
|
|
|
|
def validate_api_token!
|
|
api_token = request.params[:api_token]
|
|
api_user_name = API_TOKENS[api_token]
|
|
return if api_user_name
|
|
return if VpnOnlyRouteConstraint.new.matches?(request)
|
|
render status: 403, json: { error: "not authenticated" }
|
|
end
|
|
end
|