> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Feed Generator

> 使用 Laravel Bluesky 建立並公開自訂演算法 feed 的方法。說明註冊、指令建立、多個 feed、類別分離、認證與進階用法。

## 概觀

Feed Generator 是在 Bluesky 上運作的「演算法 feed」機制。可依特定關鍵字或使用者條件公開自訂 feed。使用 `laravel-bluesky`，可在 Laravel 應用程式上輕鬆實作 Feed Generator。

<Info>
  官方教學：[建立自訂 Feed](https://atproto.com/ja/guides/custom-feed-tutorial)
</Info>

<Info>
  官方 starter kit：[bluesky-social/feed-generator](https://github.com/bluesky-social/feed-generator)
</Info>

```mermaid theme={null}
sequenceDiagram
    participant Bluesky as Bluesky<br>伺服器
    participant App as Laravel<br>應用程式
    participant DB as 資料庫

    Bluesky->>App: GET /xrpc/app.bsky.feed.getFeedSkeleton
    App->>DB: 取得 feed 資料
    DB-->>App: posts
    App-->>Bluesky: { cursor, feed }
```

## 註冊 FeedGenerator 演算法

最簡單的用法是在 `AppServiceProvider::boot()` 中以閉包註冊演算法。

```php theme={null}
// 於 AppServiceProvider::boot() 註冊

use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::register(name: 'artisan', algo: function(int $limit, ?string $cursor, ?string $user, Request $request): array {
    // 實作內容可自由決定。

    // 因 API 的暫時性限制而需認證
    $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
                       ->searchPosts(q: '#laravel', until: $cursor, limit: $limit);

    $cursor = data_get($response->collect('posts')->last(), 'indexedAt');

    $feed = $response->collect('posts')->map(function(array $post) {
        return ['post' => data_get($post, 'uri')];
    })->toArray();

    // 也可透過 Request 物件依使用者狀態回傳不同結果。
    info('user: '.$user); // 請求端使用者的 DID，'did:plc:***'
    info('header', $request->header());

    return compact('cursor', 'feed');
});
```

`name` 請使用 URL 安全的字串。

演算法回傳值為包含 `cursor` 與 `feed` 的陣列。

```php theme={null}
[
    'cursor' => '',
    'feed' => [
       ['post' => 'at://'],
       ['post' => 'at://'],
    ],
]
```

套件會自動註冊所需的所有路由。

* `http://localhost/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:web:example.com/app.bsky.feed.generator/artisan`
* `http://localhost/xrpc/app.bsky.feed.describeFeedGenerator`
* `http://localhost/.well-known/did.json`
* Service DID 會從目前的 URL 自動產生（例：`did:web:example.com`）。

<Tip>
  你只需要決定 FeedGenerator 的 `name` 與實作內容而已。
</Tip>

## 公開 feed（建立指令）

僅在 Laravel 應用程式中實作 FeedGenerator 並不會發佈至 Bluesky。需建立呼叫 `publishFeedGenerator` 的指令並執行。

<Steps>
  <Step title="產生指令">
    ```bash theme={null}
    php artisan make:command PublishGeneratorCommand
    ```
  </Step>

  <Step title="實作指令">
    ```php theme={null}
    namespace App\Console\Commands;

    use Illuminate\Console\Command;
    use Revolution\Bluesky\Facades\Bluesky;
    use Revolution\Bluesky\Record\Generator;

    class PublishGeneratorCommand extends Command
    {
        protected $signature = 'bluesky:publish-generator';

        protected $description = '將 FeedGenerator 公開至 Bluesky';

        public function handle()
        {
            $generator = Generator::create(did: 'did:web:example.com', displayName: 'Feed name')
                                  ->description('Feed description');

            $res = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
                          ->publishFeedGenerator(name: 'artisan', generator: $generator);

            dump($res->json());

            return 0;
        }
    }
    ```
  </Step>

  <Step title="執行指令">
    ```bash theme={null}
    php artisan bluesky:publish-generator
    ```

    成功後會在 Bluesky 個人資料的 feed 清單中新增連結。`publishFeedGenerator` 僅更新資訊，可重複執行多次。
  </Step>
</Steps>

## 建立多個 FeedGenerator

只要更換 `name` 並多次呼叫 `register`，即可建立多個 feed。

```php theme={null}
// AppServiceProvider::boot()

use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::register(name: 'feed1', algo: function() {
    // feed1 的實作
});

FeedGenerator::register(name: 'feed2', algo: function() {
    // feed2 的實作
});
```

發佈指令中也同樣多次呼叫 `publishFeedGenerator`。

```php theme={null}
// PublishGeneratorCommand

Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'));

$generator1 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 1')
                       ->description('Feed 1');
Bluesky::publishFeedGenerator(name: 'feed1', generator: $generator1);

$generator2 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 2')
                       ->description('Feed 2');
Bluesky::publishFeedGenerator(name: 'feed2', generator: $generator2);
```

## 分離演算法類別

使用獨立的類別取代閉包，可使程式碼更易整理。建立實作 `FeedGeneratorAlgorithm` contract 的 callable 類別，並於 `AppServiceProvider` 中註冊。

```php theme={null}
// 建立於任意位置

namespace App\FeedGenerator;

use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\FeedGeneratorAlgorithm;

class ArtisanFeed implements FeedGeneratorAlgorithm
{
    public function __invoke(int $limit, ?string $cursor, ?string $user, Request $request): array
    {
        // 因 API 的暫時性限制而需認證
        $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
            ->searchPosts(q: '#laravel', until: $cursor, limit: $limit);

        $cursor = data_get($response->collect('posts')->last(), 'indexedAt');

        $feed = $response->collect('posts')->map(function (array $post) {
            return ['post' => data_get($post, 'uri')];
        })->toArray();

        info('user: '.$user);
        info('header', $request->header());

        return compact('cursor', 'feed');
    }
}
```

```php theme={null}
// AppServiceProvider::boot()

use Revolution\Bluesky\FeedGenerator\FeedGenerator;
use App\FeedGenerator\ArtisanFeed;

FeedGenerator::register(name: 'artisan', algo: ArtisanFeed::class);
```

## 認證

官方 starter kit 的認證功能預設為啟用。若要停用，可將只回傳使用者 DID 的閉包傳入 `validateAuthUsing`。

```php theme={null}
// AppServiceProvider::boot()

use Illuminate\Http\Request;
use Revolution\Bluesky\Crypto\JsonWebToken;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;

FeedGenerator::validateAuthUsing(function (?string $jwt, Request $request): ?string {
    [, $payload] = JsonWebToken::explode($jwt);
    return data_get($payload, 'iss');
});
```

<Warning>
  Feed 會受帳號的「語言設定」影響。若 FeedGenerator 已取得貼文但 Bluesky 上仍未顯示 feed，請檢查帳號的語言設定。
</Warning>

## 進階用法

透過 Artisan 指令與任務排程將貼文儲存至資料庫，演算法中只需從 DB 取得資料，即可實現無需呼叫 API 的高速 feed。

```php theme={null}
// 從 DB 回傳 feed 的演算法範例

FeedGenerator::register(name: 'cached-feed', algo: function(int $limit, ?string $cursor): array {
    $query = \App\Models\Post::query()
        ->orderByDesc('indexed_at')
        ->limit($limit);

    if ($cursor) {
        $query->where('indexed_at', '<', $cursor);
    }

    $posts = $query->get();

    $cursor = $posts->last()?->indexed_at;

    $feed = $posts->map(fn ($post) => ['post' => $post->uri])->toArray();

    return compact('cursor', 'feed');
});
```

```php theme={null}
// 定期蒐集貼文的排程範例（routes/console.php）

use Illuminate\Support\Facades\Schedule;

Schedule::command('bluesky:collect-posts')->everyFiveMinutes();
```

<Info>
  Source：[docs/feed-generator.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/feed-generator.md)
</Info>


## Related topics

- [測試](/zh-TW/packages/laravel-bluesky/testing.md)
- [BlueskyManager 與 HasShortHand](/zh-TW/packages/laravel-bluesky/bluesky-manager.md)
- [Bot 教學 - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/bot-tutorial.md)
- [Laravel Bluesky](/zh-TW/packages/laravel-bluesky/index.md)
- [Crypto — AT Protocol 加密](/zh-TW/packages/laravel-bluesky/crypto.md)
