diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 47abe68c..3ebccbd3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,8 @@ updates: directory: "/" schedule: interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index e1765ef3..29afebb9 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -42,7 +42,7 @@ jobs: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-php-8.3-composer-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml index e6340c67..9b0956ad 100644 --- a/.github/workflows/pint.yml +++ b/.github/workflows/pint.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP with pecl extensions uses: shivammathur/setup-php@v2 @@ -24,7 +24,7 @@ jobs: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/app/Jobs/SyndicateNoteToBluesky.php b/app/Jobs/SyndicateNoteToBluesky.php new file mode 100644 index 00000000..9e89f639 --- /dev/null +++ b/app/Jobs/SyndicateNoteToBluesky.php @@ -0,0 +1,63 @@ +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/Services/NoteService.php b/app/Services/NoteService.php index 1238b804..b101498c 100644 --- a/app/Services/NoteService.php +++ b/app/Services/NoteService.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Services; use App\Jobs\SendWebMentions; +use App\Jobs\SyndicateNoteToBluesky; use App\Jobs\SyndicateNoteToMastodon; use App\Models\Media; use App\Models\Note; @@ -53,6 +54,10 @@ class NoteService extends Service dispatch(new SyndicateNoteToMastodon($note)); } + if (in_array('bluesky', $this->getSyndicationTargets($request), true)) { + dispatch(new SyndicateNoteToBluesky($note)); + } + return $note; } @@ -156,12 +161,12 @@ class NoteService extends Service $mpSyndicateTo = Arr::wrap($mpSyndicateTo); foreach ($mpSyndicateTo as $uid) { $target = SyndicationTarget::where('uid', $uid)->first(); - if ($target && $target->service_name === 'Twitter') { - $syndication[] = 'twitter'; - } if ($target && $target->service_name === 'Mastodon') { $syndication[] = 'mastodon'; } + if ($target && $target->service_name === 'Bluesky') { + $syndication[] = 'bluesky'; + } } return $syndication; diff --git a/composer.lock b/composer.lock index d0d55e7d..afd19400 100644 --- a/composer.lock +++ b/composer.lock @@ -1710,16 +1710,16 @@ }, { "name": "intervention/image", - "version": "3.5.0", + "version": "3.5.1", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "408d3655c7705339e8c79731ea7efb51546cfa10" + "reference": "67be90e5700370c88833190d4edc07e4bb7d157b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/408d3655c7705339e8c79731ea7efb51546cfa10", - "reference": "408d3655c7705339e8c79731ea7efb51546cfa10", + "url": "https://api.github.com/repos/Intervention/image/zipball/67be90e5700370c88833190d4edc07e4bb7d157b", + "reference": "67be90e5700370c88833190d4edc07e4bb7d157b", "shasum": "" }, "require": { @@ -1766,7 +1766,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.5.0" + "source": "https://github.com/Intervention/image/tree/3.5.1" }, "funding": [ { @@ -1778,7 +1778,7 @@ "type": "github" } ], - "time": "2024-03-13T16:26:15+00:00" + "time": "2024-03-22T07:12:19+00:00" }, { "name": "jonnybarnes/indieweb", @@ -8207,16 +8207,16 @@ }, { "name": "web-auth/metadata-service", - "version": "4.8.2", + "version": "4.8.3", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-metadata-service.git", - "reference": "024df5fb26166adf388dea697d2826ae5a6001cf" + "reference": "fb7c1f107639285fab90f870aab38360252c82f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/024df5fb26166adf388dea697d2826ae5a6001cf", - "reference": "024df5fb26166adf388dea697d2826ae5a6001cf", + "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/fb7c1f107639285fab90f870aab38360252c82f5", + "reference": "fb7c1f107639285fab90f870aab38360252c82f5", "shasum": "" }, "require": { @@ -8275,7 +8275,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-metadata-service/tree/4.8.2" + "source": "https://github.com/web-auth/webauthn-metadata-service/tree/4.8.3" }, "funding": [ { @@ -8287,20 +8287,20 @@ "type": "patreon" } ], - "time": "2024-02-26T07:58:15+00:00" + "time": "2024-03-13T07:16:02+00:00" }, { "name": "web-auth/webauthn-lib", - "version": "4.8.2", + "version": "4.8.3", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "abac08104bbbbdef01ace704c90ff8290696e47f" + "reference": "d296fde8450ce6972c54315a70e32159cad7e35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/abac08104bbbbdef01ace704c90ff8290696e47f", - "reference": "abac08104bbbbdef01ace704c90ff8290696e47f", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/d296fde8450ce6972c54315a70e32159cad7e35b", + "reference": "d296fde8450ce6972c54315a70e32159cad7e35b", "shasum": "" }, "require": { @@ -8361,7 +8361,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/4.8.2" + "source": "https://github.com/web-auth/webauthn-lib/tree/4.8.3" }, "funding": [ { @@ -8373,7 +8373,7 @@ "type": "patreon" } ], - "time": "2024-02-26T19:17:26+00:00" + "time": "2024-03-22T20:51:36+00:00" }, { "name": "webmozart/assert", diff --git a/config/bridgy.php b/config/bridgy.php index 9717625f..5314afa4 100644 --- a/config/bridgy.php +++ b/config/bridgy.php @@ -15,4 +15,17 @@ return [ 'mastodon_token' => 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/database/migrations/2024_03_23_204940_add_bluesky_syndication_column.php b/database/migrations/2024_03_23_204940_add_bluesky_syndication_column.php new file mode 100644 index 00000000..864d949c --- /dev/null +++ b/database/migrations/2024_03_23_204940_add_bluesky_syndication_column.php @@ -0,0 +1,28 @@ +string('bluesky_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('notes', function (Blueprint $table) { + $table->dropColumn('bluesky_url'); + }); + } +}; diff --git a/database/seeders/NotesTableSeeder.php b/database/seeders/NotesTableSeeder.php index 4a42f379..298205e3 100644 --- a/database/seeders/NotesTableSeeder.php +++ b/database/seeders/NotesTableSeeder.php @@ -138,6 +138,7 @@ class NotesTableSeeder extends Seeder $noteSyndicated->swarm_url = 'https://www.swarmapp.com/checking/123456789'; $noteSyndicated->instagram_url = 'https://www.instagram.com/p/aWsEd123Jh'; $noteSyndicated->mastodon_url = 'https://mastodon.social/@jonnybarnes/123456789'; + $noteSyndicated->bluesky_url = 'https://bsky.app/profile/jonnybarnes.uk/post/123456789'; $noteSyndicated->save(); DB::table('notes') ->where('id', $noteSyndicated->id) diff --git a/resources/views/templates/note.blade.php b/resources/views/templates/note.blade.php index 83b799d4..fb4bb58f 100644 --- a/resources/views/templates/note.blade.php +++ b/resources/views/templates/note.blade.php @@ -71,7 +71,8 @@ $note->facebook_url || $note->swarm_url || $note->instagram_url || - $note->mastodon_url + $note->mastodon_url || + $note->bluesky_url ) @include('templates.social-links', [ 'id' => $note->id, @@ -80,6 +81,7 @@ 'swarm_url' => $note->swarm_url, 'instagram_url' => $note->instagram_url, 'mastodon_url' => $note->mastodon_url, + 'bluesky_url' => $note->bluesky_url ]) @endif diff --git a/resources/views/templates/social-links.blade.php b/resources/views/templates/social-links.blade.php index 95aa1092..c884ac6b 100644 --- a/resources/views/templates/social-links.blade.php +++ b/resources/views/templates/social-links.blade.php @@ -39,3 +39,8 @@ @endif +@if($bluesky_url !== null) + + + +@endif diff --git a/tests/Feature/MicropubControllerTest.php b/tests/Feature/MicropubControllerTest.php index e3054a20..412401d1 100644 --- a/tests/Feature/MicropubControllerTest.php +++ b/tests/Feature/MicropubControllerTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Feature; use App\Jobs\SendWebMentions; +use App\Jobs\SyndicateNoteToBluesky; use App\Jobs\SyndicateNoteToMastodon; use App\Models\Media; use App\Models\Note; @@ -123,18 +124,18 @@ class MicropubControllerTest extends TestCase } /** @test */ - public function micropubClientCanRequestTheNewNoteIsSyndicatedToTwitterAndMastodon(): void + public function micropubClientCanRequestTheNewNoteIsSyndicatedToMastodonAndBluesky(): void { Queue::fake(); - SyndicationTarget::factory()->create([ - 'uid' => 'https://twitter.com/jonnybarnes', - 'service_name' => 'Twitter', - ]); SyndicationTarget::factory()->create([ 'uid' => 'https://mastodon.social/@jonnybarnes', 'service_name' => 'Mastodon', ]); + SyndicationTarget::factory()->create([ + 'uid' => 'https://bsky.app/profile/jonnybarnes.uk', + 'service_name' => 'Bluesky', + ]); $faker = Factory::create(); $note = $faker->text; @@ -145,6 +146,7 @@ class MicropubControllerTest extends TestCase 'content' => $note, 'mp-syndicate-to' => [ 'https://mastodon.social/@jonnybarnes', + 'https://bsky.app/profile/jonnybarnes.uk', ], ], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] @@ -152,6 +154,7 @@ class MicropubControllerTest extends TestCase $response->assertJson(['response' => 'created']); $this->assertDatabaseHas('notes', ['note' => $note]); Queue::assertPushed(SyndicateNoteToMastodon::class); + Queue::assertPushed(SyndicateNoteToBluesky::class); } /** @test */ @@ -249,6 +252,10 @@ class MicropubControllerTest extends TestCase 'uid' => 'https://mastodon.social/@jonnybarnes', 'service_name' => 'Mastodon', ]); + SyndicationTarget::factory()->create([ + 'uid' => 'https://bsky.app/profile/jonnybarnes.uk', + 'service_name' => 'Bluesky', + ]); $faker = Factory::create(); $note = $faker->text; @@ -261,6 +268,7 @@ class MicropubControllerTest extends TestCase 'in-reply-to' => ['https://aaronpk.localhost'], 'mp-syndicate-to' => [ 'https://mastodon.social/@jonnybarnes', + 'https://bsky.app/profile/jonnybarnes.uk', ], 'photo' => [config('filesystems.disks.s3.url') . '/test-photo.jpg'], ], @@ -272,6 +280,7 @@ class MicropubControllerTest extends TestCase ->assertJson(['response' => 'created']); Queue::assertPushed(SendWebMentions::class); Queue::assertPushed(SyndicateNoteToMastodon::class); + Queue::assertPushed(SyndicateNoteToBluesky::class); } /** diff --git a/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php new file mode 100644 index 00000000..e1e3fb8c --- /dev/null +++ b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php @@ -0,0 +1,69 @@ + 'test']); + $faker = Factory::create(); + $randomNumber = $faker->randomNumber(); + $mock = new MockHandler([ + new Response(201, ['Location' => 'https://bsky.app/profile/jonnybarnes.uk/' . $randomNumber]), + ]); + $handler = HandlerStack::create($mock); + $client = new Client(['handler' => $handler]); + + $note = Note::factory()->create(); + $job = new SyndicateNoteToBluesky($note); + $job->handle($client); + + $this->assertDatabaseHas('notes', [ + 'bluesky_url' => 'https://bsky.app/profile/jonnybarnes.uk/' . $randomNumber, + ]); + } + + /** @test */ + public function weSyndicateTheOriginalMarkdownToBluesky(): void + { + config(['bridgy.bluesky_token' => 'test']); + $faker = Factory::create(); + $randomNumber = $faker->randomNumber(); + + $container = []; + $history = Middleware::history($container); + $mock = new MockHandler([ + new Response(201, ['Location' => 'https://bsky.app/profile/jonnybarnes.uk/' . $randomNumber]), + ]); + $handler = HandlerStack::create($mock); + $handler->push($history); + $client = new Client(['handler' => $handler]); + + $note = Note::factory()->create(['note' => 'This is a **test**']); + $job = new SyndicateNoteToBluesky($note); + $job->handle($client); + + $this->assertDatabaseHas('notes', [ + 'bluesky_url' => 'https://bsky.app/profile/jonnybarnes.uk/' . $randomNumber, + ]); + + $expectedRequestContent = '{"type":["h-entry"],"properties":{"content":["This is a **test**"]}}'; + + $this->assertEquals($expectedRequestContent, $container[0]['request']->getBody()->getContents()); + } +}