Setup support for syndicating to Bluesky

This commit is contained in:
Jonny Barnes 2024-03-23 21:19:54 +00:00
parent 5d6d611707
commit cbbe87e23c
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8
9 changed files with 204 additions and 9 deletions

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyndicateNoteToBluesky implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
protected Note $note
) {
}
/**
* Execute the job.
*
* @throws GuzzleException
*/
public function handle(Client $guzzle): void
{
// We can only make the request if we have an access token
if (config('bridgy.bluesky_token') === null) {
return;
}
// 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();
}
}
}

View file

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

View file

@ -15,4 +15,17 @@ return [
'mastodon_token' => env('BRIDGY_MASTODON_TOKEN'),
/*
|--------------------------------------------------------------------------
| Bluesky Token
|--------------------------------------------------------------------------
|
| When syndicating posts to Bluesky using Brid.gys 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'),
];

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->string('bluesky_url')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropColumn('bluesky_url');
});
}
};

View file

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

View file

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

View file

@ -39,3 +39,8 @@
</defs>
</svg>
</a>@endif
@if($bluesky_url !== null)<a class="u-syndication" href="{{ $bluesky_url }}" title="View note on Bluesky">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 530">
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="#1185fe"/>
</svg>
</a>@endif

View file

@ -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);
}
/**

View file

@ -0,0 +1,69 @@
<?php
namespace Tests\Unit\Jobs;
use App\Jobs\SyndicateNoteToBluesky;
use App\Models\Note;
use Faker\Factory;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SyndicateNoteToBlueskyJobTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function weSyndicateNotesToBluesky(): void
{
config(['bridgy.bluesky_token' => '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());
}
}