diff --git a/src/FeedGenerator/FeedGenerator.php b/src/FeedGenerator/FeedGenerator.php new file mode 100644 index 00000000..67a4c0cd --- /dev/null +++ b/src/FeedGenerator/FeedGenerator.php @@ -0,0 +1,53 @@ +json('cursor'); + * $feed = $response->collect('posts')->map(function(array $post) { + * return ['post' => data_get($post, 'uri')]; + * })->toArray(); + * + * return compact('cursor', 'feed'); + * }); + * ``` + * + * @param string $name short name. Used in generator url. `at://did:.../app.bsky.feed.generator/{name}` + * @param callable(int $limit, string $cursor, string $feed): array $algo + */ + public static function register(string $name, callable $algo): void + { + self::$algos[$name] = $algo; + } + + public static function getFeedSkeleton(string $name, int $limit, string $cursor, Request $request): mixed + { + return call_user_func(self::$algos[$name], $limit, $cursor, $request); + } + + public static function has(string $name): bool + { + return isset(self::$algos[$name]); + } + + public static function missing(string $name): bool + { + return ! self::has($name); + } +} diff --git a/src/FeedGenerator/Http/FeedSkeletonController.php b/src/FeedGenerator/Http/FeedSkeletonController.php new file mode 100644 index 00000000..b055ed00 --- /dev/null +++ b/src/FeedGenerator/Http/FeedSkeletonController.php @@ -0,0 +1,27 @@ +input('feed')); + + if ($at->collection() !== Feed::Generator->value || FeedGenerator::missing($at->rkey())) { + abort(404); + } + + return FeedGenerator::getFeedSkeleton( + name: $at->rkey(), + limit: $request->input('limit', 50), + cursor: $request->input('cursor', ''), + request: $request, + ); + } +} diff --git a/src/HasShortHand.php b/src/HasShortHand.php index 0566c49d..23d3ade7 100644 --- a/src/HasShortHand.php +++ b/src/HasShortHand.php @@ -17,6 +17,7 @@ use Revolution\Bluesky\Client\SubClient\VideoClient; use Revolution\Bluesky\Record\Block; use Revolution\Bluesky\Record\Follow; +use Revolution\Bluesky\Record\Generator; use Revolution\Bluesky\Record\Like; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\Record\Profile; @@ -492,6 +493,30 @@ public function getServiceAuth(#[Format('did')] string $aud, ?int $exp = null, # ->getServiceAuth(aud: $aud, exp: $exp, lxm: $lxm); } + /** + * @param Generator $generator + * @param string $name Generator short name + * @throws AuthenticationException + */ + public function publishFeedGenerator(string $name, Generator $generator): Response + { + return $this->putRecord( + repo: $this->assertDid(), + collection: Feed::Generator->value, + rkey: $name, + record: $generator, + ); + } + + public function unpublishFeedGenerator(string $name): Response + { + return $this->deleteRecord( + repo: $this->assertDid(), + collection: Feed::Generator->value, + rkey: $name, + ); + } + public function getSuggestions(?int $limit = 50, ?string $cursor = null): Response { return $this->client(auth: true) diff --git a/src/Providers/BlueskyServiceProvider.php b/src/Providers/BlueskyServiceProvider.php index 4c6c1911..ae824394 100644 --- a/src/Providers/BlueskyServiceProvider.php +++ b/src/Providers/BlueskyServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Laravel\Socialite\Facades\Socialite; +use Revolution\AtProto\Lexicon\Contracts\App\Bsky\Feed; use Revolution\Bluesky\BlueskyManager; use Revolution\Bluesky\Client\AtpClient; use Revolution\Bluesky\Console\DownloadRecordCommand; @@ -17,6 +18,7 @@ use Revolution\Bluesky\Console\NewPrivateKeyCommand; use Revolution\Bluesky\Contracts\Factory; use Revolution\Bluesky\Contracts\XrpcClient; +use Revolution\Bluesky\FeedGenerator\Http\FeedSkeletonController; use Revolution\Bluesky\Socalite\BlueskyProvider; use Revolution\Bluesky\Socalite\Http\OAuthMetaController; @@ -53,6 +55,7 @@ public function boot(): void } $this->socialite(); + $this->generator(); } protected function socialite(): void @@ -89,4 +92,10 @@ protected function socialite(): void ->name('bluesky.oauth.jwks'); }); } + + protected function generator(): void + { + Route::prefix('/xrpc/') + ->get(Feed::getFeedSkeleton, FeedSkeletonController::class); + } } diff --git a/src/Record/Generator.php b/src/Record/Generator.php new file mode 100644 index 00000000..8c03bd54 --- /dev/null +++ b/src/Record/Generator.php @@ -0,0 +1,82 @@ +did = $did; + $this->displayName = $displayName; + } + + public static function create(string $did, string $displayName): self + { + return new self($did, $displayName); + } + + public function did(string $did): self + { + $this->did = $did; + + return $this; + } + + public function displayName(string $displayName): self + { + $this->displayName = $displayName; + + return $this; + } + + public function description(?string $description = null): self + { + $this->description = $description; + + return $this; + } + + public function avatar(null|BlobRef|array|callable $avatar = null): self + { + if (is_null($avatar)) { + $this->avatar = $avatar; + + return $this; + } + + if (is_callable($avatar)) { + $avatar = call_user_func($avatar); + } + + if ($avatar instanceof BlobRef) { + $avatar = $avatar->toArray(); + } + + $this->avatar = $avatar; + + return $this; + } + + public function labels(?SelfLabels $labels = null): self + { + $this->labels = $labels?->toArray(); + + return $this; + } +}