Bluesky Facade는 BlueskyManager의 얇은 래퍼입니다. BlueskyManager는 여러 Agent를 관리하며, HasShortHand 트레이트가 자주 사용하는 메서드의 단축키를 한데 모아 제공합니다.
아키텍처
BlueskyServiceProvider는 Factory::class를 BlueskyManager로서 스코프 싱글톤(요청당 1개 인스턴스)으로 등록합니다.
// BlueskyServiceProvider::register()
$this->app->scoped(Factory::class, BlueskyManager::class);
Facade의 getFacadeAccessor()가 이 바인딩을 해결합니다.
// Facades/Bluesky.php
protected static function getFacadeAccessor(): string
{
return Factory::class;
}
BlueskyManager의 코어 메서드
BlueskyManager 자체가 정의하는 코어 메서드는 다음과 같습니다. HasShortHand 트레이트의 메서드와 구분해서 파악해 두면 내부 구조 이해에 도움이 됩니다.
| 메서드 | 설명 |
|---|
login(string $identifier, string $password, ?string $service = null) | App Password로 인증하고 LegacyAgent를 설정 |
withToken(?AbstractSession $token) | OAuthSession 또는 LegacySession을 전달하여 Agent를 설정 |
check(): bool | 인증되었는지 확인(토큰 유효 기간도 검증) |
refreshSession() | 토큰을 리프레시 |
logout() | Agent를 초기화 |
Agent 조작
| 메서드 | 설명 |
|---|
agent(): ?Agent | 현재의 Agent를 취득 |
withAgent(?Agent $agent) | Agent를 직접 설정 |
assertDid(): string | 인증된 DID를 반환. 미인증이면 예외를 throw |
HTTP 클라이언트
| 메서드 | 설명 |
|---|
client(bool $auth = true): AtpClient | 인증된(또는 익명의) XRPC 클라이언트를 반환 |
public(): BskyClient | 인증이 필요 없는 퍼블릭 엔드포인트 클라이언트를 반환 |
send(BackedEnum|string $api, string $method, bool $auth, ?array $params, ?callable $callback) | 임의의 AT Protocol API를 직접 호출 |
유틸리티
| 메서드 | 설명 |
|---|
identity(): Identity | DID 해결 등 아이덴티티 서비스를 취득 |
pds(): PDS | PDS 정보 서비스를 취득 |
entryway(?string $path): string | 서비스 URL(bsky.social 등)을 반환 |
publicEndpoint(): string | 퍼블릭 엔드포인트 URL을 반환 |
HasShortHand 트레이트
HasShortHand 트레이트는 AT Protocol의 저수준 API를 PHP에 알맞은 이해하기 쉬운 메서드로 래핑합니다. BlueskyManager에 use HasShortHand;라고 기재하기만 하면, 이 메서드들을 Bluesky::에서 직접 호출할 수 있습니다.
// BlueskyManager.php
class BlueskyManager implements Factory
{
use Conditionable;
use HasShortHand;
use Macroable;
// ...
}
HasShortHand를 독립된 트레이트로 만든 이유는 테스트·커스터마이즈의 용이성 때문입니다. 이 트레이트를 사용하면 BlueskyManager의 코드를 단순하게 유지하면서도 풍부한 API 단축키를 제공할 수 있습니다.
게시·피드
| 메서드 | 설명 |
|---|
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, ...) | 지정 계정의 피드를 취득 |
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) | 핸들을 DID로 해결 |
미디어
| 메서드 | 설명 |
|---|
uploadBlob(StreamInterface|string $data, string $type) | 이미지 등의 블롭을 업로드 |
uploadVideo(StreamInterface|string $data, string $type) | 동영상을 업로드 |
getJobStatus(string $jobId) | 동영상 업로드 잡의 상태를 확인 |
getUploadLimits() | 업로드 제한을 취득 |
| 메서드 | 설명 |
|---|
listNotifications(...) | 알림 목록을 취득 |
countUnreadNotifications(...) | 미읽음 알림 건수를 취득 |
updateSeenNotifications(string $seenAt) | 읽음 처리를 갱신 |
AT Protocol 레코드 조작
| 메서드 | 설명 |
|---|
createRecord(string $repo, string $collection, ...) | 레코드를 작성 |
getRecord(string $repo, string $collection, string $rkey, ...) | 레코드를 취득 |
listRecords(string $repo, string $collection, ...) | 레코드 목록을 취득 |
putRecord(string $repo, string $collection, string $rkey, ...) | 레코드를 갱신·작성 |
deleteRecord(string $repo, string $collection, string $rkey, ...) | 레코드를 삭제 |
피드 제너레이터·라벨러
| 메서드 | 설명 |
|---|
publishFeedGenerator(BackedEnum|string $name, Generator $generator) | 피드 제너레이터를 공개 |
createThreadGate(string $post, ?array $allow) | 스레드 게이트를 작성 |
upsertLabelDefinitions(callable $callback) | 라벨 정의를 갱신 |
deleteLabelDefinitions() | 라벨 정의를 삭제 |
createLabels(RepoRef|StrongRef|array $subject, array $labels) | 라벨을 부여 |
deleteLabels(RepoRef|StrongRef|array $subject, array $labels) | 라벨을 삭제 |
자주 사용하는 단축키 예시
게시하기
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 객체를 전달합니다.
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);
좋아요·리포스트
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 등을 설정할 수 있습니다.
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('프로필 설명문');
});
셀프 라벨 설정하기
SelfLabels를 사용해 계정 전체에 대해 셀프 라벨을 설정할 수 있습니다.
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()를 사용합니다.
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 인스턴스에 대한 조작입니다.
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() 이후의 세션 상태가 유지됩니다.
Conditionable과 Macroable
BlueskyManager는 Conditionable과 Macroable 트레이트도 사용합니다.
Conditionable: when() / unless()
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() 안에서 정의하는 것이 일반적입니다.
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 구현을 직접 설정할 수 있습니다.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Agent;
// 커스텀 Agent를 구현하는 경우 Agent 인터페이스를 구현
class MyCustomAgent implements Agent
{
// ...
}
Bluesky::withAgent(new MyCustomAgent());
일반적인 인증(login() / withToken())을 사용하면 자동으로 에이전트가 설정되므로, withAgent()를 직접 사용하는 경우는 주로 테스트 시입니다.
참고 링크