Simplify handle resolution to a single AppView call

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.
This commit is contained in:
Paulo Castellano 2026-06-13 18:41:43 -03:00
parent 5227cde5c7
commit 60094c9316
2 changed files with 17 additions and 76 deletions

View file

@ -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

View file

@ -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']);