diff --git a/.env.example b/.env.example
index b9bedb501..2d2180caa 100644
--- a/.env.example
+++ b/.env.example
@@ -44,6 +44,9 @@ TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=
+BLUESKY_USERNAME=
+BLUESKY_PASSWORD=
+
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHANNEL=
diff --git a/README.md b/README.md
index 7fb644cb0..5d419200b 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,7 @@ New threads will be automatically added to the index and threads which get updat
php artisan scout:flush App\\Models\\Thread
```
-### X (Twitter) Sharing (optional)
+### Social Media Sharing (optional)
To enable published articles to be automatically shared on X, you'll need to [create an app](https://developer.x.com/apps/). Once the app has been created, update the below variables in your `.env` file. The consumer key and secret and access token and secret can be found in the `Keys and tokens` section of the X developers UI.
@@ -103,6 +103,13 @@ TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=
```
+To do the same for Bluesky you simply need to set up the app keys with your login and password:
+
+```
+BLUESKY_USERNAME=
+BLUESKY_PASSWORD=
+```
+
Approved articles are shared in the order they were submitted for approval. Articles are shared twice per day at 14:00 and 18:00 UTC. Once an article has been shared, it will not be shared again.
### Telegram Notifications (optional)
diff --git a/app/Console/Commands/PostArticleToTwitter.php b/app/Console/Commands/PostArticleToSocialMedia.php
similarity index 52%
rename from app/Console/Commands/PostArticleToTwitter.php
rename to app/Console/Commands/PostArticleToSocialMedia.php
index fcb988346..7a198ed60 100644
--- a/app/Console/Commands/PostArticleToTwitter.php
+++ b/app/Console/Commands/PostArticleToSocialMedia.php
@@ -3,20 +3,22 @@
namespace App\Console\Commands;
use App\Models\Article;
-use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
+use App\Notifications\PostArticleToBluesky;
+use App\Notifications\PostArticleToTwitter;
use Illuminate\Console\Command;
use Illuminate\Notifications\AnonymousNotifiable;
-final class PostArticleToTwitter extends Command
+final class PostArticleToSocialMedia extends Command
{
- protected $signature = 'lio:post-article-to-twitter';
+ protected $signature = 'lio:post-article-to-social-media';
- protected $description = 'Posts the latest unshared article to X';
+ protected $description = 'Posts the latest unshared article to social media';
public function handle(AnonymousNotifiable $notifiable): void
{
if ($article = Article::nextForSharing()) {
- $notifiable->notify(new PostArticleToTwitterNotification($article));
+ $notifiable->notify(new PostArticleToBluesky($article));
+ $notifiable->notify(new PostArticleToTwitter($article));
$article->markAsShared();
}
diff --git a/app/Http/Requests/UpdateProfileRequest.php b/app/Http/Requests/UpdateProfileRequest.php
index 031ac123d..160e95df0 100644
--- a/app/Http/Requests/UpdateProfileRequest.php
+++ b/app/Http/Requests/UpdateProfileRequest.php
@@ -13,6 +13,7 @@ public function rules(): array
'email' => 'required|email|max:255|unique:users,email,'.Auth::id(),
'username' => 'required|alpha_dash|max:255|unique:users,username,'.Auth::id(),
'twitter' => 'max:255|nullable|unique:users,twitter,'.Auth::id(),
+ 'bluesky' => 'max:255|nullable|unique:users,bluesky,'.Auth::id(),
'website' => 'max:255|nullable|url',
'bio' => 'max:160',
];
@@ -43,6 +44,11 @@ public function twitter(): ?string
return $this->get('twitter');
}
+ public function bluesky(): ?string
+ {
+ return $this->get('bluesky');
+ }
+
public function website(): ?string
{
return $this->get('website');
diff --git a/app/Http/Resources/AuthorResource.php b/app/Http/Resources/AuthorResource.php
index 23a1e27e2..86bbe5b6c 100644
--- a/app/Http/Resources/AuthorResource.php
+++ b/app/Http/Resources/AuthorResource.php
@@ -18,6 +18,7 @@ public function toArray($request): array
'name' => $this->name(),
'bio' => $this->bio(),
'twitter_handle' => $this->twitter(),
+ 'bluesky_handle' => $this->bluesky(),
'github_username' => $this->githubUsername(),
];
}
diff --git a/app/Jobs/UpdateProfile.php b/app/Jobs/UpdateProfile.php
index b620a61c6..45182bf12 100644
--- a/app/Jobs/UpdateProfile.php
+++ b/app/Jobs/UpdateProfile.php
@@ -16,7 +16,7 @@ public function __construct(
array $attributes = []
) {
$this->attributes = Arr::only($attributes, [
- 'name', 'email', 'username', 'github_username', 'bio', 'twitter', 'website',
+ 'name', 'email', 'username', 'github_username', 'bio', 'twitter', 'bluesky', 'website',
]);
}
@@ -28,6 +28,7 @@ public static function fromRequest(User $user, UpdateProfileRequest $request): s
'username' => strtolower($request->username()),
'bio' => trim(strip_tags($request->bio())),
'twitter' => $request->twitter(),
+ 'bluesky' => $request->bluesky(),
'website' => $request->website(),
]);
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 3515d8420..10d41fb9a 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -46,6 +46,7 @@ final class User extends Authenticatable implements MustVerifyEmail
'name',
'email',
'twitter',
+ 'bluesky',
'website',
'username',
'password',
@@ -110,6 +111,11 @@ public function twitter(): ?string
return $this->twitter;
}
+ public function bluesky(): ?string
+ {
+ return $this->bluesky;
+ }
+
public function website(): ?string
{
return $this->website;
@@ -120,6 +126,11 @@ public function hasTwitterAccount(): bool
return ! empty($this->twitter());
}
+ public function hasBlueskyAccount(): bool
+ {
+ return ! empty($this->bluesky());
+ }
+
public function hasWebsite(): bool
{
return ! empty($this->website());
diff --git a/app/Notifications/PostArticleToBluesky.php b/app/Notifications/PostArticleToBluesky.php
new file mode 100644
index 000000000..fd0162570
--- /dev/null
+++ b/app/Notifications/PostArticleToBluesky.php
@@ -0,0 +1,42 @@
+text($this->generatePost());
+ }
+
+ public function generatePost(): string
+ {
+ $title = $this->article->title();
+ $url = route('articles.show', $this->article->slug());
+ $author = $this->article->author();
+ $author = $author->bluesky() ? "@{$author->bluesky()}" : $author->name();
+
+ return "{$title} by {$author}\n\n{$url}";
+ }
+
+ public function article()
+ {
+ return $this->article;
+ }
+}
diff --git a/app/Notifications/PostArticleToTwitter.php b/app/Notifications/PostArticleToTwitter.php
index 4cd1b96be..d2785fc5b 100644
--- a/app/Notifications/PostArticleToTwitter.php
+++ b/app/Notifications/PostArticleToTwitter.php
@@ -24,7 +24,7 @@ public function toTwitter($notifiable)
return new TwitterStatusUpdate($this->generateTweet());
}
- public function generateTweet()
+ public function generateTweet(): string
{
$title = $this->article->title();
$url = route('articles.show', $this->article->slug());
diff --git a/composer.json b/composer.json
index ed0e9211e..2151c6622 100644
--- a/composer.json
+++ b/composer.json
@@ -12,6 +12,7 @@
"blade-ui-kit/blade-zondicons": "^1.5",
"codeat3/blade-simple-icons": "^5.0",
"guzzlehttp/guzzle": "^7.2",
+ "innocenzi/bluesky-notification-channel": "^0.2.0",
"intervention/image": "^2.7",
"laravel-notification-channels/telegram": "^5.0",
"laravel-notification-channels/twitter": "^8.1.1",
diff --git a/composer.lock b/composer.lock
index 18a30964b..65b6d0f9c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b73667ac3de654bc08e32d2b17c974b7",
+ "content-hash": "78099b0bf3f4baa0b5748d997e7add1c",
"packages": [
{
"name": "abraham/twitteroauth",
@@ -1926,6 +1926,79 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
+ {
+ "name": "innocenzi/bluesky-notification-channel",
+ "version": "v0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/innocenzi/bluesky-notification-channel.git",
+ "reference": "3f9076affc639d8e1f9bdfcaaf996c87626532ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/innocenzi/bluesky-notification-channel/zipball/3f9076affc639d8e1f9bdfcaaf996c87626532ef",
+ "reference": "3f9076affc639d8e1f9bdfcaaf996c87626532ef",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^11.0",
+ "php": "^8.1",
+ "spatie/laravel-package-tools": "^1.16.5"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "guzzlehttp/guzzle": "^7.9.2",
+ "nunomaduro/collision": "^8.5",
+ "orchestra/testbench": "^9.5.2",
+ "pestphp/pest": "^3.5",
+ "pestphp/pest-plugin-arch": "^3.0",
+ "pestphp/pest-plugin-laravel": "^3.0",
+ "spatie/laravel-ray": "^1.37.1"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "NotificationChannels\\Bluesky\\BlueskyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "NotificationChannels\\Bluesky\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Enzo Innocenzi",
+ "email": "enzo@innocenzi.dev",
+ "role": "Developer"
+ }
+ ],
+ "description": "Bluesky notification channel for the Laravel framework",
+ "homepage": "https://github.com/innocenzi/bluesky-notification-channel",
+ "keywords": [
+ "Enzo Innocenzi",
+ "bluesky",
+ "laravel",
+ "notification-channel"
+ ],
+ "support": {
+ "issues": "https://github.com/innocenzi/bluesky-notification-channel/issues",
+ "source": "https://github.com/innocenzi/bluesky-notification-channel/tree/v0.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Enzo Innocenzi",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-29T15:53:16+00:00"
+ },
{
"name": "intervention/image",
"version": "2.7.2",
diff --git a/config/services.php b/config/services.php
index 2a575db66..455d70c87 100644
--- a/config/services.php
+++ b/config/services.php
@@ -30,6 +30,11 @@
'access_secret' => env('TWITTER_ACCESS_SECRET'),
],
+ 'bluesky' => [
+ 'username' => env('BLUESKY_USERNAME'),
+ 'password' => env('BLUESKY_PASSWORD'),
+ ],
+
'telegram-bot-api' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
'channel' => env('TELEGRAM_CHANNEL'),
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index f771f2cb9..f434385cc 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -24,6 +24,7 @@ public function definition(): array
'github_id' => $this->faker->unique()->numberBetween(10000, 99999),
'github_username' => $this->faker->unique()->userName(),
'twitter' => $this->faker->unique()->userName(),
+ 'bluesky' => $this->faker->unique()->userName(),
'website' => 'https://laravel.io',
'banned_at' => null,
'banned_reason' => null,
diff --git a/database/migrations/2024_11_28_202608_add_bluesky_column_to_users.php b/database/migrations/2024_11_28_202608_add_bluesky_column_to_users.php
new file mode 100644
index 000000000..36c32a503
--- /dev/null
+++ b/database/migrations/2024_11_28_202608_add_bluesky_column_to_users.php
@@ -0,0 +1,15 @@
+string('bluesky')->nullable()->after('twitter');
+ });
+ }
+};
diff --git a/resources/svg/bluesky.svg b/resources/svg/bluesky.svg
new file mode 100644
index 000000000..dfcf3b307
--- /dev/null
+++ b/resources/svg/bluesky.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/views/articles/show.blade.php b/resources/views/articles/show.blade.php
index ece6e8852..34d980704 100644
--- a/resources/views/articles/show.blade.php
+++ b/resources/views/articles/show.blade.php
@@ -172,6 +172,12 @@ class="prose prose-lg text-gray-800 prose-lio"
@endif
+ @if ($article->author()->hasBlueskyAccount())
+
+