Files
redux-scraper/app/controllers/domain/users_controller.rb
Dylan Knutson 1905575d19 did searching
2025-09-07 17:47:53 +00:00

291 lines
7.9 KiB
Ruby

# typed: true
class Domain::UsersController < DomainController
extend T::Sig
extend T::Helpers
before_action :set_user!,
only: %i[show followed_by following monitor_bluesky_user]
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)
if name.starts_with?("did:plc:") || name.starts_with?("did:pkh:")
@user_search_names =
Domain::UserSearchName
.select(
"domain_user_search_names.*, domain_users.*, domain_users_bluesky_aux.did",
)
.select(
"levenshtein(domain_users_bluesky_aux.did, '#{name}') as distance",
)
.where(
user: Domain::User::BlueskyUser.where("did LIKE ?", "#{name}%"),
)
.joins(:user)
.limit(10)
return
end
@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
sig { void }
def monitor_bluesky_user
user = T.cast(@user, Domain::User::BlueskyUser)
authorize user
monitor = Domain::Bluesky::MonitoredObject.build_for_user(user)
if monitor.save
Domain::Bluesky::Job::ScanUserJob.perform_later(user:)
Domain::Bluesky::Job::ScanPostsJob.perform_later(user:)
flash[:notice] = "User is now being monitored"
else
flash[
:alert
] = "Error monitoring user: #{monitor.errors.full_messages.join(", ")}"
end
redirect_to domain_user_path(user)
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
# TODO - make a typed ImmutableStruct for the return type
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
parser =
Domain::Fa::Parser::Page.from_log_entry(
pp_log_entry,
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