Files
redux-scraper/app/helpers/domain/users_helper.rb
2025-08-08 00:40:28 +00:00

270 lines
8.5 KiB
Ruby

# typed: strict
module Domain::UsersHelper
extend T::Sig
extend T::Helpers
include HelpersInterface
include Pundit::Authorization
abstract!
sig { params(user: Domain::User).returns(T.nilable(String)) }
def user_avatar_path_for_view(user)
if avatar = user.avatar
domain_user_avatar_img_src_path(avatar, thumb: "32-avatar")
end
end
sig { params(user: Domain::User).returns(T.nilable(String)) }
def user_name_for_view(user)
if avatar = user.avatar
domain_user_avatar_img_src_path(avatar, thumb: "32-avatar")
end
end
sig { params(user: Domain::User).returns(T.nilable(String)) }
def domain_user_registered_at_string_for_view(user)
ts = domain_user_registered_at_ts_for_view(user)
ts ? time_ago_in_words(ts) : nil
end
sig do
params(user: Domain::User).returns(T.nilable(ActiveSupport::TimeWithZone))
end
def domain_user_registered_at_ts_for_view(user)
case user
when Domain::User::FaUser, Domain::User::E621User
user.registered_at
else
nil
end
end
sig do
params(
avatar: T.nilable(Domain::UserAvatar),
thumb: T.nilable(String),
).returns(String)
end
def domain_user_avatar_img_src_path(avatar, thumb: nil)
cache_key = ["domain_user_avatar_img_src_path", avatar&.id, thumb]
Rails
.cache
.fetch(cache_key, expires_in: 1.day) do
if (sha256 = avatar&.log_entry&.response_sha256)
Rails.application.routes.url_helpers.blob_path(
HexUtil.bin2hex(sha256),
format: "jpg",
thumb: thumb,
)
elsif avatar && avatar.state_file_404? &&
(sha256 = avatar.last_log_entry&.response_sha256)
Rails.application.routes.url_helpers.blob_path(
HexUtil.bin2hex(sha256),
format: "jpg",
thumb: thumb,
)
else
# default / 'not found' avatar image
# "/blobs/9080fd4e7e23920eb2dccfe2d86903fc3e748eebb2e5aa8c657bbf6f3d941cdc/contents.jpg"
asset_path("user-circle.svg")
end
end
end
sig do
params(
avatar: T.nilable(Domain::UserAvatar),
thumb: T.nilable(String),
).returns(String)
end
def domain_user_avatar_img_tag(avatar, thumb: nil)
if (sha256 = avatar&.log_entry&.response_sha256)
image_tag(
blob_path(HexUtil.bin2hex(sha256), format: "jpg", thumb: thumb),
class: "inline-block h-4 w-4 flex-shrink-0 rounded-sm object-cover",
alt: avatar&.user&.name_for_view || "user avatar",
)
else
raw <<~SVG
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
SVG
end
end
sig { params(user: Domain::User).returns(String) }
def site_icon_path_for_user(user)
case user
when Domain::User::E621User
asset_path("domain-icons/e621.png")
when Domain::User::FaUser
asset_path("domain-icons/fa.png")
when Domain::User::InkbunnyUser
asset_path("domain-icons/inkbunny.png")
when Domain::User::SofurryUser
asset_path("domain-icons/sofurry.png")
when Domain::User::BlueskyUser
asset_path("domain-icons/bluesky.png")
else
Kernel.raise "Unknown user type: #{user.class}"
end
end
sig { params(user: Domain::User).returns(String) }
def domain_user_path(user)
"#{domain_users_path}/#{user.to_param}"
end
sig { returns(String) }
def domain_users_path
"/users"
end
sig { params(user: Domain::User).returns(String) }
def domain_user_posts_path(user)
"#{domain_user_path(user)}/posts"
end
sig { params(user: Domain::User).returns(String) }
def domain_user_favorites_path(user)
"#{domain_user_path(user)}/favorites"
end
sig { params(user: Domain::User).returns(String) }
def domain_user_followed_by_path(user)
"#{domain_user_path(user)}/followed_by"
end
sig { params(user: Domain::User).returns(String) }
def domain_user_following_path(user)
"#{domain_user_path(user)}/following"
end
sig { params(user: Domain::User, kind: String).returns(String) }
def tracked_objects_domain_user_job_events_path(user, kind:)
unless Domain::UserJobEvent::AddTrackedObject.kinds.include?(kind)
Kernel.raise "invalid kind: #{kind}"
end
"#{domain_user_path(user)}/job_events/tracked_objects/#{kind}"
end
sig { params(user: Domain::User).returns(String) }
def tracked_objects_domain_user_job_events_kinds_path(user)
"#{domain_user_path(user)}/job_events/tracked_objects"
end
sig { params(user: Domain::User, kind: String).returns(String) }
def enqueue_scan_job_domain_user_job_events_path(user, kind:)
unless Domain::UserJobEvent::AddTrackedObject.kinds.include?(kind)
Kernel.raise "invalid kind: #{kind}"
end
"#{domain_user_path(user)}/job_events/enqueue_scan_job/#{kind}"
end
class StatRow < T::ImmutableStruct
include T::Struct::ActsAsComparable
const :name, String
const :value,
T.nilable(
T.any(String, Integer, HasTimestampsWithDueAt::TimestampScanInfo),
)
const :link_to, T.nilable(String)
const :fa_icon_class, T.nilable(String)
const :hover_title, T.nilable(String)
end
sig { params(user: Domain::User).returns(T::Array[StatRow]) }
def stat_rows_for_user(user)
rows = T.let([], T::Array[StatRow])
if user.has_faved_posts?
rows << StatRow.new(name: "Favorites", value: user.user_post_favs.size)
end
if user.has_followed_by_users?
can_view_link = policy(user).followed_by?
rows << StatRow.new(
name: "Followed by",
value: user.user_user_follows_to.size,
link_to: can_view_link ? domain_user_followed_by_path(user) : nil,
)
end
if user.has_followed_users?
can_view_link = policy(user).following?
rows << StatRow.new(
name: "Following",
value: user.user_user_follows_from.size,
link_to: can_view_link ? domain_user_following_path(user) : nil,
)
end
can_view_timestamps = policy(user).view_page_scanned_at_timestamps?
can_view_log_entries = policy(user).view_log_entries?
icon_for =
Kernel.proc do |due_for_scan|
due_for_scan ? "fa-hourglass-half" : "fa-check"
end
if user.is_a?(Domain::User::FaUser) && can_view_timestamps
if can_view_log_entries && hle = user.guess_last_user_page_log_entry
rows << StatRow.new(
name: "Page scanned",
value: user.page_scan,
link_to: log_entry_path(hle),
fa_icon_class: icon_for.call(user.page_scan.due?),
hover_title: user.page_scan.interval.inspect,
)
else
rows << StatRow.new(
name: "Page",
value: user.page_scan,
fa_icon_class: icon_for.call(user.page_scan.due?),
hover_title: user.page_scan.interval.inspect,
)
end
rows << StatRow.new(
name: "Favs",
value: user.favs_scan,
link_to:
tracked_objects_domain_user_job_events_path(user, kind: "favs"),
fa_icon_class: icon_for.call(user.favs_scan.due?),
hover_title: user.favs_scan.interval.inspect,
)
rows << StatRow.new(
name: "Followed by",
value: user.followed_by_scan,
fa_icon_class: icon_for.call(user.followed_by_scan.due?),
hover_title: user.followed_by_scan.interval.inspect,
)
rows << StatRow.new(
name: "Following",
value: user.follows_scan,
fa_icon_class: icon_for.call(user.follows_scan.due?),
hover_title: user.follows_scan.interval.inspect,
)
rows << StatRow.new(
name: "Gallery",
value: user.gallery_scan,
fa_icon_class: icon_for.call(user.gallery_scan.due?),
hover_title: user.gallery_scan.interval.inspect,
)
elsif user.is_a?(Domain::User::E621User) && can_view_timestamps
if user.favs_are_hidden
rows << StatRow.new(name: "Favorites hidden", value: "yes")
else
rows << StatRow.new(
name: "Server favorites",
value: user.num_other_favs_cached,
)
end
rows << StatRow.new(
name: "Favorites",
value: user.favs_scan,
fa_icon_class: icon_for.call(user.favs_scan.due?),
hover_title: user.favs_scan.interval.inspect,
)
end
rows
end
end