From f9f2744fad1fdf5ab4d8f98bbbd7ac186d114642 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sun, 2 Aug 2026 18:07:39 +0100 Subject: [PATCH] Send Brrr push notifications for new webmentions Dispatches a queued job to POST to a configured Brrr webhook whenever a brand-new webmention is saved, so replies/likes/reposts show up as push notifications instead of requiring a manual check of the site. --- .env.example | 2 + app/Jobs/NotifyBrrrOfWebMention.php | 56 +++++++++++++++ app/Jobs/ProcessWebMention.php | 1 + config/services.php | 4 ++ .../Jobs/NotifyBrrrOfWebMentionJobTest.php | 71 +++++++++++++++++++ tests/Unit/Jobs/ProcessWebMentionJobTest.php | 4 ++ 6 files changed, 138 insertions(+) create mode 100644 app/Jobs/NotifyBrrrOfWebMention.php create mode 100644 tests/Unit/Jobs/NotifyBrrrOfWebMentionJobTest.php diff --git a/.env.example b/.env.example index eb423aaa..a1c38110 100644 --- a/.env.example +++ b/.env.example @@ -78,6 +78,8 @@ SESSION_SAME_SITE=strict LOG_SLACK_WEBHOOK_URL= +BRRR_WEBHOOK_URL= + FLARE_KEY= IGNITION_OPEN_AI_KEY= diff --git a/app/Jobs/NotifyBrrrOfWebMention.php b/app/Jobs/NotifyBrrrOfWebMention.php new file mode 100644 index 00000000..3273b7d6 --- /dev/null +++ b/app/Jobs/NotifyBrrrOfWebMention.php @@ -0,0 +1,56 @@ + $this->title(), + 'message' => "From {$this->webMention->source}", + 'open_url' => $this->webMention->target, + ]); + } + + /** + * Build a notification title based on the webmention type. + */ + private function title(): string + { + return match ($this->webMention->type) { + 'in-reply-to' => 'New reply', + 'like-of' => 'New like', + 'repost-of' => 'New repost', + default => 'New webmention', + }; + } +} diff --git a/app/Jobs/ProcessWebMention.php b/app/Jobs/ProcessWebMention.php index 6cb276f8..4ac6f5fd 100644 --- a/app/Jobs/ProcessWebMention.php +++ b/app/Jobs/ProcessWebMention.php @@ -96,6 +96,7 @@ class ProcessWebMention implements ShouldQueue $webmention->type = $type; $webmention->mf2 = json_encode($microformats); $webmention->save(); + dispatch(new NotifyBrrrOfWebMention($webmention)); } /** diff --git a/config/services.php b/config/services.php index b784e544..7a5cdfa5 100644 --- a/config/services.php +++ b/config/services.php @@ -39,4 +39,8 @@ return [ 'token' => env('CLOUDCONVERT_API_TOKEN'), ], + 'brrr' => [ + 'webhook_url' => env('BRRR_WEBHOOK_URL'), + ], + ]; diff --git a/tests/Unit/Jobs/NotifyBrrrOfWebMentionJobTest.php b/tests/Unit/Jobs/NotifyBrrrOfWebMentionJobTest.php new file mode 100644 index 00000000..f28bc4eb --- /dev/null +++ b/tests/Unit/Jobs/NotifyBrrrOfWebMentionJobTest.php @@ -0,0 +1,71 @@ + 'https://api.brrr.now/v1/br_usr_test']); + Http::fake(); + + $webMention = WebMention::factory()->make([ + 'source' => 'https://example.org/reply/1', + 'target' => 'https://jonnybarnes.uk/notes/1', + 'type' => 'in-reply-to', + ]); + + $job = new NotifyBrrrOfWebMention($webMention); + $job->handle(); + + Http::assertSent(function (Request $request) { + return $request->url() === 'https://api.brrr.now/v1/br_usr_test' + && $request['title'] === 'New reply' + && $request['message'] === 'From https://example.org/reply/1' + && $request['open_url'] === 'https://jonnybarnes.uk/notes/1'; + }); + } + + #[Test] + public function it_titles_notifications_by_webmention_type(): void + { + config(['services.brrr.webhook_url' => 'https://api.brrr.now/v1/br_usr_test']); + Http::fake(); + + foreach ([ + 'in-reply-to' => 'New reply', + 'like-of' => 'New like', + 'repost-of' => 'New repost', + 'something-else' => 'New webmention', + ] as $type => $expectedTitle) { + $webMention = WebMention::factory()->make(['type' => $type]); + + (new NotifyBrrrOfWebMention($webMention))->handle(); + + Http::assertSent(fn (Request $request) => $request['title'] === $expectedTitle); + } + } + + #[Test] + public function it_does_nothing_when_no_webhook_url_is_configured(): void + { + config(['services.brrr.webhook_url' => null]); + Http::fake(); + + $webMention = WebMention::factory()->make(); + + (new NotifyBrrrOfWebMention($webMention))->handle(); + + Http::assertNothingSent(); + } +} diff --git a/tests/Unit/Jobs/ProcessWebMentionJobTest.php b/tests/Unit/Jobs/ProcessWebMentionJobTest.php index 001d8f3d..f7c9329e 100644 --- a/tests/Unit/Jobs/ProcessWebMentionJobTest.php +++ b/tests/Unit/Jobs/ProcessWebMentionJobTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Jobs; use App\Exceptions\RemoteContentNotFoundException; +use App\Jobs\NotifyBrrrOfWebMention; use App\Jobs\ProcessWebMention; use App\Jobs\SaveProfileImage; use App\Models\Note; @@ -71,6 +72,7 @@ class ProcessWebMentionJobTest extends TestCase $job->handle($parser); Queue::assertPushed(SaveProfileImage::class); + Queue::assertPushed(NotifyBrrrOfWebMention::class); $this->assertDatabaseHas('webmentions', [ 'source' => $source, 'type' => 'like-of', @@ -104,6 +106,7 @@ class ProcessWebMentionJobTest extends TestCase $job->handle($parser); Queue::assertPushed(SaveProfileImage::class); + Queue::assertNotPushed(NotifyBrrrOfWebMention::class); $this->assertDatabaseHas('webmentions', [ 'source' => $source, 'type' => 'in-reply-to', @@ -206,6 +209,7 @@ class ProcessWebMentionJobTest extends TestCase $job = new ProcessWebMention($note, $source); $job->handle($parser); + Queue::assertPushed(NotifyBrrrOfWebMention::class); $this->assertGreaterThan(255, strlen($source)); $this->assertDatabaseHas('webmentions', [ 'source' => $source, -- 2.55.0