Merge pull request #1371 from jonnybarnes/develop
MTM Bluesky Syndication
This commit is contained in:
commit
fabe9d662b
13 changed files with 230 additions and 30 deletions
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
|
@ -10,3 +10,8 @@ updates:
|
|||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
|
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
|
@ -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') }}
|
||||
|
|
4
.github/workflows/pint.yml
vendored
4
.github/workflows/pint.yml
vendored
|
@ -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') }}
|
||||
|
|
63
app/Jobs/SyndicateNoteToBluesky.php
Normal file
63
app/Jobs/SyndicateNoteToBluesky.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
36
composer.lock
generated
36
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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'),
|
||||
|
||||
];
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
69
tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php
Normal file
69
tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue