Files
redux-scraper/app/controllers/domain/users_controller.rb
2025-03-01 20:23:44 +00:00

254 lines
6.8 KiB
Ruby

# typed: true
class Domain::UsersController < DomainController
extend T::Sig
extend T::Helpers
before_action :set_user!, only: %i[show followed_by following]
before_action :set_post!, only: %i[users_faving_post]
skip_before_action :authenticate_user!,
only: %i[
show
search_by_name
users_faving_post
similar_users
]
# GET /users
sig(:final) { void }
def index
authorize Domain::User
@users = policy_scope(Domain::User).order(created_at: :desc)
end
sig(:final) { void }
def followed_by
@user = T.must(@user)
authorize @user
@users =
@user
.followed_by_users
.includes(avatar: :log_entry)
.page(params[:page])
.per(50)
@index_type = :followed_by
render :index
end
sig(:final) { void }
def following
@user = T.must(@user)
authorize @user
@users =
@user
.followed_users
.includes(avatar: :log_entry)
.page(params[:page])
.per(50)
@index_type = :following
render :index
end
sig(:final) { void }
def users_faving_post
@post = T.must(@post)
authorize @post
@users =
T
.unsafe(@post)
.faving_users
.includes(avatar: :log_entry)
.page(params[:page])
.per(50)
@index_type = :users_faving_post
render :index
end
# GET /users/:id
sig(:final) { void }
def show
authorize @user
end
sig(:final) { void }
def search_by_name
authorize Domain::User
name = params[:name]&.downcase
name = ReduxApplicationRecord.sanitize_sql_like(name)
@user_search_names =
Domain::UserSearchName
.select("domain_user_search_names.*, domain_users.*")
.select("levenshtein(name, '#{name}') as distance")
.select(
"(SELECT COUNT(*) FROM domain_user_post_creations dupc WHERE dupc.user_id = domain_users.id) as num_posts",
)
.joins(:user)
.where(
"(name ilike ?) OR (similarity(dmetaphone(name), dmetaphone(?)) > 0.8)",
"%#{name}%",
name,
)
.where(
"NOT EXISTS (
SELECT 1
FROM domain_user_search_names dns2
WHERE dns2.user_id = domain_user_search_names.user_id
AND levenshtein(dns2.name, ?) < levenshtein(domain_user_search_names.name, ?)
)",
name,
name,
)
.order("distance ASC")
.limit(10)
end
sig { void }
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 =
users_similar_to_by_followers(user, limit: 10).map do |u|
user_to_similarity_entry(u)
end
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
not_followed_similar_users = nil
if exclude_url_name
exclude_followed_by =
Domain::User::FaUser.find_by(url_name: exclude_url_name)
if exclude_followed_by.nil?
render status: 500,
json: {
error: "user '#{exclude_url_name}' not found",
error_type: "exclude_user_not_found",
}
return
elsif exclude_followed_by.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",
}
return
else
not_followed_similar_users =
users_similar_to_by_followers(
user,
limit: 10,
exclude_followed_by: exclude_followed_by,
).map { |u| user_to_similarity_entry(u) }
end
end
render json: {
all: all_similar_users,
not_followed: not_followed_similar_users,
}
end
private
sig { override.returns(DomainController::DomainParamConfig) }
def self.param_config
DomainController::DomainParamConfig.new(
user_id_param: :id,
post_id_param: :domain_post_id,
post_group_id_param: :domain_post_group_id,
)
end
sig { params(user: Domain::User::FaUser).returns(T::Hash[Symbol, T.untyped]) }
def user_to_similarity_entry(user)
profile_thumb_url = user.avatar&.log_entry&.uri_str
profile_thumb_url ||=
begin
pp_log_entry = get_best_user_page_http_log_entry_for(user)
if pp_log_entry && (response_bytes = pp_log_entry.response_bytes)
parser =
Domain::Fa::Parser::Page.new(
response_bytes,
require_logged_in: false,
)
parser.user_page.profile_thumb_url
end
rescue StandardError
logger.error("error getting profile_thumb_url: #{$!.message}")
end || "https://a.furaffinity.net/0/#{user.url_name}.gif"
{
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
sig { params(user: Domain::User::FaUser).returns(T.nilable(HttpLogEntry)) }
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
end
# older versions don't end in a trailing slash
user.last_user_page_log_entry || for_path.call("/user/#{user.url_name}/") ||
for_path.call("/user/#{user.url_name}")
end
sig do
params(
user: Domain::User::FaUser,
limit: Integer,
exclude_followed_by: T.nilable(Domain::User::FaUser),
).returns(T::Array[Domain::User::FaUser])
end
def users_similar_to_by_followers(user, limit: 10, exclude_followed_by: nil)
factors = Domain::Factors::UserUserFollowToFactors.find_by(user: user)
return [] if factors.nil?
relation =
Domain::NeighborFinder
.find_neighbors(factors)
.limit(limit)
.includes(:user)
if exclude_followed_by
relation =
relation.where.not(
user_id: exclude_followed_by.followed_users.select(:to_id),
)
end
relation.map(&:user)
end
end