Class: Tempest::AvatarStore
- Inherits:
-
Object
- Object
- Tempest::AvatarStore
- Defined in:
- lib/tempest/avatar_store.rb
Overview
Resolves Bluesky DIDs to a local PNG file path for the actor’s avatar. Mirrors the shape of HandleResolver: an injected client speaks XRPC for ‘app.bsky.actor.getProfile`, and the result is cached in-process so the PDS isn’t hit on every event.
Disk layout: avatars live under ‘cache_dir/` as “<sanitized-did>__<avatar-cid>.png”. The avatar CID is read from the tail of the avatar URL so that re-uploaded avatars (which receive a new CID) invalidate the cache without server-side coordination.
Defined Under Namespace
Classes: DefaultProfileClient
Constant Summary collapse
- NOT_FOUND =
Sentinel for “we tried, there is no avatar” — distinct from “we haven’t looked yet” (nil). Mirrors the pattern in HandleResolver.
Object.new.freeze
Class Method Summary collapse
-
.default_converter ⇒ Object
Production format normalizer: uses libvips (via ruby-vips) to crop-fit the avatar into a 128x128, 8-bit sRGB PNG in-process.
-
.default_fetcher ⇒ Object
Production HTTP fetcher used when no fetcher is injected.
Instance Method Summary collapse
-
#initialize(client:, cache_dir:, fetcher: nil, converter: nil, async: true, executor: nil) ⇒ AvatarStore
constructor
A new instance of AvatarStore.
- #path_for(did) ⇒ Object
- #seed(did, path) ⇒ Object
Constructor Details
#initialize(client:, cache_dir:, fetcher: nil, converter: nil, async: true, executor: nil) ⇒ AvatarStore
Returns a new instance of AvatarStore.
48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/tempest/avatar_store.rb', line 48 def initialize(client:, cache_dir:, fetcher: nil, converter: nil, async: true, executor: nil) @client = client @cache_dir = cache_dir @fetcher = fetcher || self.class.default_fetcher @converter = converter || self.class.default_converter @async = async @executor = executor || method(:default_executor) @cache = {} @pending = {} @mutex = Mutex.new FileUtils.mkdir_p(@cache_dir) end |
Class Method Details
.default_converter ⇒ Object
Production format normalizer: uses libvips (via ruby-vips) to crop-fit the avatar into a 128x128, 8-bit sRGB PNG in-process. The center crop pads non-square inputs so the Kitty graphics protocol can render at a consistent 1-row, 2-col aspect, and the explicit 8-bit pngsave avoids the 16-bit output that some ImageMagick builds emit, which kitty refuses to draw.
83 84 85 86 87 88 89 90 |
# File 'lib/tempest/avatar_store.rb', line 83 def self.default_converter @default_converter ||= lambda do |bytes, content_type:| require "vips" image = Vips::Image.thumbnail_buffer(bytes, 128, height: 128, crop: :centre) image = image.colourspace("srgb") unless image.interpretation == :srgb image.pngsave_buffer(bitdepth: 8) end end |
.default_fetcher ⇒ Object
Production HTTP fetcher used when no fetcher is injected. Returns the raw bytes and Content-Type header so the converter can pick the right input format.
64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/tempest/avatar_store.rb', line 64 def self.default_fetcher @default_fetcher ||= lambda do |url| uri = URI(url) res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http| http.get(uri.request_uri) end unless res.is_a?(Net::HTTPSuccess) raise Tempest::APIError.new(res.code.to_i, { "error" => res. }) end [res.body, res["content-type"].to_s] end end |
Instance Method Details
#path_for(did) ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/tempest/avatar_store.rb', line 92 def path_for(did) cached = @mutex.synchronize { @cache[did] } return cached_value(cached) unless cached.nil? if @async && (path = cached_file_for(did)) @mutex.synchronize { @cache[did] = path } return path end if @async enqueue_resolve(did) nil else resolve_and_cache(did) end end |
#seed(did, path) ⇒ Object
109 110 111 |
# File 'lib/tempest/avatar_store.rb', line 109 def seed(did, path) @mutex.synchronize { @cache[did] = path } end |