diff --git a/.env.example b/.env.example index eb423aaa..4235fe34 100644 --- a/.env.example +++ b/.env.example @@ -81,3 +81,5 @@ LOG_SLACK_WEBHOOK_URL= FLARE_KEY= IGNITION_OPEN_AI_KEY= + +BRIDGY_MASTODON_TOKEN= diff --git a/app/Console/Commands/ReprocessMediaImages.php b/app/Console/Commands/ReprocessMediaImages.php index c5e22d1b..299f0103 100644 --- a/app/Console/Commands/ReprocessMediaImages.php +++ b/app/Console/Commands/ReprocessMediaImages.php @@ -25,20 +25,18 @@ class ReprocessMediaImages extends Command $dryRun = $this->option('dry-run'); - $this->info("Found {$media->count()} images to reprocess.".($dryRun ? ' (dry run)' : '')); + $this->info("Found {$media->count()} images to reprocess." . ($dryRun ? ' (dry run)' : '')); foreach ($media as $item) { $path = $item->path; if (! Storage::disk('public')->exists($path)) { $this->warn("{$path}: original not found on public disk, skipping."); - continue; } if ($dryRun) { $this->line("{$path} ({$item->image_widths}px wide)"); - continue; } @@ -48,8 +46,7 @@ class ReprocessMediaImages extends Command $file = Storage::disk('public')->get($path); $image = $manager->read($file); } catch (DecoderException) { - $this->warn(' Could not decode image, skipping.'); - + $this->warn(" Could not decode image, skipping."); continue; } @@ -63,9 +60,9 @@ class ReprocessMediaImages extends Command $small = $image->scale(width: 500); Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); - $this->info(' Done.'); + $this->info(" Done."); } - $this->info('Reprocessing complete.'.($dryRun ? ' (dry run — no files were changed)' : '')); + $this->info('Reprocessing complete.' . ($dryRun ? ' (dry run — no files were changed)' : '')); } } diff --git a/app/Jobs/SyndicateNoteToBluesky.php b/app/Jobs/SyndicateNoteToBluesky.php index a306801b..f9f2486b 100644 --- a/app/Jobs/SyndicateNoteToBluesky.php +++ b/app/Jobs/SyndicateNoteToBluesky.php @@ -17,11 +17,6 @@ class SyndicateNoteToBluesky implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Only try once — retrying would send Bridgy a duplicate publish webmention. - */ - public int $tries = 1; - /** * Create a new job instance. */ @@ -36,32 +31,32 @@ class SyndicateNoteToBluesky implements ShouldQueue */ public function handle(Client $guzzle): void { - $response = $guzzle->request( - 'POST', - 'https://brid.gy/publish/webmention', - [ - 'headers' => [ - 'Accept' => 'application/json', - ], - 'form_params' => [ - 'source' => $this->note->uri, - 'target' => 'https://brid.gy/publish/bluesky', - ], - 'http_errors' => false, - ] - ); - - $body = json_decode((string) $response->getBody(), true); - - if ($response->getStatusCode() === 201) { - $this->note->bluesky_url = $body['url']; - $this->note->save(); - + // We can only make the request if we have an access token + if (config('bridgy.bluesky_token') === null) { return; } - throw new \RuntimeException( - 'Bridgy publish to Bluesky failed: '.($body['error'] ?? (string) $response->getBody()) + // Make micropub request + $response = $guzzle->request( + 'POST', + 'https://brid.gy/micropub', + [ + 'headers' => [ + 'Authorization' => 'Bearer '.config('bridgy.bluesky_token'), + ], + 'json' => [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => [$this->note->getRawOriginal('note')], + ], + ], + ] ); + + // Parse for syndication URL + if ($response->getStatusCode() === 201) { + $this->note->bluesky_url = $response->getHeader('Location')[0]; + $this->note->save(); + } } } diff --git a/app/Jobs/SyndicateNoteToMastodon.php b/app/Jobs/SyndicateNoteToMastodon.php index 456680e2..07ea3b71 100644 --- a/app/Jobs/SyndicateNoteToMastodon.php +++ b/app/Jobs/SyndicateNoteToMastodon.php @@ -17,11 +17,6 @@ class SyndicateNoteToMastodon implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Only try once — retrying would send Bridgy a duplicate publish webmention. - */ - public int $tries = 1; - /** * Create a new job instance. */ @@ -36,32 +31,33 @@ class SyndicateNoteToMastodon implements ShouldQueue */ public function handle(Client $guzzle): void { - $response = $guzzle->request( - 'POST', - 'https://brid.gy/publish/webmention', - [ - 'headers' => [ - 'Accept' => 'application/json', - ], - 'form_params' => [ - 'source' => $this->note->uri, - 'target' => 'https://brid.gy/publish/mastodon', - ], - 'http_errors' => false, - ] - ); - - $body = json_decode((string) $response->getBody(), true); - - if ($response->getStatusCode() === 201) { - $this->note->mastodon_url = $body['url']; - $this->note->save(); - + // We can only make the request if we have an access token + if (config('bridgy.mastodon_token') === null) { return; } - throw new \RuntimeException( - 'Bridgy publish to Mastodon failed: '.($body['error'] ?? (string) $response->getBody()) + // Make micropub request + $response = $guzzle->request( + 'POST', + 'https://brid.gy/micropub', + [ + 'headers' => [ + 'Authorization' => 'Bearer '.config('bridgy.mastodon_token'), + ], + 'json' => [ + 'type' => ['h-entry'], + 'properties' => [ + 'content' => [$this->note->getRawOriginal('note')], + ], + ], + ] ); + + // Parse for syndication URL + if ($response->getStatusCode() === 201) { + $mastodonUrl = $response->getHeader('Location')[0]; + $this->note->mastodon_url = $mastodonUrl; + $this->note->save(); + } } } diff --git a/config/bridgy.php b/config/bridgy.php new file mode 100644 index 00000000..5314afa4 --- /dev/null +++ b/config/bridgy.php @@ -0,0 +1,31 @@ + env('BRIDGY_MASTODON_TOKEN'), + + /* + |-------------------------------------------------------------------------- + | Bluesky Token + |-------------------------------------------------------------------------- + | + | When syndicating posts to Bluesky using Brid.gy’s Micropub endpoint, we + | need to provide an access token. This token can be generated by going to + | https://brid.gy/bluesky and clicking the “Get token” button. + | + */ + + 'bluesky_token' => env('BRIDGY_BLUESKY_TOKEN'), + +]; diff --git a/resources/views/templates/note.blade.php b/resources/views/templates/note.blade.php index 8e83d5d8..b0893387 100644 --- a/resources/views/templates/note.blade.php +++ b/resources/views/templates/note.blade.php @@ -1,10 +1,4 @@
- @unless($note->mastodon_url) - - @endunless - @unless($note->bluesky_url) - - @endunless @if ($note->in_reply_to)
In reply to {{ $note->in_reply_to }} diff --git a/tests/Feature/NotesControllerTest.php b/tests/Feature/NotesControllerTest.php index c34bc1da..0c51d689 100644 --- a/tests/Feature/NotesControllerTest.php +++ b/tests/Feature/NotesControllerTest.php @@ -77,38 +77,4 @@ class NotesControllerTest extends TestCase $response = $this->get('/notes/photou-photologo'); $response->assertNotFound(); } - - /** - * Notes not yet syndicated should carry the hidden Bridgy publish - * verification links, so Bridgy can confirm intent when it re-fetches - * the page after receiving our webmention. - */ - #[Test] - public function note_page_includes_bridgy_verification_links_when_not_yet_syndicated(): void - { - $note = Note::factory()->create(); - - $response = $this->get($note->uri); - - $response->assertSee('https://brid.gy/publish/mastodon', false); - $response->assertSee('https://brid.gy/publish/bluesky', false); - } - - /** - * Once a note has been syndicated to a silo, the hidden verification - * link for that silo should no longer be rendered. - */ - #[Test] - public function note_page_omits_bridgy_verification_link_once_syndicated(): void - { - $note = Note::factory()->create(); - $note->mastodon_url = 'https://mastodon.example/@jonny/1'; - $note->bluesky_url = 'https://bsky.app/profile/jonnybarnes.uk/1'; - $note->save(); - - $response = $this->get($note->uri); - - $response->assertDontSee('https://brid.gy/publish/mastodon', false); - $response->assertDontSee('https://brid.gy/publish/bluesky', false); - } } diff --git a/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php index 23ab2dcd..b90b621b 100644 --- a/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php +++ b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php @@ -21,15 +21,11 @@ class SyndicateNoteToBlueskyJobTest extends TestCase #[Test] public function we_syndicate_notes_to_bluesky(): void { + config(['bridgy.bluesky_token' => 'test']); $faker = Factory::create(); $randomNumber = $faker->randomNumber(); - $blueskyUrl = 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber; $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ - 'url' => $blueskyUrl, - 'id' => (string) $randomNumber, - 'type' => ['h-entry'], - ])), + new Response(201, ['Location' => 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber]), ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); @@ -39,19 +35,21 @@ class SyndicateNoteToBlueskyJobTest extends TestCase $job->handle($client); $this->assertDatabaseHas('notes', [ - 'bluesky_url' => $blueskyUrl, + 'bluesky_url' => 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber, ]); } #[Test] - public function we_post_the_correct_source_and_target(): void + public function we_syndicate_the_original_markdown_to_bluesky(): void { + config(['bridgy.bluesky_token' => 'test']); + $faker = Factory::create(); + $randomNumber = $faker->randomNumber(); + $container = []; $history = Middleware::history($container); $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ - 'url' => 'https://bsky.app/profile/jonnybarnes.uk/1', - ])), + new Response(201, ['Location' => 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber]), ]); $handler = HandlerStack::create($mock); $handler->push($history); @@ -61,37 +59,12 @@ class SyndicateNoteToBlueskyJobTest extends TestCase $job = new SyndicateNoteToBluesky($note); $job->handle($client); - $request = $container[0]['request']; - $body = []; - parse_str((string) $request->getBody(), $body); - - $this->assertSame('https://brid.gy/publish/webmention', (string) $request->getUri()); - $this->assertSame($note->uri, $body['source']); - $this->assertSame('https://brid.gy/publish/bluesky', $body['target']); - } - - #[Test] - public function a_bridgy_failure_throws_and_does_not_set_bluesky_url(): void - { - $mock = new MockHandler([ - new Response(400, ['Content-Type' => 'application/json'], json_encode([ - 'error' => 'Could not find target link', - ])), + $this->assertDatabaseHas('notes', [ + 'bluesky_url' => 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber, ]); - $client = new Client(['handler' => HandlerStack::create($mock)]); - $note = Note::factory()->create(); - $job = new SyndicateNoteToBluesky($note); + $expectedRequestContent = '{"type":["h-entry"],"properties":{"content":["This is a **test**"]}}'; - $this->expectException(\RuntimeException::class); - - try { - $job->handle($client); - } finally { - $this->assertDatabaseHas('notes', [ - 'id' => $note->id, - 'bluesky_url' => null, - ]); - } + $this->assertEquals($expectedRequestContent, $container[0]['request']->getBody()->getContents()); } } diff --git a/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php b/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php index bdc780e6..64c0cc90 100644 --- a/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php +++ b/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php @@ -21,15 +21,11 @@ class SyndicateNoteToMastodonJobTest extends TestCase #[Test] public function we_syndicate_notes_to_mastodon(): void { + config(['bridgy.mastodon_token' => 'test']); $faker = Factory::create(); $randomNumber = $faker->randomNumber(); - $mastodonUrl = 'https://mastodon.example/@jonny/'.$randomNumber; $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ - 'url' => $mastodonUrl, - 'id' => (string) $randomNumber, - 'type' => ['h-entry'], - ])), + new Response(201, ['Location' => 'https://mastodon.example/@jonny/'.$randomNumber]), ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); @@ -39,19 +35,21 @@ class SyndicateNoteToMastodonJobTest extends TestCase $job->handle($client); $this->assertDatabaseHas('notes', [ - 'mastodon_url' => $mastodonUrl, + 'mastodon_url' => 'https://mastodon.example/@jonny/'.$randomNumber, ]); } #[Test] - public function we_post_the_correct_source_and_target(): void + public function we_syndicate_the_original_markdown(): void { + config(['bridgy.mastodon_token' => 'test']); + $faker = Factory::create(); + $randomNumber = $faker->randomNumber(); + $container = []; $history = Middleware::history($container); $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ - 'url' => 'https://mastodon.example/@jonny/1', - ])), + new Response(201, ['Location' => 'https://mastodon.example/@jonny/'.$randomNumber]), ]); $handler = HandlerStack::create($mock); $handler->push($history); @@ -61,37 +59,12 @@ class SyndicateNoteToMastodonJobTest extends TestCase $job = new SyndicateNoteToMastodon($note); $job->handle($client); - $request = $container[0]['request']; - $body = []; - parse_str((string) $request->getBody(), $body); - - $this->assertSame('https://brid.gy/publish/webmention', (string) $request->getUri()); - $this->assertSame($note->uri, $body['source']); - $this->assertSame('https://brid.gy/publish/mastodon', $body['target']); - } - - #[Test] - public function a_bridgy_failure_throws_and_does_not_set_mastodon_url(): void - { - $mock = new MockHandler([ - new Response(400, ['Content-Type' => 'application/json'], json_encode([ - 'error' => 'Could not find target link', - ])), + $this->assertDatabaseHas('notes', [ + 'mastodon_url' => 'https://mastodon.example/@jonny/'.$randomNumber, ]); - $client = new Client(['handler' => HandlerStack::create($mock)]); - $note = Note::factory()->create(); - $job = new SyndicateNoteToMastodon($note); + $expectedRequestContent = '{"type":["h-entry"],"properties":{"content":["This is a **test**"]}}'; - $this->expectException(\RuntimeException::class); - - try { - $job->handle($client); - } finally { - $this->assertDatabaseHas('notes', [ - 'id' => $note->id, - 'mastodon_url' => null, - ]); - } + $this->assertEquals($expectedRequestContent, $container[0]['request']->getBody()->getContents()); } }