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

# BlueskyManager 與 HasShortHand

> 說明 Bluesky Facade 的實體 BlueskyManager 的架構，以及 HasShortHand trait 所提供的捷徑方法。

## 概觀

`Bluesky` Facade 是 `BlueskyManager` 的薄層封裝。`BlueskyManager` 管理多個 Agent，`HasShortHand` trait 則整合了常用方法的捷徑。

## 架構

```mermaid theme={null}
graph TD
    A["Bluesky Facade<br>(Revolution\\Bluesky\\Facades\\Bluesky)"] --> B["BlueskyManager<br>(Factory 介面)"]
    B --> C["HasShortHand trait<br>捷徑方法群"]
    B --> D["Conditionable trait<br>when() / unless()"]
    B --> E["Macroable trait<br>macro() / mixin()"]
    B --> F{"Agent"}
    F --> G["LegacyAgent<br>(App Password)"]
    F --> H["OAuthAgent<br>(OAuth)"]
    G --> I["LegacySession"]
    H --> J["OAuthSession"]
```

`BlueskyServiceProvider` 將 `Factory::class` 註冊為 `BlueskyManager` 的 scoped 單例（每次請求 1 個實體）。

```php theme={null}
// BlueskyServiceProvider::register()
$this->app->scoped(Factory::class, BlueskyManager::class);
```

Facade 的 `getFacadeAccessor()` 會解析此綁定。

```php theme={null}
// Facades/Bluesky.php
protected static function getFacadeAccessor(): string
{
    return Factory::class;
}
```

## BlueskyManager 的核心方法

以下是 `BlueskyManager` 自身定義的核心方法。將其與 `HasShortHand` trait 的方法分開理解，有助於掌握內部結構。

### 認證

| 方法                                                                     | 說明                                           |
| ---------------------------------------------------------------------- | -------------------------------------------- |
| `login(string $identifier, string $password, ?string $service = null)` | 使用 App Password 進行認證並設定 `LegacyAgent`        |
| `withToken(?AbstractSession $token)`                                   | 傳入 `OAuthSession` 或 `LegacySession` 設定 Agent |
| `check(): bool`                                                        | 確認是否已認證（並驗證 token 有效期限）                      |
| `refreshSession()`                                                     | 刷新 token                                     |
| `logout()`                                                             | 清除 Agent                                     |

### Agent 操作

| 方法                         | 說明                  |
| -------------------------- | ------------------- |
| `agent(): ?Agent`          | 取得目前的 Agent         |
| `withAgent(?Agent $agent)` | 直接設定 Agent          |
| `assertDid(): string`      | 回傳已認證的 DID，未認證時擲出例外 |

### HTTP client

| 方法                                                                                               | 說明                      |
| ------------------------------------------------------------------------------------------------ | ----------------------- |
| `client(bool $auth = true): AtpClient`                                                           | 回傳已認證（或匿名）的 XRPC client |
| `public(): BskyClient`                                                                           | 回傳無需認證的公開端點 client      |
| `send(BackedEnum\|string $api, string $method, bool $auth, ?array $params, ?callable $callback)` | 直接呼叫任意的 AT Protocol API |

### 工具

| 方法                                | 說明                          |
| --------------------------------- | --------------------------- |
| `identity(): Identity`            | 取得 DID 解析等 identity 服務      |
| `pds(): PDS`                      | 取得 PDS 資訊服務                 |
| `entryway(?string $path): string` | 回傳服務 URL（如 `bsky.social` 等） |
| `publicEndpoint(): string`        | 回傳公開端點 URL                  |

## HasShortHand trait

`HasShortHand` trait 將 AT Protocol 的低階 API 封裝為易讀的 PHP 方法。只要在 `BlueskyManager` 中寫入 `use HasShortHand;`，這些方法即可直接透過 `Bluesky::` 呼叫。

```php theme={null}
// BlueskyManager.php
class BlueskyManager implements Factory
{
    use Conditionable;
    use HasShortHand;
    use Macroable;
    // ...
}
```

<Info>
  之所以將 `HasShortHand` 獨立為 trait，是為了便於測試與客製化。透過此 trait，可讓 `BlueskyManager` 的程式碼保持精簡，同時提供豐富的 API 捷徑。
</Info>

### 貼文 / feed

