From 60094c93162131ff4e0056ba9921ca0164f376bb Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 13 Jun 2026 18:41:43 -0300 Subject: [PATCH] Simplify handle resolution to a single AppView call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveHandle is a public read served by the AppView, so drop the PDS-first auth, the multi-endpoint fallback loop and the threaded service/account args in favor of one unauthenticated GET against config public_appview — matching how BlueskyAnalytics already reads from the AppView. The try/catch stays so a network error degrades the mention to plain text instead of failing the post. Remove the now-obsolete fallback test. --- app/Services/Social/BlueskyPublisher.php | 56 ++++++------------- .../Services/Social/BlueskyPublisherTest.php | 37 ------------ 2 files changed, 17 insertions(+), 76 deletions(-) diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index b44060ab..a47115e1 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -61,7 +61,7 @@ public function publish(PostPlatform $postPlatform): array // Parse facets (links, mentions, hashtags) from text $text = $content ?? ''; - $facets = $this->parseFacets($text, $service, $account); + $facets = $this->parseFacets($text); // Create post record $record = [ @@ -167,7 +167,7 @@ private function uploadBlob(SocialAccount $account, string $service, string $url } } - private function parseFacets(string $text, ?string $service = null, ?SocialAccount $account = null): array + private function parseFacets(string $text): array { $facets = []; @@ -214,7 +214,7 @@ private function parseFacets(string $text, ?string $service = null, ?SocialAccou // A mention facet needs the target's DID, not the handle; skip it if unresolvable. // Cache by key (not ??) so an unresolvable handle is resolved once, not per occurrence. if (! array_key_exists($handle, $didCache)) { - $didCache[$handle] = $this->resolveHandleToDid($handle, $service, $account); + $didCache[$handle] = $this->resolveHandleToDid($handle); } $did = $didCache[$handle]; if ($did === null) { @@ -272,48 +272,26 @@ private function parseFacets(string $text, ?string $service = null, ?SocialAccou /** * Resolve a Bluesky handle to its DID via com.atproto.identity.resolveHandle. * - * Tries the account's own PDS first (authenticated), then falls back to the - * public AppView and the configured default service. Returns null on failure - * so the caller can skip the mention facet instead of sending an invalid record. + * resolveHandle is a public read served by the AppView (no auth). Returns + * null on any failure so the caller can skip the mention facet and publish + * the @handle as plain text instead of an invalid record. */ - private function resolveHandleToDid(string $handle, ?string $service = null, ?SocialAccount $account = null): ?string + private function resolveHandleToDid(string $handle): ?string { - // Normalize so a trailing slash neither produces a double-slash URL nor - // breaks the `=== $service` check below (which decides authentication). - $service = $service !== null ? rtrim($service, '/') : null; + $appView = (string) config('trypost.platforms.bluesky.public_appview'); - $endpoints = array_values(array_unique(array_filter([ - $service, - config('trypost.platforms.bluesky.public_appview'), - config('trypost.platforms.bluesky.default_service'), - ]))); + try { + $response = $this->socialHttp()->get( + "{$appView}/xrpc/com.atproto.identity.resolveHandle", + ['handle' => $handle], + ); - foreach ($endpoints as $endpoint) { - try { - $request = $this->socialHttp(); - if ($account && $endpoint === $service) { - $request = $request->withToken($account->access_token); - } + $did = $response->successful() ? data_get($response->json(), 'did') : null; - $response = $request->get( - "{$endpoint}/xrpc/com.atproto.identity.resolveHandle", - ['handle' => $handle], - ); - - $did = $response->successful() ? data_get($response->json(), 'did') : null; - if (is_string($did) && str_starts_with($did, 'did:')) { - return $did; - } - } catch (Throwable $e) { - Log::debug('Bluesky handle resolution attempt failed', [ - 'handle' => $handle, - 'endpoint' => $endpoint, - 'error' => $e->getMessage(), - ]); - } + return is_string($did) && str_starts_with($did, 'did:') ? $did : null; + } catch (Throwable) { + return null; } - - return null; } private function getUtf8ByteOffset(string $text, int $charOffset): int diff --git a/tests/Feature/Services/Social/BlueskyPublisherTest.php b/tests/Feature/Services/Social/BlueskyPublisherTest.php index db740e74..8df42b21 100644 --- a/tests/Feature/Services/Social/BlueskyPublisherTest.php +++ b/tests/Feature/Services/Social/BlueskyPublisherTest.php @@ -169,43 +169,6 @@ }); }); -test('bluesky publisher falls back to the public AppView when the PDS cannot resolve', function () { - $this->post->update(['content' => 'Shout out to @friend.bsky.social']); - - $service = config('trypost.platforms.bluesky.default_service'); - $appView = config('trypost.platforms.bluesky.public_appview'); - - Http::fake([ - // The account's PDS fails to resolve; resolution must fall back to the - // public AppView rather than giving up after the first endpoint. - $service.'/xrpc/com.atproto.identity.resolveHandle*' => Http::response(['error' => 'InvalidRequest'], 400), - $appView.'/xrpc/com.atproto.identity.resolveHandle*' => Http::response(['did' => 'did:plc:friend456'], 200), - $service.'/xrpc/com.atproto.repo.createRecord' => Http::response([ - 'uri' => 'at://did:plc:testuser123/app.bsky.feed.post/3abc123xyz', - 'cid' => 'bafyreiabc123', - ], 200), - ]); - - $this->publisher->publish($this->postPlatform); - - Http::assertSent(function ($request) { - $record = $request->data()['record'] ?? null; - - if (! $record) { - return false; - } - - foreach ($record['facets'] ?? [] as $facet) { - $feature = $facet['features'][0]; - if ($feature['$type'] === 'app.bsky.richtext.facet#mention') { - return $feature['did'] === 'did:plc:friend456'; - } - } - - return false; - }); -}); - test('bluesky publisher resolves some mentions and skips the unresolvable ones', function () { $this->post->update(['content' => 'cc @good.bsky.social and @bad.bsky.social']);