atp->client->session()->did(); $collection = $collection instanceof BackedEnum ? $collection->value : $collection; $this->checkCollectionScope($collection, 'create'); $response = $this->atp->client->post( endpoint: AtprotoRepo::CreateRecord, body: array_filter( compact('repo', 'collection', 'record', 'rkey', 'validate', 'swapCommit'), fn ($v) => ! is_null($v) ) ); return CreateRecordResponse::fromArray($response->json()); } /** * Delete a record * * @requires transition:generic OR repo:[collection]?action=delete * * @see https://docs.bsky.app/docs/api/com-atproto-repo-delete-record */ #[ScopedEndpoint(Scope::TransitionGeneric, description: 'Delete records from repository')] public function deleteRecord( string|BackedEnum $collection, string $rkey, ?string $swapRecord = null, ?string $swapCommit = null ): DeleteRecordResponse { $repo = $this->atp->client->session()->did(); $collection = $collection instanceof BackedEnum ? $collection->value : $collection; $this->checkCollectionScope($collection, 'delete'); $response = $this->atp->client->post( endpoint: AtprotoRepo::DeleteRecord, body: array_filter( compact('repo', 'collection', 'rkey', 'swapRecord', 'swapCommit'), fn ($v) => ! is_null($v) ) ); return DeleteRecordResponse::fromArray($response->json()); } /** * Put (upsert) a record * * @requires transition:generic OR repo:[collection]?action=update * * @see https://docs.bsky.app/docs/api/com-atproto-repo-put-record */ #[ScopedEndpoint(Scope::TransitionGeneric, description: 'Update records in repository')] public function putRecord( string|BackedEnum $collection, string $rkey, array $record, bool $validate = true, ?string $swapRecord = null, ?string $swapCommit = null ): PutRecordResponse { $repo = $this->atp->client->session()->did(); $collection = $collection instanceof BackedEnum ? $collection->value : $collection; $this->checkCollectionScope($collection, 'update'); $response = $this->atp->client->post( endpoint: AtprotoRepo::PutRecord, body: array_filter( compact('repo', 'collection', 'rkey', 'record', 'validate', 'swapRecord', 'swapCommit'), fn ($v) => ! is_null($v) ) ); return PutRecordResponse::fromArray($response->json()); } /** * Get a record * * @see https://docs.bsky.app/docs/api/com-atproto-repo-get-record */ #[PublicEndpoint] public function getRecord( string $repo, string|BackedEnum $collection, string $rkey, ?string $cid = null ): GetRecordResponse { $collection = $collection instanceof BackedEnum ? $collection->value : $collection; $response = $this->atp->client->get( endpoint: AtprotoRepo::GetRecord, params: compact('repo', 'collection', 'rkey', 'cid') ); return GetRecordResponse::fromArray($response->json()); } /** * List records in a collection * * @see https://docs.bsky.app/docs/api/com-atproto-repo-list-records */ #[PublicEndpoint] public function listRecords( string $repo, string|BackedEnum $collection, int $limit = 50, ?string $cursor = null, bool $reverse = false ): ListRecordsResponse { $collection = $collection instanceof BackedEnum ? $collection->value : $collection; $response = $this->atp->client->get( endpoint: AtprotoRepo::ListRecords, params: compact('repo', 'collection', 'limit', 'cursor', 'reverse') ); return ListRecordsResponse::fromArray($response->json()); } /** * Upload a new blob, to be referenced from a repository record * * The blob will be deleted if it is not referenced within a time window. * * @requires transition:generic (blob:*\/*\) * * @param UploadedFile|SplFileInfo|string $file The file to upload * @param string|null $mimeType MIME type (required for string input, auto-detected for file objects) * * @throws InvalidArgumentException|Throwable When $file is a string and $mimeType is not provided * * @see https://docs.bsky.app/docs/api/com-atproto-repo-upload-blob */ #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'blob:*/*')] public function uploadBlob(UploadedFile|SplFileInfo|string $file, ?string $mimeType = null): BlobReference { // Handle different input types if ($file instanceof UploadedFile) { $data = $file->getContent(); $mimeType ??= $file->getMimeType(); } elseif ($file instanceof SplFileInfo) { $data = file_get_contents($file->getRealPath()); $mimeType ??= mime_content_type($file->getRealPath()) ?: 'application/octet-stream'; } else { throw_if($mimeType === null, new InvalidArgumentException('The $mimeType parameter is required when $file is a string.')); $data = $file; } $response = $this->atp->client->postBlob( endpoint: AtprotoRepo::UploadBlob, data: $data, mimeType: $mimeType ); return BlobReference::fromArray($response->json()['blob']); } /** * Describe the repository * * @see https://docs.bsky.app/docs/api/com-atproto-repo-describe-repo */ #[PublicEndpoint] public function describeRepo(string $repo): DescribeRepoResponse { $response = $this->atp->client->get( endpoint: AtprotoRepo::DescribeRepo, params: compact('repo') ); return DescribeRepoResponse::fromArray($response->json()); } /** * Check if the session has repo access for a specific collection and action. * * This check is in addition to the transition:generic scope check. * Users need either transition:generic OR the specific repo scope. */ protected function checkCollectionScope(string $collection, string $action): void { $session = $this->atp->client->session(); $checker = app(ScopeChecker::class); // If user has transition:generic, they have broad access if ($checker->hasScope($session, Scope::TransitionGeneric)) { return; } // Otherwise, check for specific repo scope $checker->checkRepoScopeOrFail($session, $collection, $action); } }