| 方法                                                              | 說明            |
| --------------------------------------------------------------- | ------------- |
| `post(Post\|string\|array $text)`                               | 建立新貼文         |
| `getPost(string $uri)`                                          | 依 AT-URI 取得貼文 |
| `getPosts(array $uris)`                                         | 批次取得多則貼文      |
| `deletePost(string $uri)`                                       | 刪除貼文          |
| `getTimeline(?string $algorithm, ?int $limit, ?string $cursor)` | 取得首頁時間軸       |
| `getAuthorFeed(?string $actor, ...)`                            | 取得指定帳號的 feed  |
| `searchPosts(string $q, ...)`                                   | 搜尋貼文          |

### 互動

| 方法                                   | 說明        |
| ------------------------------------ | --------- |
| `like(Like\|StrongRef $subject)`     | 建立按讚      |
| `deleteLike(string $uri)`            | 取消按讚      |
| `repost(Repost\|StrongRef $subject)` | 建立轉發      |
| `deleteRepost(string $uri)`          | 取消轉發      |
| `getActorLikes(?string $actor, ...)` | 取得帳號的按讚清單 |

### 追蹤

| 方法                                  | 說明      |
| ----------------------------------- | ------- |
| `follow(Follow\|string $did)`       | 追蹤      |
| `deleteFollow(string $uri)`         | 取消追蹤    |
| `getFollowers(?string $actor, ...)` | 取得追蹤者清單 |
| `getFollows(?string $actor, ...)`   | 取得追蹤中清單 |

### 個人資料 / 帳號

| 方法                                  | 說明               |
| ----------------------------------- | ---------------- |
| `getProfile(?string $actor)`        | 取得個人資料           |
| `upsertProfile(callable $callback)` | 更新個人資料           |
| `resolveHandle(string $handle)`     | 將 handle 解析為 DID |

### 媒體

| 方法                                                         | 說明             |
| ---------------------------------------------------------- | -------------- |
| `uploadBlob(StreamInterface\|string $data, string $type)`  | 上傳圖像等 blob     |
| `uploadVideo(StreamInterface\|string $data, string $type)` | 上傳影片           |
| `getJobStatus(string $jobId)`                              | 檢查影片上傳的 job 狀態 |
| `getUploadLimits()`                                        | 取得上傳限制         |

### 通知

| 方法                                        | 說明       |
| ----------------------------------------- | -------- |
| `listNotifications(...)`                  | 取得通知清單   |
| `countUnreadNotifications(...)`           | 取得未讀通知件數 |
| `updateSeenNotifications(string $seenAt)` | 更新已讀狀態   |

### AT Protocol record 操作

| 方法                                                                  | 說明             |
| ------------------------------------------------------------------- | -------------- |
| `createRecord(string $repo, string $collection, ...)`               | 建立 record      |
| `getRecord(string $repo, string $collection, string $rkey, ...)`    | 取得 record      |
| `listRecords(string $repo, string $collection, ...)`                | 取得 record 清單   |
| `putRecord(string $repo, string $collection, string $rkey, ...)`    | 更新 / 建立 record |
| `deleteRecord(string $repo, string $collection, string $rkey, ...)` | 刪除 record      |

### Feed generator / labeler

| 方法                                                                     | 說明                |
| ---------------------------------------------------------------------- | ----------------- |
| `publishFeedGenerator(BackedEnum\|string $name, Generator $generator)` | 發佈 feed generator |
| `createThreadGate(string $post, ?array $allow)`                        | 建立 thread gate    |
| `upsertLabelDefinitions(callable $callback)`                           | 更新標籤定義            |
| `deleteLabelDefinitions()`                                             | 刪除標籤定義            |
| `createLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 附加標籤              |
| `deleteLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 移除標籤              |

## 常用捷徑範例

### 貼文

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

$response = Bluesky::login(
    identifier: config('bluesky.identifier'),
    password: config('bluesky.password'),
)->post('Hello Bluesky');
```

### 回覆

回覆時透過 `Post::build()` 設定父貼文的 `StrongRef`。`HasShortHand` 中並沒有專用的 `reply()` 方法，而是將 `Post` 物件傳給 `post()`。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Types\StrongRef;

$parent = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

$reply = Post::create('回覆的文字內容')
    ->reply(root: $parent, parent: $parent);

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post($reply);
```

