Files
redux-scraper/app/helpers/domain/bluesky_post_helper.rb
Dylan Knutson 73f6f77596 Add comprehensive Bluesky tests to posts_helper_spec
- Add extensive test coverage for Bluesky user profile URL matching
- Test handle-based and DID-based profile URLs with various formats
- Add edge cases and error condition tests for malformed URLs
- Test user avatar icon path and model path generation
- Verify fallback behavior for users without display names
- Test priority logic for handle vs DID lookup
- Add tests for special characters and very long handles
- All 82 tests now pass successfully
2025-08-17 00:10:31 +00:00

211 lines
5.4 KiB
Ruby

# typed: strict
# frozen_string_literal: true
module Domain::BlueskyPostHelper
extend T::Sig
include ActionView::Helpers::UrlHelper
include HelpersInterface
include Domain::PostsHelper
class FacetPart < T::Struct
const :type, Symbol
const :value, String
end
sig do
params(text: String, facets: T.nilable(T::Array[T.untyped])).returns(
T.nilable(String),
)
end
def render_bsky_post_facets(text, facets = nil)
return text if facets.blank?
facets =
begin
facets.map { |facet| Bluesky::Text::Facet.from_hash(facet) }
rescue => e
Rails.logger.error("error parsing Bluesky facets: #{e.message}")
return text
end
result_parts = T.let([], T::Array[FacetPart])
last_end = 0
# Sort facets by start position to handle them in order
sorted_facets = facets.sort_by(&:byteStart)
sorted_facets.each do |facet|
if facet.byteStart < 0 || facet.byteEnd <= facet.byteStart ||
facet.byteEnd > text.bytesize
next
end
# Skip overlapping facets
next if facet.byteStart < last_end
# Add text before this facet
if facet.byteStart > last_end
before_text = text.byteslice(last_end, facet.byteStart - last_end)
if before_text
result_parts << FacetPart.new(type: :text, value: before_text)
end
end
# Extract the facet text using byteslice for accurate character extraction
facet_text =
text.byteslice(facet.byteStart, facet.byteEnd - facet.byteStart)
next unless facet_text # Skip if byteslice returns nil
# Process the facet
rendered_facet = render_facet(facet, facet_text)
result_parts << FacetPart.new(type: :facet, value: rendered_facet)
last_end = facet.byteEnd
end
# Add remaining text after the last facet
if last_end < text.bytesize
remaining_text = text.byteslice(last_end, text.bytesize - last_end)
if remaining_text
result_parts << FacetPart.new(type: :text, value: remaining_text)
end
end
result_parts
.map do |part|
case part.type
when :text
part.value.gsub("\n", "<br />")
when :facet
part.value
end
end
.join
.html_safe
end
private
sig do
params(facet: Bluesky::Text::Facet, facet_text: String).returns(String)
end
def render_facet(facet, facet_text)
return facet_text unless facet.features.any?
# Process the first feature (Bluesky facets typically have one feature per facet)
feature = facet.features.first
return facet_text unless feature.is_a?(Bluesky::Text::FacetFeature)
case feature
when Bluesky::Text::FacetFeatureMention
render_mention_facet(feature, facet_text)
when Bluesky::Text::FacetFeatureURI
render_link_facet(feature, facet_text)
when Bluesky::Text::FacetFeatureTag
render_tag_facet(feature, facet_text)
else
# Unknown facet type, return original text
facet_text
end
end
sig do
params(
feature: Bluesky::Text::FacetFeatureMention,
facet_text: String,
).returns(String)
end
def render_mention_facet(feature, facet_text)
did = feature.did
return facet_text unless did.present?
# Try to find the user in the database
user = Domain::User::BlueskyUser.find_by(did: did)
if user
# Render the inline user partial
render(
partial: "domain/has_description_html/inline_link_domain_user",
locals: {
user: user,
link_text: facet_text,
visual_style: "description-section-link-light",
},
)
else
# Render external link to Bluesky profile
render(
partial: "domain/has_description_html/external_link",
locals: {
link_text: facet_text,
url: "https://bsky.app/profile/#{did}",
},
)
end
end
sig do
params(feature: Bluesky::Text::FacetFeatureURI, facet_text: String).returns(
String,
)
end
def render_link_facet(feature, facet_text)
uri = feature.uri
return facet_text unless uri.present?
source = link_for_source(uri)
if source.present? && (model = source.model)
case model
when Domain::Post
return(
render(
partial: "domain/has_description_html/inline_link_domain_post",
locals: {
post: model,
link_text: facet_text,
visual_style: "description-section-link-light",
},
)
)
when Domain::User
return(
render(
partial: "domain/has_description_html/inline_link_domain_user",
locals: {
user: model,
link_text: facet_text,
visual_style: "description-section-link-light",
},
)
)
end
end
render(
partial: "domain/has_description_html/external_link",
locals: {
link_text: facet_text,
url: uri,
},
)
end
sig do
params(feature: Bluesky::Text::FacetFeatureTag, facet_text: String).returns(
String,
)
end
def render_tag_facet(feature, facet_text)
tag = feature.tag
return facet_text unless tag.present?
render(
partial: "domain/has_description_html/external_link",
locals: {
link_text: facet_text,
url: "https://bsky.app/hashtag/#{tag}",
},
)
end
end