> ## 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 Generator는 Bluesky에서 동작하는 "알고리즘적 피드"의 구조입니다. 특정 키워드나 사용자 조건에 맞춘 자체 피드를 공개할 수 있습니다. `laravel-bluesky`를 사용하면 Laravel 애플리케이션에서 Feed Generator를 손쉽게 구현할 수 있습니다.

<Info>
  공식 튜토리얼: [커스텀 피드 만들기](https://atproto.com/ja/guides/custom-feed-tutorial)
</Info>

<Info>
  공식 스타터 키트: [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: 피드 데이터 취득
    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>

## 피드 공개(명령어 생성)

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 = 'Bluesky에 FeedGenerator를 공개';

        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 프로필의 피드 목록에 링크가 추가됩니다. `publishFeedGenerator`는 정보를 갱신만 하기 때문에 몇 번이든 실행할 수 있습니다.
  </Step>
</Steps>

## 다중 FeedGenerator 생성

`name`을 바꿔 `register`를 여러 번 호출하기만 하면 여러 피드를 생성할 수 있습니다.

```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` 컨트랙트를 구현한 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);
```

## 인증

공식 스타터 키트의 인증 기능은 기본적으로 활성화되어 있습니다. 비활성화하려면 `validateAuthUsing`에 단순히 사용자 DID를 반환하는 클로저를 전달합니다.

```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>
  피드가 계정의 "언어 설정"에 영향을 받습니다. FeedGenerator가 게시를 취득하고 있음에도 Bluesky에서 피드가 표시되지 않는 경우에는 계정의 언어 설정을 확인해 주세요.
</Warning>

## 고급 사용법

Artisan 명령어와 태스크 스케줄을 사용해 게시를 데이터베이스에 저장하고, 알고리즘에서는 DB에서 취득하기만 하도록 하면 API 호출 없이 빠른 피드를 실현할 수 있습니다.

```php theme={null}
// 알고리즘에서 DB로부터 피드를 반환하는 예시

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

- [테스트](/ko/packages/laravel-bluesky/testing.md)
- [BlueskyManager와 HasShortHand](/ko/packages/laravel-bluesky/bluesky-manager.md)
- [봇 튜토리얼 - Laravel Bluesky](/ko/packages/laravel-bluesky/bot-tutorial.md)
- [Crypto — AT Protocol 암호화](/ko/packages/laravel-bluesky/crypto.md)
- [라우트](/ko/packages/laravel-bluesky/route.md)