### 按讚 / 轉發

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Types\StrongRef;

$ref = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

// 按讚
Bluesky::withToken($session)->like($ref);

// 轉發
Bluesky::withToken($session)->repost($ref);
```

### 更新個人資料

`upsertProfile()` 會取得目前的個人資料，並儲存於閉包中修改的內容。可透過 `Profile` 物件的方法設定 displayName、description 等。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->displayName('新的顯示名稱')
                ->description('個人資料說明文字');
    });
```

#### 設定 self label

可以透過 `SelfLabels` 對整個帳號設定 self label。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;
use Revolution\Bluesky\Types\SelfLabels;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->labels(SelfLabels::make(['!no-unauthenticated']));
    });
```

### 直接呼叫任意 API

`HasShortHand` 中沒有的方法，可使用 `send()` 或 `client()`。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

// 以 send() 呼叫任意 XRPC 方法
$response = Bluesky::withToken($session)
    ->send(
        api: 'app.bsky.actor.getProfiles',
        method: 'get',
        params: ['actors' => ['did:plc:...', 'did:plc:...']],
    );

// 以 client() 進行更細部的操作
$response = Bluesky::withToken($session)
    ->client()
    ->bsky()
    ->getProfiles(actors: ['did:plc:...']);
```

## 透過 Facade 與直接使用的差異

`Bluesky::post()` 與 `app(Factory::class)->post()` 對應的是同一個 `BlueskyManager` 實體。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Factory;

// 透過 Facade（常見用法）
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post('Hello');

// 從容器直接取得
$manager = app(Factory::class);
$manager->login(config('bluesky.identifier'), config('bluesky.password'))
        ->post('Hello');

// 以型別提示進行 DI
class MyService
{
    public function __construct(private Factory $bluesky) {}

    public function doPost(): void
    {
        $this->bluesky->login(
            config('bluesky.identifier'),
            config('bluesky.password'),
        )->post('Hello from DI');
    }
}
```

由於是 `scoped` 註冊，因此在一次請求內，`login()` 之後的 session 狀態會被保留。

## Conditionable 與 Macroable

`BlueskyManager` 同時使用了 `Conditionable` 與 `Macroable` trait。

### Conditionable：when() / unless()

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->when(config('app.env') === 'production', function ($bluesky) {
        $bluesky->post('從正式環境的貼文');
    });
```

### Macroable：新增自訂方法

透過 `macro()` 可從應用程式端向 `BlueskyManager` 增加方法。通常在 `AppServiceProvider` 的 `boot()` 中定義。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

Bluesky::macro('postWithHashtag', function (string $text, string $tag) {
    /** @var \Revolution\Bluesky\BlueskyManager $this */
    return $this->post("{$text} #{$tag}");
});

// 使用範例
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->postWithHashtag('Hello', 'laravel');
```

## 插入自訂 Agent

透過 `withAgent()` 可直接設定任意的 `Agent` 實作。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Agent;

// 實作自訂 Agent 時，需實作 Agent 介面
class MyCustomAgent implements Agent
{
    // ...
}

Bluesky::withAgent(new MyCustomAgent());
```

<Info>
  一般認證（`login()` / `withToken()`）會自動設定 agent，因此直接使用 `withAgent()` 的機會主要在測試時。
</Info>

## 參考連結

* [認證方式比較](/zh-TW/packages/laravel-bluesky/authentication) — App Password 與 OAuth 的詳細差異
* [Basic client](/zh-TW/packages/laravel-bluesky/basic-client) — 認證後的 API 操作範例
* [測試](/zh-TW/packages/laravel-bluesky/testing) — 使用 Fake 進行測試的方式
* Source：[src/BlueskyManager.php](https://github.com/invokable/laravel-bluesky/blob/main/src/BlueskyManager.php)
* Source：[src/HasShortHand.php](https://github.com/invokable/laravel-bluesky/blob/main/src/HasShortHand.php)


## Related topics

- [Bot 教學 - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/bot-tutorial.md)
- [認證方式比較 - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/authentication.md)
- [Queue 與 Job](/zh-TW/queues.md)
- [Illuminate\Support\Manager — Driver 系統解剖](/zh-TW/advanced/manager.md)
- [Socialite - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/socialite.md)
