254 lines
6.8 KiB
Ruby
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
|