Laravel AT Protocol Client (alpha & unstable)

Merge branch 'refs/heads/dev'

+41 -38
docs/extensions.md
··· 14 14 | `AtpClient::hasDomainExtension($domain, $name)` | Check if a request client extension is registered | 15 15 | `AtpClient::flushExtensions()` | Clear all extensions (useful for testing) | 16 16 17 - The same methods are available on `AtpPublicClient` for unauthenticated extensions. 18 - 19 17 ### Extension Types 20 18 21 19 | Type | Access Pattern | Use Case | ··· 30 28 ```bash 31 29 # Create a domain client extension 32 30 php artisan make:atp-client AnalyticsClient 33 - 34 - # Create a public domain client extension 35 - php artisan make:atp-client DiscoverClient --public 36 31 37 32 # Create a request client extension for an existing domain 38 33 php artisan make:atp-request MetricsClient --domain=bsky 39 - 40 - # Create a public request client extension 41 - php artisan make:atp-request TrendingClient --domain=bsky --public 42 34 ``` 43 35 44 36 The generated files are placed in configurable directories. You can customize these paths in `config/client.php`: ··· 46 38 ```php 47 39 'generators' => [ 48 40 'client_path' => 'app/Services/Clients', 49 - 'client_public_path' => 'app/Services/Clients/Public', 50 41 'request_path' => 'app/Services/Clients/Requests', 51 - 'request_public_path' => 'app/Services/Clients/Public/Requests', 52 42 ], 53 43 ``` 54 44 ··· 225 215 $authorMetrics = $client->bsky->metrics->getAuthorMetrics('someone.bsky.social'); 226 216 ``` 227 217 228 - ## Public Client Extensions 218 + ## Public vs Authenticated Mode 229 219 230 - The `AtpPublicClient` supports the same extension system for unauthenticated API access: 220 + The `AtpClient` class works in both public and authenticated modes. Both `Atp::public()` and `Atp::as()` return the same `AtpClient` class: 231 221 232 222 ```php 233 - use SocialDept\AtpClient\Client\Public\AtpPublicClient; 223 + // Public mode - no authentication 224 + $publicClient = Atp::public('https://public.api.bsky.app'); 225 + $publicClient->bsky->actor->getProfile('someone.bsky.social'); 234 226 235 - // Domain client extension 236 - AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 237 - 238 - // Request client extension on existing domain 239 - AtpPublicClient::extendDomain('bsky', 'trending', fn($bsky) => new TrendingClient($bsky)); 227 + // Authenticated mode - with session 228 + $authClient = Atp::as('did:plc:xxx'); 229 + $authClient->bsky->actor->getProfile('someone.bsky.social'); 240 230 ``` 241 231 242 - For public request clients, extend `PublicRequest` instead of `Request`: 243 - 244 - ```php 245 - <?php 246 - 247 - namespace App\Atp; 248 - 249 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 250 - 251 - class TrendingPublicClient extends PublicRequest 252 - { 253 - public function getPopularFeeds(int $limit = 10): array 254 - { 255 - return $this->atp->bsky->feed->getPopularFeedGenerators($limit)->feeds; 256 - } 257 - } 258 - ``` 232 + Extensions registered on `AtpClient` work in both modes. The underlying HTTP layer automatically handles authentication based on whether a session is present. 259 233 260 234 ## Registering Multiple Extensions 261 235 ··· 272 246 AtpClient::extendDomain('bsky', 'metrics', fn($bsky) => new BskyMetricsClient($bsky)); 273 247 AtpClient::extendDomain('bsky', 'lists', fn($bsky) => new BskyListsClient($bsky)); 274 248 AtpClient::extendDomain('atproto', 'backup', fn($atproto) => new RepoBackupClient($atproto)); 275 - 276 - // Public client extensions 277 - AtpPublicClient::extend('discover', fn($atp) => new DiscoverClient($atp)); 278 249 } 279 250 ``` 280 251 ··· 451 422 } 452 423 } 453 424 ``` 425 + 426 + ### Documenting Scope Requirements 427 + 428 + Use the `#[ScopedEndpoint]` and `#[PublicEndpoint]` attributes to document the authentication requirements of your extension methods: 429 + 430 + ```php 431 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 432 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 433 + use SocialDept\AtpClient\Client\Requests\Request; 434 + use SocialDept\AtpClient\Enums\Scope; 435 + 436 + class BskyMetricsClient extends Request 437 + { 438 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getTimeline')] 439 + public function getTimelineMetrics(): array 440 + { 441 + $timeline = $this->atp->bsky->feed->getTimeline(); 442 + // Process and return metrics... 443 + } 444 + 445 + #[PublicEndpoint] 446 + public function getPublicPostMetrics(string $uri): array 447 + { 448 + $thread = $this->atp->bsky->feed->getPostThread($uri); 449 + // Process and return metrics... 450 + } 451 + } 452 + ``` 453 + 454 + > **Note:** These attributes currently serve as documentation only. Runtime scope enforcement will be implemented in a future release. Using them correctly now ensures forward compatibility. 455 + 456 + Methods with `#[ScopedEndpoint]` indicate they require authentication, while methods with `#[PublicEndpoint]` work without authentication. See [scopes.md](scopes.md) for full documentation on scope handling. 454 457 455 458 ## Available Domains 456 459
+408
docs/scopes.md
··· 1 + # OAuth Scopes 2 + 3 + The AT Protocol uses OAuth scopes to control what actions an application can perform on behalf of a user. AtpClient provides attributes for documenting scope requirements on endpoints. 4 + 5 + > **Note:** The `#[ScopedEndpoint]` and `#[PublicEndpoint]` attributes currently serve as documentation only. Runtime scope validation and enforcement will be implemented in a future release. Using these attributes correctly now ensures forward compatibility. 6 + 7 + ## Quick Reference 8 + 9 + ### Scope Enum 10 + 11 + ```php 12 + use SocialDept\AtpClient\Enums\Scope; 13 + 14 + // Transition scopes (current AT Protocol scopes) 15 + Scope::Atproto // 'atproto' - Full access 16 + Scope::TransitionGeneric // 'transition:generic' - General API access 17 + Scope::TransitionEmail // 'transition:email' - Email access 18 + Scope::TransitionChat // 'transition:chat.bsky' - Chat access 19 + 20 + // Granular scope builders (future AT Protocol scopes) 21 + Scope::repo('app.bsky.feed.post', ['create', 'delete']) // Record operations 22 + Scope::rpc('app.bsky.feed.getTimeline') // RPC endpoint access 23 + Scope::blob('image/*') // Blob upload access 24 + Scope::account('email') // Account attribute access 25 + Scope::identity('handle') // Identity attribute access 26 + ``` 27 + 28 + ### ScopedEndpoint Attribute 29 + 30 + ```php 31 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 32 + use SocialDept\AtpClient\Enums\Scope; 33 + 34 + #[ScopedEndpoint(Scope::TransitionGeneric)] 35 + public function getTimeline(): GetTimelineResponse 36 + { 37 + // Method implementation 38 + } 39 + ``` 40 + 41 + ## Understanding AT Protocol Scopes 42 + 43 + ### Current Transition Scopes 44 + 45 + The AT Protocol is currently in a transition period where broad "transition scopes" are used: 46 + 47 + | Scope | Description | 48 + |-------|-------------| 49 + | `atproto` | Full access to the AT Protocol | 50 + | `transition:generic` | General API access for most operations | 51 + | `transition:email` | Access to email-related operations | 52 + | `transition:chat.bsky` | Access to Bluesky chat features | 53 + 54 + ### Future Granular Scopes 55 + 56 + The AT Protocol is moving toward granular scopes that provide fine-grained access control: 57 + 58 + ```php 59 + // Record operations 60 + 'repo:app.bsky.feed.post' // All operations on posts 61 + 'repo:app.bsky.feed.post?action=create' // Only create posts 62 + 'repo:app.bsky.feed.like?action=create&action=delete' // Create or delete likes 63 + 'repo:*' // All collections, all actions 64 + 65 + // RPC endpoint access 66 + 'rpc:app.bsky.feed.getTimeline' // Access to timeline endpoint 67 + 'rpc:app.bsky.feed.*' // All feed endpoints 68 + 69 + // Blob operations 70 + 'blob:image/*' // Upload images 71 + 'blob:*/*' // Upload any blob type 72 + 73 + // Account and identity 74 + 'account:email' // Access email 75 + 'identity:handle' // Manage handle 76 + ``` 77 + 78 + ## The ScopedEndpoint Attribute 79 + 80 + The `#[ScopedEndpoint]` attribute documents scope requirements on methods that require authentication. 81 + 82 + ### Basic Usage 83 + 84 + ```php 85 + <?php 86 + 87 + namespace App\Atp; 88 + 89 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 90 + use SocialDept\AtpClient\Client\Requests\Request; 91 + use SocialDept\AtpClient\Enums\Scope; 92 + 93 + class CustomClient extends Request 94 + { 95 + #[ScopedEndpoint(Scope::TransitionGeneric)] 96 + public function getTimeline(): array 97 + { 98 + return $this->atp->client->get('app.bsky.feed.getTimeline')->json(); 99 + } 100 + } 101 + ``` 102 + 103 + ### With Granular Scope 104 + 105 + Document the future granular scope that will replace the transition scope: 106 + 107 + ```php 108 + #[ScopedEndpoint( 109 + Scope::TransitionGeneric, 110 + granular: 'rpc:app.bsky.feed.getTimeline' 111 + )] 112 + public function getTimeline(): GetTimelineResponse 113 + { 114 + // ... 115 + } 116 + ``` 117 + 118 + ### With Description 119 + 120 + Add a human-readable description for documentation: 121 + 122 + ```php 123 + #[ScopedEndpoint( 124 + Scope::TransitionGeneric, 125 + granular: 'rpc:app.bsky.feed.getTimeline', 126 + description: 'Access to the user\'s home timeline' 127 + )] 128 + public function getTimeline(): GetTimelineResponse 129 + { 130 + // ... 131 + } 132 + ``` 133 + 134 + ### Multiple Scopes (AND Logic) 135 + 136 + When a method requires multiple scopes, all must be present: 137 + 138 + ```php 139 + #[ScopedEndpoint([Scope::TransitionGeneric, Scope::TransitionEmail])] 140 + public function getEmailPreferences(): array 141 + { 142 + // Requires BOTH scopes 143 + } 144 + ``` 145 + 146 + ### Multiple Attributes (OR Logic) 147 + 148 + Use multiple attributes for alternative scope requirements: 149 + 150 + ```php 151 + #[ScopedEndpoint(Scope::Atproto)] 152 + #[ScopedEndpoint(Scope::TransitionGeneric)] 153 + public function getProfile(string $actor): ProfileViewDetailed 154 + { 155 + // Either scope satisfies the requirement 156 + } 157 + ``` 158 + 159 + ## Scope Enforcement (Planned) 160 + 161 + > **Coming Soon:** Runtime scope enforcement is not yet implemented. The following documentation describes planned functionality for a future release. 162 + 163 + ### Configuration 164 + 165 + Configure scope enforcement in `config/client.php` or via environment variables: 166 + 167 + ```php 168 + 'scope_enforcement' => ScopeEnforcementLevel::Permissive, 169 + ``` 170 + 171 + | Level | Behavior | 172 + |-------|----------| 173 + | `Strict` | Throws `MissingScopeException` if required scopes are missing | 174 + | `Permissive` | Logs a warning but attempts the request anyway | 175 + 176 + Set via environment variable: 177 + 178 + ```env 179 + ATP_SCOPE_ENFORCEMENT=strict 180 + ``` 181 + 182 + ### Programmatic Scope Checking 183 + 184 + Check scopes programmatically using the `ScopeChecker`: 185 + 186 + ```php 187 + use SocialDept\AtpClient\Auth\ScopeChecker; 188 + use SocialDept\AtpClient\Facades\Atp; 189 + 190 + $checker = app(ScopeChecker::class); 191 + $session = Atp::as($did)->client->session(); 192 + 193 + // Check if session has a scope 194 + if ($checker->hasScope($session, Scope::TransitionGeneric)) { 195 + // Session has the scope 196 + } 197 + 198 + // Check multiple scopes 199 + if ($checker->check($session, [Scope::TransitionGeneric, Scope::TransitionEmail])) { 200 + // Session has ALL required scopes 201 + } 202 + 203 + // Check and fail if missing (respects enforcement level) 204 + $checker->checkOrFail($session, [Scope::TransitionGeneric]); 205 + 206 + // Check repo scope for specific action 207 + if ($checker->checkRepoScope($session, 'app.bsky.feed.post', 'create')) { 208 + // Can create posts 209 + } 210 + ``` 211 + 212 + ### Granular Pattern Matching 213 + 214 + The scope checker supports wildcard patterns: 215 + 216 + ```php 217 + // Check if session can access any feed endpoint 218 + $checker->matchesGranular($session, 'rpc:app.bsky.feed.*'); 219 + 220 + // Check if session can upload images 221 + $checker->matchesGranular($session, 'blob:image/*'); 222 + 223 + // Check if session has any repo access 224 + $checker->matchesGranular($session, 'repo:*'); 225 + ``` 226 + 227 + ## Route Middleware (Planned) 228 + 229 + > **Coming Soon:** Route middleware is not yet implemented. The following documentation describes planned functionality for a future release. 230 + 231 + Protect Laravel routes based on ATP session scopes: 232 + 233 + ```php 234 + use Illuminate\Support\Facades\Route; 235 + 236 + // Single scope 237 + Route::get('/timeline', TimelineController::class) 238 + ->middleware('atp.scope:transition:generic'); 239 + 240 + // Multiple scopes (AND logic) 241 + Route::get('/email-settings', EmailSettingsController::class) 242 + ->middleware('atp.scope:transition:generic,transition:email'); 243 + ``` 244 + 245 + ### Middleware Configuration 246 + 247 + Configure middleware behavior in `config/client.php`: 248 + 249 + ```php 250 + 'scope_authorization' => [ 251 + // What to do when scope check fails 252 + 'failure_action' => ScopeAuthorizationFailure::Abort, // abort, redirect, or exception 253 + 254 + // Where to redirect (when failure_action is 'redirect') 255 + 'redirect_to' => '/login', 256 + ], 257 + ``` 258 + 259 + | Failure Action | Behavior | 260 + |----------------|----------| 261 + | `Abort` | Returns 403 Forbidden response | 262 + | `Redirect` | Redirects to configured URL | 263 + | `Exception` | Throws `ScopeAuthorizationException` | 264 + 265 + Set via environment variables: 266 + 267 + ```env 268 + ATP_SCOPE_FAILURE_ACTION=redirect 269 + ATP_SCOPE_REDIRECT=/auth/login 270 + ``` 271 + 272 + ### User Model Integration 273 + 274 + For the middleware to work, your User model must implement `HasAtpSession`: 275 + 276 + ```php 277 + <?php 278 + 279 + namespace App\Models; 280 + 281 + use Illuminate\Foundation\Auth\User as Authenticatable; 282 + use SocialDept\AtpClient\Contracts\HasAtpSession; 283 + 284 + class User extends Authenticatable implements HasAtpSession 285 + { 286 + public function getAtpDid(): ?string 287 + { 288 + return $this->atp_did; 289 + } 290 + } 291 + ``` 292 + 293 + ## Public Mode and Scopes 294 + 295 + Methods marked with `#[PublicEndpoint]` can be called without authentication using `Atp::public()`: 296 + 297 + ```php 298 + // Public mode - no authentication required 299 + $client = Atp::public('https://public.api.bsky.app'); 300 + $client->bsky->actor->getProfile('someone.bsky.social'); // Works without auth 301 + 302 + // Authenticated mode - for endpoints requiring scopes 303 + $client = Atp::as($did); 304 + $client->bsky->feed->getTimeline(); // Requires transition:generic scope 305 + ``` 306 + 307 + Methods with `#[PublicEndpoint]` work in both modes, while methods with `#[ScopedEndpoint]` require authentication. 308 + 309 + ## Exception Handling (Planned) 310 + 311 + > **Coming Soon:** These exceptions will be thrown when scope enforcement is implemented in a future release. 312 + 313 + ### MissingScopeException 314 + 315 + Will be thrown when required scopes are missing and enforcement is strict: 316 + 317 + ```php 318 + use SocialDept\AtpClient\Exceptions\MissingScopeException; 319 + 320 + try { 321 + $timeline = $client->bsky->feed->getTimeline(); 322 + } catch (MissingScopeException $e) { 323 + $missing = $e->getMissingScopes(); // Scopes that are missing 324 + $granted = $e->getGrantedScopes(); // Scopes the session has 325 + 326 + // Handle missing scope 327 + } 328 + ``` 329 + 330 + ### ScopeAuthorizationException 331 + 332 + Will be thrown by middleware when route access is denied: 333 + 334 + ```php 335 + use SocialDept\AtpClient\Exceptions\ScopeAuthorizationException; 336 + 337 + try { 338 + // Route protected by atp.scope middleware 339 + } catch (ScopeAuthorizationException $e) { 340 + $required = $e->getRequiredScopes(); 341 + $granted = $e->getGrantedScopes(); 342 + $message = $e->getMessage(); 343 + } 344 + ``` 345 + 346 + ## Best Practices 347 + 348 + ### 1. Document All Scope Requirements 349 + 350 + Always add `#[ScopedEndpoint]` to methods that require authentication: 351 + 352 + ```php 353 + #[ScopedEndpoint( 354 + Scope::TransitionGeneric, 355 + granular: 'rpc:app.bsky.feed.getTimeline', 356 + description: 'Fetches the authenticated user\'s home timeline' 357 + )] 358 + public function getTimeline(): GetTimelineResponse 359 + ``` 360 + 361 + ### 2. Use the Scope Enum 362 + 363 + Prefer the `Scope` enum over string literals for type safety: 364 + 365 + ```php 366 + // Good 367 + #[ScopedEndpoint(Scope::TransitionGeneric)] 368 + 369 + // Avoid 370 + #[ScopedEndpoint('transition:generic')] 371 + ``` 372 + 373 + ### 3. Request Minimal Scopes 374 + 375 + When implementing OAuth, request only the scopes your application needs: 376 + 377 + ```php 378 + $authUrl = Atp::oauth()->getAuthorizationUrl([ 379 + 'scope' => 'atproto transition:generic', 380 + ]); 381 + ``` 382 + 383 + ### 4. Handle Missing Scopes Gracefully 384 + 385 + Check for scope availability before attempting operations: 386 + 387 + ```php 388 + $checker = app(ScopeChecker::class); 389 + $session = $client->client->session(); 390 + 391 + if ($checker->hasScope($session, Scope::TransitionChat)) { 392 + $conversations = $client->chat->getConversations(); 393 + } else { 394 + // Inform user they need to re-authorize with chat scope 395 + } 396 + ``` 397 + 398 + ### 5. Use Permissive Mode in Development 399 + 400 + Start with permissive enforcement during development, then switch to strict for production: 401 + 402 + ```env 403 + # .env.local 404 + ATP_SCOPE_ENFORCEMENT=permissive 405 + 406 + # .env.production 407 + ATP_SCOPE_ENFORCEMENT=strict 408 + ```
+14 -4
src/AtpClient.php
··· 13 13 class AtpClient 14 14 { 15 15 use HasExtensions; 16 + 16 17 /** 17 18 * Raw API communication/networking class 18 19 */ ··· 39 40 public OzoneClient $ozone; 40 41 41 42 public function __construct( 42 - SessionManager $sessions, 43 - string $did, 43 + ?SessionManager $sessions = null, 44 + ?string $did = null, 45 + ?string $serviceUrl = null, 44 46 ) { 45 - // Load the network client 46 - $this->client = new Client($this, $sessions, $did); 47 + // Load the network client (supports both public and authenticated modes) 48 + $this->client = new Client($this, $sessions, $did, $serviceUrl); 47 49 48 50 // Load all function collections 49 51 $this->bsky = new BskyClient($this); 50 52 $this->atproto = new AtprotoClient($this); 51 53 $this->chat = new ChatClient($this); 52 54 $this->ozone = new OzoneClient($this); 55 + } 56 + 57 + /** 58 + * Check if client is in public mode (no authentication). 59 + */ 60 + public function isPublicMode(): bool 61 + { 62 + return $this->client->isPublicMode(); 53 63 } 54 64 }
+5 -4
src/AtpClientServiceProvider.php
··· 25 25 use SocialDept\AtpClient\Http\DPoPClient; 26 26 use SocialDept\AtpClient\Session\SessionManager; 27 27 use SocialDept\AtpClient\Storage\EncryptedFileKeyStore; 28 - use SocialDept\AtpClient\Client\Public\AtpPublicClient; 29 28 30 29 class AtpClientServiceProvider extends ServiceProvider 31 30 { ··· 120 119 $this->app->instance(CredentialProvider::class, $provider); 121 120 } 122 121 123 - public function public(?string $service = null): AtpPublicClient 122 + public function public(?string $service = null): AtpClient 124 123 { 125 - return new AtpPublicClient( 126 - $service ?? config('atp-client.public.service_url', 'https://public.api.bsky.app') 124 + return new AtpClient( 125 + sessions: null, 126 + did: null, 127 + serviceUrl: $service ?? config('atp-client.public.service_url', 'https://public.api.bsky.app') 127 128 ); 128 129 } 129 130 };
+43
src/Attributes/PublicEndpoint.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Attributes; 4 + 5 + use Attribute; 6 + 7 + /** 8 + * Documents that a method is a public endpoint that does not require authentication. 9 + * 10 + * This attribute currently serves as documentation to indicate which AT Protocol 11 + * endpoints can be called without an authenticated session. It helps developers 12 + * understand which endpoints work with `Atp::public()` against public API endpoints 13 + * like `https://public.api.bsky.app`. 14 + * 15 + * While this attribute does not currently perform runtime enforcement, scope 16 + * validation will be implemented in a future release. Correctly attributing 17 + * endpoints now ensures forward compatibility when enforcement is enabled. 18 + * 19 + * Public endpoints typically include operations like: 20 + * - Reading public profiles and posts 21 + * - Searching actors and content 22 + * - Resolving handles to DIDs 23 + * - Accessing repository data (sync endpoints) 24 + * - Describing servers and feed generators 25 + * 26 + * @example Basic usage 27 + * ```php 28 + * #[PublicEndpoint] 29 + * public function getProfile(string $actor): ProfileViewDetailed 30 + * ``` 31 + * 32 + * @see \SocialDept\AtpClient\Attributes\ScopedEndpoint For endpoints that require authentication 33 + */ 34 + #[Attribute(Attribute::TARGET_METHOD)] 35 + class PublicEndpoint 36 + { 37 + /** 38 + * @param string $description Human-readable description of the endpoint 39 + */ 40 + public function __construct( 41 + public readonly string $description = '', 42 + ) {} 43 + }
-37
src/Attributes/RequiresScope.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Attributes; 4 - 5 - use Attribute; 6 - use SocialDept\AtpClient\Enums\Scope; 7 - 8 - #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] 9 - class RequiresScope 10 - { 11 - public array $scopes; 12 - 13 - /** 14 - * @param string|Scope|array<string|Scope> $scopes Required scope(s) for this method 15 - * @param string|null $granular Future granular scope equivalent 16 - * @param string $description Human-readable description of scope requirement 17 - */ 18 - public function __construct( 19 - string|Scope|array $scopes, 20 - public readonly ?string $granular = null, 21 - public readonly string $description = '', 22 - ) { 23 - $this->scopes = $this->normalizeScopes($scopes); 24 - } 25 - 26 - protected function normalizeScopes(string|Scope|array $scopes): array 27 - { 28 - if (! is_array($scopes)) { 29 - $scopes = [$scopes]; 30 - } 31 - 32 - return array_map( 33 - fn ($scope) => $scope instanceof Scope ? $scope->value : $scope, 34 - $scopes 35 - ); 36 - } 37 - }
+67
src/Attributes/ScopedEndpoint.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Attributes; 4 + 5 + use Attribute; 6 + use SocialDept\AtpClient\Enums\Scope; 7 + 8 + /** 9 + * Documents that a method requires authentication with specific OAuth scopes. 10 + * 11 + * This attribute currently serves as documentation to indicate which AT Protocol 12 + * endpoints require authentication and what scopes they need. It helps developers 13 + * understand scope requirements when building applications. 14 + * 15 + * While this attribute does not currently perform runtime enforcement, scope 16 + * validation will be implemented in a future release. Correctly attributing 17 + * endpoints now ensures forward compatibility when enforcement is enabled. 18 + * 19 + * The AT Protocol currently uses "transition scopes" (like `transition:generic`) while 20 + * moving toward more granular scopes. The `granular` parameter allows documenting the 21 + * future granular scope that will replace the transition scope. 22 + * 23 + * @example Basic usage with a transition scope 24 + * ```php 25 + * #[ScopedEndpoint(Scope::TransitionGeneric)] 26 + * public function getTimeline(): GetTimelineResponse 27 + * ``` 28 + * 29 + * @example With future granular scope documented 30 + * ```php 31 + * #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getTimeline')] 32 + * public function getTimeline(): GetTimelineResponse 33 + * ``` 34 + * 35 + * @see \SocialDept\AtpClient\Attributes\PublicEndpoint For endpoints that don't require authentication 36 + * @see \SocialDept\AtpClient\Enums\Scope For available scope values 37 + */ 38 + #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] 39 + class ScopedEndpoint 40 + { 41 + public array $scopes; 42 + 43 + /** 44 + * @param string|Scope|array<string|Scope> $scopes Required scope(s) for this method 45 + * @param string|null $granular Future granular scope equivalent 46 + * @param string $description Human-readable description of scope requirement 47 + */ 48 + public function __construct( 49 + string|Scope|array $scopes, 50 + public readonly ?string $granular = null, 51 + public readonly string $description = '', 52 + ) { 53 + $this->scopes = $this->normalizeScopes($scopes); 54 + } 55 + 56 + protected function normalizeScopes(string|Scope|array $scopes): array 57 + { 58 + if (! is_array($scopes)) { 59 + $scopes = [$scopes]; 60 + } 61 + 62 + return array_map( 63 + fn ($scope) => $scope instanceof Scope ? $scope->value : $scope, 64 + $scopes 65 + ); 66 + } 67 + }
+13
src/Client/BskyClient.php
··· 13 13 class BskyClient 14 14 { 15 15 use HasDomainExtensions; 16 + 16 17 /** 17 18 * The parent AtpClient instance 18 19 */ ··· 29 30 public Bsky\ActorRequestClient $actor; 30 31 31 32 /** 33 + * Graph operations (app.bsky.graph.*) 34 + */ 35 + public Bsky\GraphRequestClient $graph; 36 + 37 + /** 38 + * Labeler operations (app.bsky.labeler.*) 39 + */ 40 + public Bsky\LabelerRequestClient $labeler; 41 + 42 + /** 32 43 * Post record client 33 44 */ 34 45 public PostRecordClient $post; ··· 53 64 $this->atp = $parent; 54 65 $this->feed = new Bsky\FeedRequestClient($this); 55 66 $this->actor = new Bsky\ActorRequestClient($this); 67 + $this->graph = new Bsky\GraphRequestClient($this); 68 + $this->labeler = new Bsky\LabelerRequestClient($this); 56 69 $this->post = new PostRecordClient($this); 57 70 $this->profile = new ProfileRecordClient($this); 58 71 $this->like = new LikeRecordClient($this);
+93 -4
src/Client/Client.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client; 4 4 5 + use BackedEnum; 6 + use Illuminate\Support\Facades\Http; 5 7 use SocialDept\AtpClient\AtpClient; 8 + use SocialDept\AtpClient\Exceptions\AtpResponseException; 6 9 use SocialDept\AtpClient\Http\DPoPClient; 7 10 use SocialDept\AtpClient\Http\HasHttp; 11 + use SocialDept\AtpClient\Http\Response; 8 12 use SocialDept\AtpClient\Session\Session; 9 13 use SocialDept\AtpClient\Session\SessionManager; 10 14 11 15 class Client 12 16 { 13 - use HasHttp; 17 + use HasHttp { 18 + call as authenticatedCall; 19 + postBlob as authenticatedPostBlob; 20 + } 14 21 15 22 /** 16 23 * The parent AtpClient instance we belong to 17 24 */ 18 25 protected AtpClient $atp; 19 26 27 + /** 28 + * Service URL for public mode 29 + */ 30 + protected ?string $serviceUrl; 31 + 20 32 public function __construct( 21 33 AtpClient $parent, 22 - SessionManager $sessions, 23 - string $did, 34 + ?SessionManager $sessions = null, 35 + ?string $did = null, 36 + ?string $serviceUrl = null, 24 37 ) { 25 38 $this->atp = $parent; 26 39 $this->sessions = $sessions; 27 40 $this->did = $did; 28 - $this->dpopClient = app(DPoPClient::class); 41 + $this->serviceUrl = $serviceUrl; 42 + 43 + if (! $this->isPublicMode()) { 44 + $this->dpopClient = app(DPoPClient::class); 45 + } 46 + } 47 + 48 + /** 49 + * Check if client is in public mode (no authentication). 50 + */ 51 + public function isPublicMode(): bool 52 + { 53 + return $this->sessions === null || $this->did === null; 29 54 } 30 55 31 56 /** ··· 34 59 public function session(): Session 35 60 { 36 61 return $this->sessions->session($this->did); 62 + } 63 + 64 + /** 65 + * Get the service URL. 66 + */ 67 + public function serviceUrl(): string 68 + { 69 + return $this->serviceUrl; 70 + } 71 + 72 + /** 73 + * Make XRPC call - routes to public or authenticated based on mode. 74 + */ 75 + protected function call( 76 + string|BackedEnum $endpoint, 77 + string $method, 78 + ?array $params = null, 79 + ?array $body = null 80 + ): Response { 81 + if ($this->isPublicMode()) { 82 + return $this->publicCall($endpoint, $method, $params, $body); 83 + } 84 + 85 + return $this->authenticatedCall($endpoint, $method, $params, $body); 86 + } 87 + 88 + /** 89 + * Make public XRPC call (no authentication). 90 + */ 91 + protected function publicCall( 92 + string|BackedEnum $endpoint, 93 + string $method, 94 + ?array $params = null, 95 + ?array $body = null 96 + ): Response { 97 + $endpoint = $endpoint instanceof BackedEnum ? $endpoint->value : $endpoint; 98 + $url = rtrim($this->serviceUrl, '/') . '/xrpc/' . $endpoint; 99 + $params = array_filter($params ?? [], fn ($v) => ! is_null($v)); 100 + 101 + $response = match ($method) { 102 + 'GET' => Http::get($url, $params), 103 + 'POST' => Http::post($url, $body ?? $params), 104 + 'DELETE' => Http::delete($url, $params), 105 + default => throw new \InvalidArgumentException("Unsupported method: {$method}"), 106 + }; 107 + 108 + if ($response->failed() || isset($response->json()['error'])) { 109 + throw AtpResponseException::fromResponse($response, $endpoint); 110 + } 111 + 112 + return new Response($response); 113 + } 114 + 115 + /** 116 + * Make POST request with raw binary body (for blob uploads). 117 + * Only works in authenticated mode. 118 + */ 119 + public function postBlob(string|BackedEnum $endpoint, string $data, string $mimeType): Response 120 + { 121 + if ($this->isPublicMode()) { 122 + throw new \RuntimeException('Blob uploads require authentication.'); 123 + } 124 + 125 + return $this->authenticatedPostBlob($endpoint, $data, $mimeType); 37 126 } 38 127 }
-22
src/Client/Public/AtpPublicClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public; 4 - 5 - use SocialDept\AtpClient\Concerns\HasExtensions; 6 - 7 - class AtpPublicClient 8 - { 9 - use HasExtensions; 10 - public PublicClient $client; 11 - public BskyPublicClient $bsky; 12 - public AtprotoPublicClient $atproto; 13 - 14 - public function __construct(string $serviceUrl = null) 15 - { 16 - $serviceUrl = $serviceUrl ?? config('client.public.service_url'); 17 - 18 - $this->client = new PublicClient($serviceUrl); 19 - $this->bsky = new BskyPublicClient($this); 20 - $this->atproto = new AtprotoPublicClient($this); 21 - } 22 - }
-38
src/Client/Public/AtprotoPublicClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\Atproto\IdentityPublicRequestClient; 6 - use SocialDept\AtpClient\Client\Public\Requests\Atproto\RepoPublicRequestClient; 7 - use SocialDept\AtpClient\Concerns\HasDomainExtensions; 8 - 9 - class AtprotoPublicClient 10 - { 11 - use HasDomainExtensions; 12 - 13 - protected AtpPublicClient $atp; 14 - public IdentityPublicRequestClient $identity; 15 - public RepoPublicRequestClient $repo; 16 - 17 - public function __construct(AtpPublicClient $parent) 18 - { 19 - $this->atp = $parent; 20 - $this->identity = new IdentityPublicRequestClient($this); 21 - $this->repo = new RepoPublicRequestClient($this); 22 - } 23 - 24 - protected function getDomainName(): string 25 - { 26 - return 'atproto'; 27 - } 28 - 29 - protected function getRootClientClass(): string 30 - { 31 - return AtpPublicClient::class; 32 - } 33 - 34 - public function root(): AtpPublicClient 35 - { 36 - return $this->atp; 37 - } 38 - }
-44
src/Client/Public/BskyPublicClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\Bsky\ActorPublicRequestClient; 6 - use SocialDept\AtpClient\Client\Public\Requests\Bsky\FeedPublicRequestClient; 7 - use SocialDept\AtpClient\Client\Public\Requests\Bsky\GraphPublicRequestClient; 8 - use SocialDept\AtpClient\Client\Public\Requests\Bsky\LabelerPublicRequestClient; 9 - use SocialDept\AtpClient\Concerns\HasDomainExtensions; 10 - 11 - class BskyPublicClient 12 - { 13 - use HasDomainExtensions; 14 - 15 - protected AtpPublicClient $atp; 16 - public ActorPublicRequestClient $actor; 17 - public FeedPublicRequestClient $feed; 18 - public GraphPublicRequestClient $graph; 19 - public LabelerPublicRequestClient $labeler; 20 - 21 - public function __construct(AtpPublicClient $parent) 22 - { 23 - $this->atp = $parent; 24 - $this->actor = new ActorPublicRequestClient($this); 25 - $this->feed = new FeedPublicRequestClient($this); 26 - $this->graph = new GraphPublicRequestClient($this); 27 - $this->labeler = new LabelerPublicRequestClient($this); 28 - } 29 - 30 - protected function getDomainName(): string 31 - { 32 - return 'bsky'; 33 - } 34 - 35 - protected function getRootClientClass(): string 36 - { 37 - return AtpPublicClient::class; 38 - } 39 - 40 - public function root(): AtpPublicClient 41 - { 42 - return $this->atp; 43 - } 44 - }
-35
src/Client/Public/PublicClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public; 4 - 5 - use BackedEnum; 6 - use Illuminate\Support\Facades\Http; 7 - use SocialDept\AtpClient\Exceptions\AtpResponseException; 8 - use SocialDept\AtpClient\Http\Response; 9 - 10 - class PublicClient 11 - { 12 - public function __construct( 13 - protected string $serviceUrl 14 - ) {} 15 - 16 - public function get(string|BackedEnum $endpoint, array $params = []): Response 17 - { 18 - $endpoint = $endpoint instanceof BackedEnum ? $endpoint->value : $endpoint; 19 - $url = rtrim($this->serviceUrl, '/') . '/xrpc/' . $endpoint; 20 - $params = array_filter($params, fn ($v) => !is_null($v)); 21 - 22 - $response = Http::get($url, $params); 23 - 24 - if ($response->failed() || isset($response->json()['error'])) { 25 - throw AtpResponseException::fromResponse($response, $endpoint); 26 - } 27 - 28 - return new Response($response); 29 - } 30 - 31 - public function serviceUrl(): string 32 - { 33 - return $this->serviceUrl; 34 - } 35 - }
-19
src/Client/Public/Requests/Atproto/IdentityPublicRequestClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Atproto; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 6 - use SocialDept\AtpClient\Enums\Nsid\AtprotoIdentity; 7 - 8 - class IdentityPublicRequestClient extends PublicRequest 9 - { 10 - public function resolveHandle(string $handle): string 11 - { 12 - $response = $this->atp->client->get( 13 - endpoint: AtprotoIdentity::ResolveHandle, 14 - params: compact('handle') 15 - ); 16 - 17 - return $response->json()['did']; 18 - } 19 - }
-69
src/Client/Public/Requests/Atproto/RepoPublicRequestClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Atproto; 4 - 5 - use BackedEnum; 6 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 7 - use SocialDept\AtpClient\Data\Responses\Atproto\Repo\DescribeRepoResponse; 8 - use SocialDept\AtpClient\Data\Responses\Atproto\Repo\GetRecordResponse; 9 - use SocialDept\AtpClient\Data\Responses\Atproto\Repo\ListRecordsResponse; 10 - use SocialDept\AtpClient\Enums\Nsid\AtprotoRepo; 11 - 12 - class RepoPublicRequestClient extends PublicRequest 13 - { 14 - /** 15 - * Get a record 16 - * 17 - * @see https://docs.bsky.app/docs/api/com-atproto-repo-get-record 18 - */ 19 - public function getRecord( 20 - string $repo, 21 - string|BackedEnum $collection, 22 - string $rkey, 23 - ?string $cid = null 24 - ): GetRecordResponse { 25 - $collection = $collection instanceof BackedEnum ? $collection->value : $collection; 26 - $response = $this->atp->client->get( 27 - endpoint: AtprotoRepo::GetRecord, 28 - params: compact('repo', 'collection', 'rkey', 'cid') 29 - ); 30 - 31 - return GetRecordResponse::fromArray($response->json()); 32 - } 33 - 34 - /** 35 - * List records in a collection 36 - * 37 - * @see https://docs.bsky.app/docs/api/com-atproto-repo-list-records 38 - */ 39 - public function listRecords( 40 - string $repo, 41 - string|BackedEnum $collection, 42 - int $limit = 50, 43 - ?string $cursor = null, 44 - bool $reverse = false 45 - ): ListRecordsResponse { 46 - $collection = $collection instanceof BackedEnum ? $collection->value : $collection; 47 - $response = $this->atp->client->get( 48 - endpoint: AtprotoRepo::ListRecords, 49 - params: compact('repo', 'collection', 'limit', 'cursor', 'reverse') 50 - ); 51 - 52 - return ListRecordsResponse::fromArray($response->json()); 53 - } 54 - 55 - /** 56 - * Describe the repository 57 - * 58 - * @see https://docs.bsky.app/docs/api/com-atproto-repo-describe-repo 59 - */ 60 - public function describeRepo(string $repo): DescribeRepoResponse 61 - { 62 - $response = $this->atp->client->get( 63 - endpoint: AtprotoRepo::DescribeRepo, 64 - params: compact('repo') 65 - ); 66 - 67 - return DescribeRepoResponse::fromArray($response->json()); 68 - } 69 - }
-64
src/Client/Public/Requests/Bsky/ActorPublicRequestClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Bsky; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 6 - use SocialDept\AtpClient\Enums\Nsid\BskyActor; 7 - use SocialDept\AtpClient\Data\Responses\Bsky\Actor\GetProfilesResponse; 8 - use SocialDept\AtpClient\Data\Responses\Bsky\Actor\GetSuggestionsResponse; 9 - use SocialDept\AtpClient\Data\Responses\Bsky\Actor\SearchActorsResponse; 10 - use SocialDept\AtpClient\Data\Responses\Bsky\Actor\SearchActorsTypeaheadResponse; 11 - use SocialDept\AtpSchema\Generated\App\Bsky\Actor\Defs\ProfileViewDetailed; 12 - 13 - class ActorPublicRequestClient extends PublicRequest 14 - { 15 - public function getProfile(string $actor): ProfileViewDetailed 16 - { 17 - $response = $this->atp->client->get( 18 - endpoint: BskyActor::GetProfile, 19 - params: compact('actor') 20 - ); 21 - 22 - return ProfileViewDetailed::fromArray($response->json()); 23 - } 24 - 25 - public function getProfiles(array $actors): GetProfilesResponse 26 - { 27 - $response = $this->atp->client->get( 28 - endpoint: BskyActor::GetProfiles, 29 - params: compact('actors') 30 - ); 31 - 32 - return GetProfilesResponse::fromArray($response->json()); 33 - } 34 - 35 - public function getSuggestions(int $limit = 50, ?string $cursor = null): GetSuggestionsResponse 36 - { 37 - $response = $this->atp->client->get( 38 - endpoint: BskyActor::GetSuggestions, 39 - params: compact('limit', 'cursor') 40 - ); 41 - 42 - return GetSuggestionsResponse::fromArray($response->json()); 43 - } 44 - 45 - public function searchActors(string $q, int $limit = 25, ?string $cursor = null): SearchActorsResponse 46 - { 47 - $response = $this->atp->client->get( 48 - endpoint: BskyActor::SearchActors, 49 - params: compact('q', 'limit', 'cursor') 50 - ); 51 - 52 - return SearchActorsResponse::fromArray($response->json()); 53 - } 54 - 55 - public function searchActorsTypeahead(string $q, int $limit = 10): SearchActorsTypeaheadResponse 56 - { 57 - $response = $this->atp->client->get( 58 - endpoint: BskyActor::SearchActorsTypeahead, 59 - params: compact('q', 'limit') 60 - ); 61 - 62 - return SearchActorsTypeaheadResponse::fromArray($response->json()); 63 - } 64 - }
-162
src/Client/Public/Requests/Bsky/FeedPublicRequestClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Bsky; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 6 - use SocialDept\AtpClient\Enums\Nsid\BskyFeed; 7 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\DescribeFeedGeneratorResponse; 8 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetActorFeedsResponse; 9 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetActorLikesResponse; 10 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetAuthorFeedResponse; 11 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedGeneratorResponse; 12 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedGeneratorsResponse; 13 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedResponse; 14 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetLikesResponse; 15 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetPostsResponse; 16 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetPostThreadResponse; 17 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetQuotesResponse; 18 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetRepostedByResponse; 19 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetSuggestedFeedsResponse; 20 - use SocialDept\AtpClient\Data\Responses\Bsky\Feed\SearchPostsResponse; 21 - 22 - class FeedPublicRequestClient extends PublicRequest 23 - { 24 - public function describeFeedGenerator(): DescribeFeedGeneratorResponse 25 - { 26 - $response = $this->atp->client->get( 27 - endpoint: BskyFeed::DescribeFeedGenerator 28 - ); 29 - 30 - return DescribeFeedGeneratorResponse::fromArray($response->json()); 31 - } 32 - 33 - public function getAuthorFeed(string $actor, int $limit = 50, ?string $cursor = null, ?string $filter = null): GetAuthorFeedResponse 34 - { 35 - $response = $this->atp->client->get( 36 - endpoint: BskyFeed::GetAuthorFeed, 37 - params: compact('actor', 'limit', 'cursor', 'filter') 38 - ); 39 - 40 - return GetAuthorFeedResponse::fromArray($response->json()); 41 - } 42 - 43 - public function getActorFeeds(string $actor, int $limit = 50, ?string $cursor = null): GetActorFeedsResponse 44 - { 45 - $response = $this->atp->client->get( 46 - endpoint: BskyFeed::GetActorFeeds, 47 - params: compact('actor', 'limit', 'cursor') 48 - ); 49 - 50 - return GetActorFeedsResponse::fromArray($response->json()); 51 - } 52 - 53 - public function getActorLikes(string $actor, int $limit = 50, ?string $cursor = null): GetActorLikesResponse 54 - { 55 - $response = $this->atp->client->get( 56 - endpoint: BskyFeed::GetActorLikes, 57 - params: compact('actor', 'limit', 'cursor') 58 - ); 59 - 60 - return GetActorLikesResponse::fromArray($response->json()); 61 - } 62 - 63 - public function getFeed(string $feed, int $limit = 50, ?string $cursor = null): GetFeedResponse 64 - { 65 - $response = $this->atp->client->get( 66 - endpoint: BskyFeed::GetFeed, 67 - params: compact('feed', 'limit', 'cursor') 68 - ); 69 - 70 - return GetFeedResponse::fromArray($response->json()); 71 - } 72 - 73 - public function getFeedGenerator(string $feed): GetFeedGeneratorResponse 74 - { 75 - $response = $this->atp->client->get( 76 - endpoint: BskyFeed::GetFeedGenerator, 77 - params: compact('feed') 78 - ); 79 - 80 - return GetFeedGeneratorResponse::fromArray($response->json()); 81 - } 82 - 83 - public function getFeedGenerators(array $feeds): GetFeedGeneratorsResponse 84 - { 85 - $response = $this->atp->client->get( 86 - endpoint: BskyFeed::GetFeedGenerators, 87 - params: compact('feeds') 88 - ); 89 - 90 - return GetFeedGeneratorsResponse::fromArray($response->json()); 91 - } 92 - 93 - public function getLikes(string $uri, int $limit = 50, ?string $cursor = null, ?string $cid = null): GetLikesResponse 94 - { 95 - $response = $this->atp->client->get( 96 - endpoint: BskyFeed::GetLikes, 97 - params: compact('uri', 'limit', 'cursor', 'cid') 98 - ); 99 - 100 - return GetLikesResponse::fromArray($response->json()); 101 - } 102 - 103 - public function getPostThread(string $uri, int $depth = 6, int $parentHeight = 80): GetPostThreadResponse 104 - { 105 - $response = $this->atp->client->get( 106 - endpoint: BskyFeed::GetPostThread, 107 - params: compact('uri', 'depth', 'parentHeight') 108 - ); 109 - 110 - return GetPostThreadResponse::fromArray($response->json()); 111 - } 112 - 113 - public function getPosts(array $uris): GetPostsResponse 114 - { 115 - $response = $this->atp->client->get( 116 - endpoint: BskyFeed::GetPosts, 117 - params: compact('uris') 118 - ); 119 - 120 - return GetPostsResponse::fromArray($response->json()); 121 - } 122 - 123 - public function getQuotes(string $uri, int $limit = 50, ?string $cursor = null, ?string $cid = null): GetQuotesResponse 124 - { 125 - $response = $this->atp->client->get( 126 - endpoint: BskyFeed::GetQuotes, 127 - params: compact('uri', 'limit', 'cursor', 'cid') 128 - ); 129 - 130 - return GetQuotesResponse::fromArray($response->json()); 131 - } 132 - 133 - public function getRepostedBy(string $uri, int $limit = 50, ?string $cursor = null, ?string $cid = null): GetRepostedByResponse 134 - { 135 - $response = $this->atp->client->get( 136 - endpoint: BskyFeed::GetRepostedBy, 137 - params: compact('uri', 'limit', 'cursor', 'cid') 138 - ); 139 - 140 - return GetRepostedByResponse::fromArray($response->json()); 141 - } 142 - 143 - public function getSuggestedFeeds(int $limit = 50, ?string $cursor = null): GetSuggestedFeedsResponse 144 - { 145 - $response = $this->atp->client->get( 146 - endpoint: BskyFeed::GetSuggestedFeeds, 147 - params: compact('limit', 'cursor') 148 - ); 149 - 150 - return GetSuggestedFeedsResponse::fromArray($response->json()); 151 - } 152 - 153 - public function searchPosts(string $q, int $limit = 25, ?string $cursor = null, ?string $sort = null): SearchPostsResponse 154 - { 155 - $response = $this->atp->client->get( 156 - endpoint: BskyFeed::SearchPosts, 157 - params: compact('q', 'limit', 'cursor', 'sort') 158 - ); 159 - 160 - return SearchPostsResponse::fromArray($response->json()); 161 - } 162 - }
+59 -4
src/Client/Public/Requests/Bsky/GraphPublicRequestClient.php src/Client/Requests/Bsky/GraphRequestClient.php
··· 1 1 <?php 2 2 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Bsky; 3 + namespace SocialDept\AtpClient\Client\Requests\Bsky; 4 4 5 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 6 - use SocialDept\AtpClient\Enums\Nsid\BskyGraph; 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 + use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetFollowersResponse; 8 8 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetFollowsResponse; 9 9 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetKnownFollowersResponse; ··· 12 12 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetRelationshipsResponse; 13 13 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetStarterPacksResponse; 14 14 use SocialDept\AtpClient\Data\Responses\Bsky\Graph\GetSuggestedFollowsByActorResponse; 15 + use SocialDept\AtpClient\Enums\Nsid\BskyGraph; 15 16 use SocialDept\AtpSchema\Generated\App\Bsky\Graph\Defs\StarterPackView; 16 17 17 - class GraphPublicRequestClient extends PublicRequest 18 + class GraphRequestClient extends Request 18 19 { 20 + /** 21 + * Get followers of an actor 22 + * 23 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-followers 24 + */ 25 + #[PublicEndpoint] 19 26 public function getFollowers(string $actor, int $limit = 50, ?string $cursor = null): GetFollowersResponse 20 27 { 21 28 $response = $this->atp->client->get( ··· 26 33 return GetFollowersResponse::fromArray($response->json()); 27 34 } 28 35 36 + /** 37 + * Get accounts that an actor follows 38 + * 39 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-follows 40 + */ 41 + #[PublicEndpoint] 29 42 public function getFollows(string $actor, int $limit = 50, ?string $cursor = null): GetFollowsResponse 30 43 { 31 44 $response = $this->atp->client->get( ··· 36 49 return GetFollowsResponse::fromArray($response->json()); 37 50 } 38 51 52 + /** 53 + * Get followers of an actor that you also follow 54 + * 55 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-known-followers 56 + */ 57 + #[PublicEndpoint] 39 58 public function getKnownFollowers(string $actor, int $limit = 50, ?string $cursor = null): GetKnownFollowersResponse 40 59 { 41 60 $response = $this->atp->client->get( ··· 46 65 return GetKnownFollowersResponse::fromArray($response->json()); 47 66 } 48 67 68 + /** 69 + * Get a list by URI 70 + * 71 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-list 72 + */ 73 + #[PublicEndpoint] 49 74 public function getList(string $list, int $limit = 50, ?string $cursor = null): GetListResponse 50 75 { 51 76 $response = $this->atp->client->get( ··· 56 81 return GetListResponse::fromArray($response->json()); 57 82 } 58 83 84 + /** 85 + * Get lists created by an actor 86 + * 87 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-lists 88 + */ 89 + #[PublicEndpoint] 59 90 public function getLists(string $actor, int $limit = 50, ?string $cursor = null): GetListsResponse 60 91 { 61 92 $response = $this->atp->client->get( ··· 66 97 return GetListsResponse::fromArray($response->json()); 67 98 } 68 99 100 + /** 101 + * Get relationships between actors 102 + * 103 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-relationships 104 + */ 105 + #[PublicEndpoint] 69 106 public function getRelationships(string $actor, array $others = []): GetRelationshipsResponse 70 107 { 71 108 $response = $this->atp->client->get( ··· 76 113 return GetRelationshipsResponse::fromArray($response->json()); 77 114 } 78 115 116 + /** 117 + * Get a starter pack by URI 118 + * 119 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-starter-pack 120 + */ 121 + #[PublicEndpoint] 79 122 public function getStarterPack(string $starterPack): StarterPackView 80 123 { 81 124 $response = $this->atp->client->get( ··· 86 129 return StarterPackView::fromArray($response->json()['starterPack']); 87 130 } 88 131 132 + /** 133 + * Get multiple starter packs 134 + * 135 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-starter-packs 136 + */ 137 + #[PublicEndpoint] 89 138 public function getStarterPacks(array $uris): GetStarterPacksResponse 90 139 { 91 140 $response = $this->atp->client->get( ··· 96 145 return GetStarterPacksResponse::fromArray($response->json()); 97 146 } 98 147 148 + /** 149 + * Get suggested follows based on an actor 150 + * 151 + * @see https://docs.bsky.app/docs/api/app-bsky-graph-get-suggested-follows-by-actor 152 + */ 153 + #[PublicEndpoint] 99 154 public function getSuggestedFollowsByActor(string $actor): GetSuggestedFollowsByActorResponse 100 155 { 101 156 $response = $this->atp->client->get(
-20
src/Client/Public/Requests/Bsky/LabelerPublicRequestClient.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests\Bsky; 4 - 5 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 6 - use SocialDept\AtpClient\Data\Responses\Bsky\Labeler\GetServicesResponse; 7 - use SocialDept\AtpClient\Enums\Nsid\BskyLabeler; 8 - 9 - class LabelerPublicRequestClient extends PublicRequest 10 - { 11 - public function getServices(array $dids, bool $detailed = false): GetServicesResponse 12 - { 13 - $response = $this->atp->client->get( 14 - endpoint: BskyLabeler::GetServices, 15 - params: compact('dids', 'detailed') 16 - ); 17 - 18 - return GetServicesResponse::fromArray($response->json(), $detailed); 19 - } 20 - }
-15
src/Client/Public/Requests/PublicRequest.php
··· 1 - <?php 2 - 3 - namespace SocialDept\AtpClient\Client\Public\Requests; 4 - 5 - use SocialDept\AtpClient\Client\Public\AtpPublicClient; 6 - 7 - class PublicRequest 8 - { 9 - protected AtpPublicClient $atp; 10 - 11 - public function __construct($parent) 12 - { 13 - $this->atp = $parent->root(); 14 - } 15 - }
+6 -6
src/Client/Records/FollowRecordClient.php
··· 3 3 namespace SocialDept\AtpClient\Client\Records; 4 4 5 5 use DateTimeInterface; 6 - use SocialDept\AtpClient\Attributes\RequiresScope; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 7 7 use SocialDept\AtpClient\Client\Requests\Request; 8 8 use SocialDept\AtpClient\Data\StrongRef; 9 9 use SocialDept\AtpClient\Enums\Nsid\BskyGraph; ··· 16 16 * 17 17 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.graph.follow?action=create) 18 18 */ 19 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 20 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.graph.follow?action=create')] 19 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 20 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.graph.follow?action=create')] 21 21 public function create( 22 22 string $subject, 23 23 ?DateTimeInterface $createdAt = null ··· 42 42 * 43 43 * @requires transition:generic OR (rpc:com.atproto.repo.deleteRecord AND repo:app.bsky.graph.follow?action=delete) 44 44 */ 45 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 46 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.graph.follow?action=delete')] 45 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 46 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.graph.follow?action=delete')] 47 47 public function delete(string $rkey): void 48 48 { 49 49 $this->atp->atproto->repo->deleteRecord( ··· 58 58 * 59 59 * @requires transition:generic (rpc:com.atproto.repo.getRecord) 60 60 */ 61 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 61 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 62 62 public function get(string $rkey, ?string $cid = null): array 63 63 { 64 64 $response = $this->atp->atproto->repo->getRecord(
+6 -6
src/Client/Records/LikeRecordClient.php
··· 3 3 namespace SocialDept\AtpClient\Client\Records; 4 4 5 5 use DateTimeInterface; 6 - use SocialDept\AtpClient\Attributes\RequiresScope; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 7 7 use SocialDept\AtpClient\Client\Requests\Request; 8 8 use SocialDept\AtpClient\Data\StrongRef; 9 9 use SocialDept\AtpClient\Enums\Nsid\BskyFeed; ··· 16 16 * 17 17 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.like?action=create) 18 18 */ 19 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 20 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.like?action=create')] 19 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 20 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.like?action=create')] 21 21 public function create( 22 22 StrongRef $subject, 23 23 ?DateTimeInterface $createdAt = null ··· 42 42 * 43 43 * @requires transition:generic OR (rpc:com.atproto.repo.deleteRecord AND repo:app.bsky.feed.like?action=delete) 44 44 */ 45 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 46 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.like?action=delete')] 45 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 46 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.like?action=delete')] 47 47 public function delete(string $rkey): void 48 48 { 49 49 $this->atp->atproto->repo->deleteRecord( ··· 58 58 * 59 59 * @requires transition:generic (rpc:com.atproto.repo.getRecord) 60 60 */ 61 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 61 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 62 62 public function get(string $rkey, ?string $cid = null): array 63 63 { 64 64 $response = $this->atp->atproto->repo->getRecord(
+20 -19
src/Client/Records/PostRecordClient.php
··· 3 3 namespace SocialDept\AtpClient\Client\Records; 4 4 5 5 use DateTimeInterface; 6 - use SocialDept\AtpClient\Attributes\RequiresScope; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 7 7 use SocialDept\AtpClient\Client\Requests\Request; 8 8 use SocialDept\AtpClient\Contracts\Recordable; 9 9 use SocialDept\AtpClient\Data\StrongRef; 10 10 use SocialDept\AtpClient\Enums\Nsid\BskyFeed; 11 11 use SocialDept\AtpClient\Enums\Scope; 12 12 use SocialDept\AtpClient\RichText\TextBuilder; 13 + use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Defs\PostView; 13 14 14 15 class PostRecordClient extends Request 15 16 { ··· 18 19 * 19 20 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create) 20 21 */ 21 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 22 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 22 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 23 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 23 24 public function create( 24 25 string|array|Recordable $content, 25 26 ?array $facets = null, ··· 73 74 * 74 75 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.feed.post?action=update) 75 76 */ 76 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 77 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=update')] 77 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 78 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=update')] 78 79 public function update(string $rkey, array $record): StrongRef 79 80 { 80 81 // Ensure $type is set ··· 89 90 record: $record 90 91 ); 91 92 92 - return StrongRef::fromResponse($response->json()); 93 + return StrongRef::fromResponse($response->toArray()); 93 94 } 94 95 95 96 /** ··· 97 98 * 98 99 * @requires transition:generic OR (rpc:com.atproto.repo.deleteRecord AND repo:app.bsky.feed.post?action=delete) 99 100 */ 100 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 101 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=delete')] 101 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')] 102 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=delete')] 102 103 public function delete(string $rkey): void 103 104 { 104 105 $this->atp->atproto->repo->deleteRecord( ··· 113 114 * 114 115 * @requires transition:generic (rpc:com.atproto.repo.getRecord) 115 116 */ 116 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 117 - public function get(string $rkey, ?string $cid = null): array 117 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 118 + public function get(string $rkey, ?string $cid = null): PostView 118 119 { 119 120 $response = $this->atp->atproto->repo->getRecord( 120 121 repo: $this->atp->client->session()->did(), ··· 123 124 cid: $cid 124 125 ); 125 126 126 - return $response->json('value'); 127 + return PostView::fromArray($response->value); 127 128 } 128 129 129 130 /** ··· 131 132 * 132 133 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create) 133 134 */ 134 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 135 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 135 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 136 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 136 137 public function reply( 137 138 StrongRef $parent, 138 139 StrongRef $root, ··· 162 163 * 163 164 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create) 164 165 */ 165 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 166 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 166 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 167 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 167 168 public function quote( 168 169 StrongRef $quotedPost, 169 170 string|array|Recordable $content, ··· 190 191 * 191 192 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create) 192 193 */ 193 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 194 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 194 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 195 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 195 196 public function withImages( 196 197 string|array|Recordable $content, 197 198 array $images, ··· 218 219 * 219 220 * @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create) 220 221 */ 221 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 222 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 222 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')] 223 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')] 223 224 public function withLink( 224 225 string|array|Recordable $content, 225 226 string $uri,
+12 -12
src/Client/Records/ProfileRecordClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Records; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\StrongRef; 8 8 use SocialDept\AtpClient\Enums\Nsid\BskyActor; ··· 15 15 * 16 16 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.actor.profile?action=update) 17 17 */ 18 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 19 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 18 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 19 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 20 20 public function update(array $profile): StrongRef 21 21 { 22 22 // Ensure $type is set ··· 39 39 * 40 40 * @requires transition:generic (rpc:com.atproto.repo.getRecord) 41 41 */ 42 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 42 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 43 43 public function get(): array 44 44 { 45 45 $response = $this->atp->atproto->repo->getRecord( ··· 56 56 * 57 57 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.actor.profile?action=update) 58 58 */ 59 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 60 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 59 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 60 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 61 61 public function updateDisplayName(string $displayName): StrongRef 62 62 { 63 63 $profile = $this->getOrCreateProfile(); ··· 71 71 * 72 72 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.actor.profile?action=update) 73 73 */ 74 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 75 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 74 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 75 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 76 76 public function updateDescription(string $description): StrongRef 77 77 { 78 78 $profile = $this->getOrCreateProfile(); ··· 86 86 * 87 87 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.actor.profile?action=update) 88 88 */ 89 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 90 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 89 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 90 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 91 91 public function updateAvatar(array $avatarBlob): StrongRef 92 92 { 93 93 $profile = $this->getOrCreateProfile(); ··· 101 101 * 102 102 * @requires transition:generic OR (rpc:com.atproto.repo.putRecord AND repo:app.bsky.actor.profile?action=update) 103 103 */ 104 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 105 - #[RequiresScope(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 104 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')] 105 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')] 106 106 public function updateBanner(array $bannerBlob): StrongRef 107 107 { 108 108 $profile = $this->getOrCreateProfile();
+4 -5
src/Client/Requests/Atproto/IdentityRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Atproto; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 7 use SocialDept\AtpClient\Client\Requests\Request; 7 8 use SocialDept\AtpClient\Enums\Nsid\AtprotoIdentity; 8 9 use SocialDept\AtpClient\Enums\Scope; ··· 12 13 /** 13 14 * Resolve handle to DID 14 15 * 15 - * @requires transition:generic (rpc:com.atproto.identity.resolveHandle) 16 - * 17 16 * @see https://docs.bsky.app/docs/api/com-atproto-identity-resolve-handle 18 17 */ 19 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.identity.resolveHandle')] 18 + #[PublicEndpoint] 20 19 public function resolveHandle(string $handle): string 21 20 { 22 21 $response = $this->atp->client->get( ··· 34 33 * 35 34 * @see https://docs.bsky.app/docs/api/com-atproto-identity-update-handle 36 35 */ 37 - #[RequiresScope(Scope::Atproto, granular: 'identity:handle')] 36 + #[ScopedEndpoint(Scope::Atproto, granular: 'identity:handle')] 38 37 public function updateHandle(string $handle): void 39 38 { 40 39 $this->atp->client->post(
+9 -14
src/Client/Requests/Atproto/RepoRequestClient.php
··· 5 5 use BackedEnum; 6 6 use Illuminate\Http\UploadedFile; 7 7 use InvalidArgumentException; 8 - use SocialDept\AtpClient\Attributes\RequiresScope; 8 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 9 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 9 10 use SocialDept\AtpClient\Auth\ScopeChecker; 10 11 use SocialDept\AtpClient\Client\Requests\Request; 11 12 use SocialDept\AtpClient\Data\Responses\Atproto\Repo\CreateRecordResponse; ··· 29 30 * 30 31 * @see https://docs.bsky.app/docs/api/com-atproto-repo-create-record 31 32 */ 32 - #[RequiresScope(Scope::TransitionGeneric, description: 'Create records in repository')] 33 + #[ScopedEndpoint(Scope::TransitionGeneric, description: 'Create records in repository')] 33 34 public function createRecord( 34 35 string $repo, 35 36 string|BackedEnum $collection, ··· 59 60 * 60 61 * @see https://docs.bsky.app/docs/api/com-atproto-repo-delete-record 61 62 */ 62 - #[RequiresScope(Scope::TransitionGeneric, description: 'Delete records from repository')] 63 + #[ScopedEndpoint(Scope::TransitionGeneric, description: 'Delete records from repository')] 63 64 public function deleteRecord( 64 65 string $repo, 65 66 string|BackedEnum $collection, ··· 88 89 * 89 90 * @see https://docs.bsky.app/docs/api/com-atproto-repo-put-record 90 91 */ 91 - #[RequiresScope(Scope::TransitionGeneric, description: 'Update records in repository')] 92 + #[ScopedEndpoint(Scope::TransitionGeneric, description: 'Update records in repository')] 92 93 public function putRecord( 93 94 string $repo, 94 95 string|BackedEnum $collection, ··· 115 116 /** 116 117 * Get a record 117 118 * 118 - * @requires transition:generic (rpc:com.atproto.repo.getRecord) 119 - * 120 119 * @see https://docs.bsky.app/docs/api/com-atproto-repo-get-record 121 120 */ 122 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')] 121 + #[PublicEndpoint] 123 122 public function getRecord( 124 123 string $repo, 125 124 string|BackedEnum $collection, ··· 138 137 /** 139 138 * List records in a collection 140 139 * 141 - * @requires transition:generic (rpc:com.atproto.repo.listRecords) 142 - * 143 140 * @see https://docs.bsky.app/docs/api/com-atproto-repo-list-records 144 141 */ 145 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.listRecords')] 142 + #[PublicEndpoint] 146 143 public function listRecords( 147 144 string $repo, 148 145 string|BackedEnum $collection, ··· 173 170 * 174 171 * @see https://docs.bsky.app/docs/api/com-atproto-repo-upload-blob 175 172 */ 176 - #[RequiresScope(Scope::TransitionGeneric, granular: 'blob:*/*')] 173 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'blob:*/*')] 177 174 public function uploadBlob(UploadedFile|SplFileInfo|string $file, ?string $mimeType = null): BlobReference 178 175 { 179 176 // Handle different input types ··· 200 197 /** 201 198 * Describe the repository 202 199 * 203 - * @requires transition:generic (rpc:com.atproto.repo.describeRepo) 204 - * 205 200 * @see https://docs.bsky.app/docs/api/com-atproto-repo-describe-repo 206 201 */ 207 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.describeRepo')] 202 + #[PublicEndpoint] 208 203 public function describeRepo(string $repo): DescribeRepoResponse 209 204 { 210 205 $response = $this->atp->client->get(
+4 -5
src/Client/Requests/Atproto/ServerRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Atproto; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 7 use SocialDept\AtpClient\Client\Requests\Request; 7 8 use SocialDept\AtpClient\Data\Responses\Atproto\Server\DescribeServerResponse; 8 9 use SocialDept\AtpClient\Data\Responses\Atproto\Server\GetSessionResponse; ··· 18 19 * 19 20 * @see https://docs.bsky.app/docs/api/com-atproto-server-get-session 20 21 */ 21 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.server.getSession')] 22 + #[ScopedEndpoint(Scope::Atproto, granular: 'rpc:com.atproto.server.getSession')] 22 23 public function getSession(): GetSessionResponse 23 24 { 24 25 $response = $this->atp->client->get( ··· 31 32 /** 32 33 * Describe server 33 34 * 34 - * @requires atproto (rpc:com.atproto.server.describeServer) 35 - * 36 35 * @see https://docs.bsky.app/docs/api/com-atproto-server-describe-server 37 36 */ 38 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.server.describeServer')] 37 + #[PublicEndpoint] 39 38 public function describeServer(): DescribeServerResponse 40 39 { 41 40 $response = $this->atp->client->get(
+31 -26
src/Client/Requests/Atproto/SyncRequestClient.php
··· 3 3 namespace SocialDept\AtpClient\Client\Requests\Atproto; 4 4 5 5 use BackedEnum; 6 - use SocialDept\AtpClient\Attributes\RequiresScope; 6 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 7 7 use SocialDept\AtpClient\Client\Requests\Request; 8 8 use SocialDept\AtpClient\Data\Responses\Atproto\Sync\GetRepoStatusResponse; 9 9 use SocialDept\AtpClient\Data\Responses\Atproto\Sync\ListBlobsResponse; 10 + use SocialDept\AtpClient\Data\Responses\Atproto\Sync\ListReposByCollectionResponse; 10 11 use SocialDept\AtpClient\Data\Responses\Atproto\Sync\ListReposResponse; 11 12 use SocialDept\AtpClient\Enums\Nsid\AtprotoSync; 12 - use SocialDept\AtpClient\Enums\Scope; 13 13 use SocialDept\AtpClient\Http\Response; 14 14 use SocialDept\AtpSchema\Generated\Com\Atproto\Repo\Defs\CommitMeta; 15 15 ··· 18 18 /** 19 19 * Get a blob associated with a given account 20 20 * 21 - * @requires atproto (rpc:com.atproto.sync.getBlob) 22 - * 23 21 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-blob 24 22 */ 25 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getBlob')] 23 + #[PublicEndpoint] 26 24 public function getBlob(string $did, string $cid): Response 27 25 { 28 26 return $this->atp->client->get( ··· 34 32 /** 35 33 * Download a repository export as CAR file 36 34 * 37 - * @requires atproto (rpc:com.atproto.sync.getRepo) 38 - * 39 35 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-repo 40 36 */ 41 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getRepo')] 37 + #[PublicEndpoint] 42 38 public function getRepo(string $did, ?string $since = null): Response 43 39 { 44 40 return $this->atp->client->get( ··· 50 46 /** 51 47 * Enumerates all the DID, rev, and commit CID for all repos hosted by this service 52 48 * 53 - * @requires atproto (rpc:com.atproto.sync.listRepos) 54 - * 55 49 * @see https://docs.bsky.app/docs/api/com-atproto-sync-list-repos 56 50 */ 57 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.listRepos')] 51 + #[PublicEndpoint] 58 52 public function listRepos(int $limit = 500, ?string $cursor = null): ListReposResponse 59 53 { 60 54 $response = $this->atp->client->get( ··· 66 60 } 67 61 68 62 /** 63 + * Enumerates all the DIDs with records in a specific collection 64 + * 65 + * @see https://docs.bsky.app/docs/api/com-atproto-sync-list-repos-by-collection 66 + */ 67 + #[PublicEndpoint] 68 + public function listReposByCollection( 69 + string|BackedEnum $collection, 70 + int $limit = 500, 71 + ?string $cursor = null 72 + ): ListReposByCollectionResponse { 73 + $collection = $collection instanceof BackedEnum ? $collection->value : $collection; 74 + 75 + $response = $this->atp->client->get( 76 + endpoint: AtprotoSync::ListReposByCollection, 77 + params: compact('collection', 'limit', 'cursor') 78 + ); 79 + 80 + return ListReposByCollectionResponse::fromArray($response->json()); 81 + } 82 + 83 + /** 69 84 * Get the current commit CID & revision of the specified repo 70 - * 71 - * @requires atproto (rpc:com.atproto.sync.getLatestCommit) 72 85 * 73 86 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-latest-commit 74 87 */ 75 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getLatestCommit')] 88 + #[PublicEndpoint] 76 89 public function getLatestCommit(string $did): CommitMeta 77 90 { 78 91 $response = $this->atp->client->get( ··· 85 98 86 99 /** 87 100 * Get data blocks needed to prove the existence or non-existence of record 88 - * 89 - * @requires atproto (rpc:com.atproto.sync.getRecord) 90 101 * 91 102 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-record 92 103 */ 93 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getRecord')] 104 + #[PublicEndpoint] 94 105 public function getRecord(string $did, string|BackedEnum $collection, string $rkey): Response 95 106 { 96 107 $collection = $collection instanceof BackedEnum ? $collection->value : $collection; ··· 103 114 104 115 /** 105 116 * List blob CIDs for an account, since some repo revision 106 - * 107 - * @requires atproto (rpc:com.atproto.sync.listBlobs) 108 117 * 109 118 * @see https://docs.bsky.app/docs/api/com-atproto-sync-list-blobs 110 119 */ 111 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.listBlobs')] 120 + #[PublicEndpoint] 112 121 public function listBlobs( 113 122 string $did, 114 123 ?string $since = null, ··· 126 135 /** 127 136 * Get data blocks from a given repo, by CID 128 137 * 129 - * @requires atproto (rpc:com.atproto.sync.getBlocks) 130 - * 131 138 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-blocks 132 139 */ 133 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getBlocks')] 140 + #[PublicEndpoint] 134 141 public function getBlocks(string $did, array $cids): Response 135 142 { 136 143 return $this->atp->client->get( ··· 142 149 /** 143 150 * Get the hosting status for a repository, on this server 144 151 * 145 - * @requires atproto (rpc:com.atproto.sync.getRepoStatus) 146 - * 147 152 * @see https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 148 153 */ 149 - #[RequiresScope(Scope::Atproto, granular: 'rpc:com.atproto.sync.getRepoStatus')] 154 + #[PublicEndpoint] 150 155 public function getRepoStatus(string $did): GetRepoStatusResponse 151 156 { 152 157 $response = $this->atp->client->get(
+70 -5
src/Client/Requests/Bsky/ActorRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Bsky; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 + use SocialDept\AtpClient\Data\Responses\Bsky\Actor\GetProfilesResponse; 8 + use SocialDept\AtpClient\Data\Responses\Bsky\Actor\GetSuggestionsResponse; 9 + use SocialDept\AtpClient\Data\Responses\Bsky\Actor\SearchActorsResponse; 10 + use SocialDept\AtpClient\Data\Responses\Bsky\Actor\SearchActorsTypeaheadResponse; 7 11 use SocialDept\AtpClient\Enums\Nsid\BskyActor; 8 - use SocialDept\AtpClient\Enums\Scope; 9 12 use SocialDept\AtpSchema\Generated\App\Bsky\Actor\Defs\ProfileViewDetailed; 10 13 11 14 class ActorRequestClient extends Request ··· 13 16 /** 14 17 * Get actor profile 15 18 * 16 - * @requires transition:generic (rpc:app.bsky.actor.getProfile) 17 - * 18 19 * @see https://docs.bsky.app/docs/api/app-bsky-actor-get-profile 19 20 */ 20 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.actor.getProfile')] 21 + #[PublicEndpoint] 21 22 public function getProfile(string $actor): ProfileViewDetailed 22 23 { 23 24 $response = $this->atp->client->get( ··· 26 27 ); 27 28 28 29 return ProfileViewDetailed::fromArray($response->json()); 30 + } 31 + 32 + /** 33 + * Get multiple actor profiles 34 + * 35 + * @see https://docs.bsky.app/docs/api/app-bsky-actor-get-profiles 36 + */ 37 + #[PublicEndpoint] 38 + public function getProfiles(array $actors): GetProfilesResponse 39 + { 40 + $response = $this->atp->client->get( 41 + endpoint: BskyActor::GetProfiles, 42 + params: compact('actors') 43 + ); 44 + 45 + return GetProfilesResponse::fromArray($response->json()); 46 + } 47 + 48 + /** 49 + * Get suggestions for actors to follow 50 + * 51 + * @see https://docs.bsky.app/docs/api/app-bsky-actor-get-suggestions 52 + */ 53 + #[PublicEndpoint] 54 + public function getSuggestions(int $limit = 50, ?string $cursor = null): GetSuggestionsResponse 55 + { 56 + $response = $this->atp->client->get( 57 + endpoint: BskyActor::GetSuggestions, 58 + params: compact('limit', 'cursor') 59 + ); 60 + 61 + return GetSuggestionsResponse::fromArray($response->json()); 62 + } 63 + 64 + /** 65 + * Search for actors 66 + * 67 + * @see https://docs.bsky.app/docs/api/app-bsky-actor-search-actors 68 + */ 69 + #[PublicEndpoint] 70 + public function searchActors(string $q, int $limit = 25, ?string $cursor = null): SearchActorsResponse 71 + { 72 + $response = $this->atp->client->get( 73 + endpoint: BskyActor::SearchActors, 74 + params: compact('q', 'limit', 'cursor') 75 + ); 76 + 77 + return SearchActorsResponse::fromArray($response->json()); 78 + } 79 + 80 + /** 81 + * Search for actors matching a prefix (typeahead/autocomplete) 82 + * 83 + * @see https://docs.bsky.app/docs/api/app-bsky-actor-search-actors-typeahead 84 + */ 85 + #[PublicEndpoint] 86 + public function searchActorsTypeahead(string $q, int $limit = 10): SearchActorsTypeaheadResponse 87 + { 88 + $response = $this->atp->client->get( 89 + endpoint: BskyActor::SearchActorsTypeahead, 90 + params: compact('q', 'limit') 91 + ); 92 + 93 + return SearchActorsTypeaheadResponse::fromArray($response->json()); 29 94 } 30 95 }
+186 -37
src/Client/Requests/Bsky/FeedRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Bsky; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 7 use SocialDept\AtpClient\Client\Requests\Request; 8 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\DescribeFeedGeneratorResponse; 9 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetActorFeedsResponse; 10 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetActorLikesResponse; 7 11 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetAuthorFeedResponse; 12 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedGeneratorResponse; 13 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedGeneratorsResponse; 14 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetFeedResponse; 8 15 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetLikesResponse; 16 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetPostsResponse; 9 17 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetPostThreadResponse; 18 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetQuotesResponse; 10 19 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetRepostedByResponse; 20 + use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetSuggestedFeedsResponse; 11 21 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\GetTimelineResponse; 12 22 use SocialDept\AtpClient\Data\Responses\Bsky\Feed\SearchPostsResponse; 13 23 use SocialDept\AtpClient\Enums\Nsid\BskyFeed; ··· 16 26 class FeedRequestClient extends Request 17 27 { 18 28 /** 19 - * Get timeline feed 29 + * Describe feed generator 20 30 * 21 - * @requires transition:generic (rpc:app.bsky.feed.getTimeline) 31 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-describe-feed-generator 32 + */ 33 + #[PublicEndpoint] 34 + public function describeFeedGenerator(): DescribeFeedGeneratorResponse 35 + { 36 + $response = $this->atp->client->get( 37 + endpoint: BskyFeed::DescribeFeedGenerator 38 + ); 39 + 40 + return DescribeFeedGeneratorResponse::fromArray($response->json()); 41 + } 42 + 43 + /** 44 + * Get timeline feed (requires authentication) 22 45 * 23 46 * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-timeline 24 47 */ 25 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getTimeline')] 48 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getTimeline')] 26 49 public function getTimeline(int $limit = 50, ?string $cursor = null): GetTimelineResponse 27 50 { 28 51 $response = $this->atp->client->get( ··· 36 59 /** 37 60 * Get author feed 38 61 * 39 - * @requires transition:generic (rpc:app.bsky.feed.getAuthorFeed) 40 - * 41 62 * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-author-feed 42 63 */ 43 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getAuthorFeed')] 64 + #[PublicEndpoint] 44 65 public function getAuthorFeed( 45 66 string $actor, 46 67 int $limit = 50, 47 - ?string $cursor = null 68 + ?string $cursor = null, 69 + ?string $filter = null 48 70 ): GetAuthorFeedResponse { 49 71 $response = $this->atp->client->get( 50 72 endpoint: BskyFeed::GetAuthorFeed, 73 + params: compact('actor', 'limit', 'cursor', 'filter') 74 + ); 75 + 76 + return GetAuthorFeedResponse::fromArray($response->json()); 77 + } 78 + 79 + /** 80 + * Get feeds created by an actor 81 + * 82 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-actor-feeds 83 + */ 84 + #[PublicEndpoint] 85 + public function getActorFeeds(string $actor, int $limit = 50, ?string $cursor = null): GetActorFeedsResponse 86 + { 87 + $response = $this->atp->client->get( 88 + endpoint: BskyFeed::GetActorFeeds, 51 89 params: compact('actor', 'limit', 'cursor') 52 90 ); 53 91 54 - return GetAuthorFeedResponse::fromArray($response->json()); 92 + return GetActorFeedsResponse::fromArray($response->json()); 93 + } 94 + 95 + /** 96 + * Get posts liked by an actor 97 + * 98 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-actor-likes 99 + */ 100 + #[PublicEndpoint] 101 + public function getActorLikes(string $actor, int $limit = 50, ?string $cursor = null): GetActorLikesResponse 102 + { 103 + $response = $this->atp->client->get( 104 + endpoint: BskyFeed::GetActorLikes, 105 + params: compact('actor', 'limit', 'cursor') 106 + ); 107 + 108 + return GetActorLikesResponse::fromArray($response->json()); 109 + } 110 + 111 + /** 112 + * Get a feed 113 + * 114 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-feed 115 + */ 116 + #[PublicEndpoint] 117 + public function getFeed(string $feed, int $limit = 50, ?string $cursor = null): GetFeedResponse 118 + { 119 + $response = $this->atp->client->get( 120 + endpoint: BskyFeed::GetFeed, 121 + params: compact('feed', 'limit', 'cursor') 122 + ); 123 + 124 + return GetFeedResponse::fromArray($response->json()); 125 + } 126 + 127 + /** 128 + * Get a feed generator 129 + * 130 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-feed-generator 131 + */ 132 + #[PublicEndpoint] 133 + public function getFeedGenerator(string $feed): GetFeedGeneratorResponse 134 + { 135 + $response = $this->atp->client->get( 136 + endpoint: BskyFeed::GetFeedGenerator, 137 + params: compact('feed') 138 + ); 139 + 140 + return GetFeedGeneratorResponse::fromArray($response->json()); 141 + } 142 + 143 + /** 144 + * Get multiple feed generators 145 + * 146 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-feed-generators 147 + */ 148 + #[PublicEndpoint] 149 + public function getFeedGenerators(array $feeds): GetFeedGeneratorsResponse 150 + { 151 + $response = $this->atp->client->get( 152 + endpoint: BskyFeed::GetFeedGenerators, 153 + params: compact('feeds') 154 + ); 155 + 156 + return GetFeedGeneratorsResponse::fromArray($response->json()); 55 157 } 56 158 57 159 /** 58 160 * Get post thread 59 161 * 60 - * @requires transition:generic (rpc:app.bsky.feed.getPostThread) 61 - * 62 162 * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread 63 163 */ 64 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getPostThread')] 65 - public function getPostThread(string $uri, int $depth = 6): GetPostThreadResponse 164 + #[PublicEndpoint] 165 + public function getPostThread(string $uri, int $depth = 6, int $parentHeight = 80): GetPostThreadResponse 66 166 { 67 167 $response = $this->atp->client->get( 68 168 endpoint: BskyFeed::GetPostThread, 69 - params: compact('uri', 'depth') 169 + params: compact('uri', 'depth', 'parentHeight') 70 170 ); 71 171 72 172 return GetPostThreadResponse::fromArray($response->json()); 73 173 } 74 174 75 175 /** 76 - * Search posts 176 + * Get multiple posts by URI 77 177 * 78 - * @requires transition:generic (rpc:app.bsky.feed.searchPosts) 79 - * 80 - * @see https://docs.bsky.app/docs/api/app-bsky-feed-search-posts 178 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-posts 81 179 */ 82 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.searchPosts')] 83 - public function searchPosts( 84 - string $q, 85 - int $limit = 25, 86 - ?string $cursor = null 87 - ): SearchPostsResponse { 180 + #[PublicEndpoint] 181 + public function getPosts(array $uris): GetPostsResponse 182 + { 88 183 $response = $this->atp->client->get( 89 - endpoint: BskyFeed::SearchPosts, 90 - params: compact('q', 'limit', 'cursor') 184 + endpoint: BskyFeed::GetPosts, 185 + params: compact('uris') 91 186 ); 92 187 93 - return SearchPostsResponse::fromArray($response->json()); 188 + return GetPostsResponse::fromArray($response->json()); 94 189 } 95 190 96 191 /** 97 192 * Get likes for a post 98 - * 99 - * @requires transition:generic (rpc:app.bsky.feed.getLikes) 100 193 * 101 194 * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-likes 102 195 */ 103 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getLikes')] 196 + #[PublicEndpoint] 104 197 public function getLikes( 105 198 string $uri, 106 199 int $limit = 50, 107 - ?string $cursor = null 200 + ?string $cursor = null, 201 + ?string $cid = null 108 202 ): GetLikesResponse { 109 203 $response = $this->atp->client->get( 110 204 endpoint: BskyFeed::GetLikes, 111 - params: compact('uri', 'limit', 'cursor') 205 + params: compact('uri', 'limit', 'cursor', 'cid') 112 206 ); 113 207 114 208 return GetLikesResponse::fromArray($response->json()); 115 209 } 116 210 117 211 /** 212 + * Get quotes of a post 213 + * 214 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-quotes 215 + */ 216 + #[PublicEndpoint] 217 + public function getQuotes( 218 + string $uri, 219 + int $limit = 50, 220 + ?string $cursor = null, 221 + ?string $cid = null 222 + ): GetQuotesResponse { 223 + $response = $this->atp->client->get( 224 + endpoint: BskyFeed::GetQuotes, 225 + params: compact('uri', 'limit', 'cursor', 'cid') 226 + ); 227 + 228 + return GetQuotesResponse::fromArray($response->json()); 229 + } 230 + 231 + /** 118 232 * Get reposts for a post 119 233 * 120 - * @requires transition:generic (rpc:app.bsky.feed.getRepostedBy) 121 - * 122 234 * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-reposted-by 123 235 */ 124 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:app.bsky.feed.getRepostedBy')] 236 + #[PublicEndpoint] 125 237 public function getRepostedBy( 126 238 string $uri, 127 239 int $limit = 50, 128 - ?string $cursor = null 240 + ?string $cursor = null, 241 + ?string $cid = null 129 242 ): GetRepostedByResponse { 130 243 $response = $this->atp->client->get( 131 244 endpoint: BskyFeed::GetRepostedBy, 132 - params: compact('uri', 'limit', 'cursor') 245 + params: compact('uri', 'limit', 'cursor', 'cid') 133 246 ); 134 247 135 248 return GetRepostedByResponse::fromArray($response->json()); 249 + } 250 + 251 + /** 252 + * Get suggested feeds 253 + * 254 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-get-suggested-feeds 255 + */ 256 + #[PublicEndpoint] 257 + public function getSuggestedFeeds(int $limit = 50, ?string $cursor = null): GetSuggestedFeedsResponse 258 + { 259 + $response = $this->atp->client->get( 260 + endpoint: BskyFeed::GetSuggestedFeeds, 261 + params: compact('limit', 'cursor') 262 + ); 263 + 264 + return GetSuggestedFeedsResponse::fromArray($response->json()); 265 + } 266 + 267 + /** 268 + * Search posts 269 + * 270 + * @see https://docs.bsky.app/docs/api/app-bsky-feed-search-posts 271 + */ 272 + #[PublicEndpoint] 273 + public function searchPosts( 274 + string $q, 275 + int $limit = 25, 276 + ?string $cursor = null, 277 + ?string $sort = null 278 + ): SearchPostsResponse { 279 + $response = $this->atp->client->get( 280 + endpoint: BskyFeed::SearchPosts, 281 + params: compact('q', 'limit', 'cursor', 'sort') 282 + ); 283 + 284 + return SearchPostsResponse::fromArray($response->json()); 136 285 } 137 286 }
+27
src/Client/Requests/Bsky/LabelerRequestClient.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Client\Requests\Bsky; 4 + 5 + use SocialDept\AtpClient\Attributes\PublicEndpoint; 6 + use SocialDept\AtpClient\Client\Requests\Request; 7 + use SocialDept\AtpClient\Data\Responses\Bsky\Labeler\GetServicesResponse; 8 + use SocialDept\AtpClient\Enums\Nsid\BskyLabeler; 9 + 10 + class LabelerRequestClient extends Request 11 + { 12 + /** 13 + * Get labeler services 14 + * 15 + * @see https://docs.bsky.app/docs/api/app-bsky-labeler-get-services 16 + */ 17 + #[PublicEndpoint] 18 + public function getServices(array $dids, bool $detailed = false): GetServicesResponse 19 + { 20 + $response = $this->atp->client->get( 21 + endpoint: BskyLabeler::GetServices, 22 + params: compact('dids', 'detailed') 23 + ); 24 + 25 + return GetServicesResponse::fromArray($response->json(), $detailed); 26 + } 27 + }
+4 -4
src/Client/Requests/Chat/ActorRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Chat; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Enums\Nsid\ChatActor; 8 8 use SocialDept\AtpClient\Enums\Scope; ··· 17 17 * 18 18 * @see https://docs.bsky.app/docs/api/chat-bsky-actor-export-account-data 19 19 */ 20 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.getActorMetadata')] 20 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.getActorMetadata')] 21 21 public function getActorMetadata(): Response 22 22 { 23 23 return $this->atp->client->get( ··· 32 32 * 33 33 * @see https://docs.bsky.app/docs/api/chat-bsky-actor-export-account-data 34 34 */ 35 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.exportAccountData')] 35 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.exportAccountData')] 36 36 public function exportAccountData(): Response 37 37 { 38 38 return $this->atp->client->get( ··· 47 47 * 48 48 * @see https://docs.bsky.app/docs/api/chat-bsky-actor-delete-account 49 49 */ 50 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.deleteAccount')] 50 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.deleteAccount')] 51 51 public function deleteAccount(): void 52 52 { 53 53 $this->atp->client->post(
+13 -13
src/Client/Requests/Chat/ConvoRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Chat; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\Responses\Chat\Convo\GetLogResponse; 8 8 use SocialDept\AtpClient\Data\Responses\Chat\Convo\GetMessagesResponse; ··· 24 24 * 25 25 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-convo 26 26 */ 27 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getConvo')] 27 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getConvo')] 28 28 public function getConvo(string $convoId): ConvoView 29 29 { 30 30 $response = $this->atp->client->get( ··· 42 42 * 43 43 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-convo-for-members 44 44 */ 45 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getConvoForMembers')] 45 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getConvoForMembers')] 46 46 public function getConvoForMembers(array $members): ConvoView 47 47 { 48 48 $response = $this->atp->client->get( ··· 60 60 * 61 61 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-list-convos 62 62 */ 63 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.listConvos')] 63 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.listConvos')] 64 64 public function listConvos(int $limit = 50, ?string $cursor = null): ListConvosResponse 65 65 { 66 66 $response = $this->atp->client->get( ··· 78 78 * 79 79 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-messages 80 80 */ 81 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getMessages')] 81 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getMessages')] 82 82 public function getMessages( 83 83 string $convoId, 84 84 int $limit = 50, ··· 99 99 * 100 100 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-send-message 101 101 */ 102 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.sendMessage')] 102 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.sendMessage')] 103 103 public function sendMessage(string $convoId, array $message): MessageView 104 104 { 105 105 $response = $this->atp->client->post( ··· 117 117 * 118 118 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-send-message-batch 119 119 */ 120 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.sendMessageBatch')] 120 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.sendMessageBatch')] 121 121 public function sendMessageBatch(array $items): SendMessageBatchResponse 122 122 { 123 123 $response = $this->atp->client->post( ··· 135 135 * 136 136 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-delete-message-for-self 137 137 */ 138 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.deleteMessageForSelf')] 138 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.deleteMessageForSelf')] 139 139 public function deleteMessageForSelf(string $convoId, string $messageId): DeletedMessageView 140 140 { 141 141 $response = $this->atp->client->post( ··· 153 153 * 154 154 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-update-read 155 155 */ 156 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.updateRead')] 156 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.updateRead')] 157 157 public function updateRead(string $convoId, ?string $messageId = null): ConvoView 158 158 { 159 159 $response = $this->atp->client->post( ··· 171 171 * 172 172 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-mute-convo 173 173 */ 174 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.muteConvo')] 174 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.muteConvo')] 175 175 public function muteConvo(string $convoId): ConvoView 176 176 { 177 177 $response = $this->atp->client->post( ··· 189 189 * 190 190 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-unmute-convo 191 191 */ 192 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.unmuteConvo')] 192 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.unmuteConvo')] 193 193 public function unmuteConvo(string $convoId): ConvoView 194 194 { 195 195 $response = $this->atp->client->post( ··· 207 207 * 208 208 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-leave-convo 209 209 */ 210 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.leaveConvo')] 210 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.leaveConvo')] 211 211 public function leaveConvo(string $convoId): LeaveConvoResponse 212 212 { 213 213 $response = $this->atp->client->post( ··· 225 225 * 226 226 * @see https://docs.bsky.app/docs/api/chat-bsky-convo-get-log 227 227 */ 228 - #[RequiresScope(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getLog')] 228 + #[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.convo.getLog')] 229 229 public function getLog(?string $cursor = null): GetLogResponse 230 230 { 231 231 $response = $this->atp->client->get(
+9 -9
src/Client/Requests/Ozone/ModerationRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Ozone; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\Responses\Ozone\Moderation\QueryEventsResponse; 8 8 use SocialDept\AtpClient\Data\Responses\Ozone\Moderation\QueryStatusesResponse; ··· 24 24 * 25 25 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-event 26 26 */ 27 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getEvent')] 27 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getEvent')] 28 28 public function getModerationEvent(int $id): ModEventViewDetail 29 29 { 30 30 $response = $this->atp->client->get( ··· 42 42 * 43 43 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-events 44 44 */ 45 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getEvents')] 45 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getEvents')] 46 46 public function getModerationEvents( 47 47 ?string $subject = null, 48 48 ?array $types = null, ··· 66 66 * 67 67 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-record 68 68 */ 69 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getRecord')] 69 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getRecord')] 70 70 public function getRecord(string $uri, ?string $cid = null): RecordViewDetail 71 71 { 72 72 $response = $this->atp->client->get( ··· 84 84 * 85 85 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-get-repo 86 86 */ 87 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getRepo')] 87 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.getRepo')] 88 88 public function getRepo(string $did): RepoViewDetail 89 89 { 90 90 $response = $this->atp->client->get( ··· 102 102 * 103 103 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-events 104 104 */ 105 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.queryEvents')] 105 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.queryEvents')] 106 106 public function queryEvents( 107 107 ?array $types = null, 108 108 ?string $createdBy = null, ··· 129 129 * 130 130 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-query-statuses 131 131 */ 132 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.queryStatuses')] 132 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.queryStatuses')] 133 133 public function queryStatuses( 134 134 ?string $subject = null, 135 135 ?array $tags = null, ··· 155 155 * 156 156 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-search-repos 157 157 */ 158 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.searchRepos')] 158 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.searchRepos')] 159 159 public function searchRepos( 160 160 ?string $term = null, 161 161 ?string $invitedBy = null, ··· 180 180 * 181 181 * @see https://docs.bsky.app/docs/api/tools-ozone-moderation-emit-event 182 182 */ 183 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.emitEvent')] 183 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.moderation.emitEvent')] 184 184 public function emitEvent( 185 185 array $event, 186 186 string $subject,
+3 -3
src/Client/Requests/Ozone/ServerRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Ozone; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\Responses\Ozone\Server\GetConfigResponse; 8 8 use SocialDept\AtpClient\Enums\Nsid\OzoneServer; ··· 18 18 * 19 19 * @see https://docs.bsky.app/docs/api/tools-ozone-server-get-config 20 20 */ 21 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.server.getBlob')] 21 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.server.getBlob')] 22 22 public function getBlob(string $did, string $cid): Response 23 23 { 24 24 return $this->atp->client->get( ··· 34 34 * 35 35 * @see https://docs.bsky.app/docs/api/tools-ozone-server-get-config 36 36 */ 37 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.server.getConfig')] 37 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.server.getConfig')] 38 38 public function getConfig(): GetConfigResponse 39 39 { 40 40 $response = $this->atp->client->get(
+6 -6
src/Client/Requests/Ozone/TeamRequestClient.php
··· 2 2 3 3 namespace SocialDept\AtpClient\Client\Requests\Ozone; 4 4 5 - use SocialDept\AtpClient\Attributes\RequiresScope; 5 + use SocialDept\AtpClient\Attributes\ScopedEndpoint; 6 6 use SocialDept\AtpClient\Client\Requests\Request; 7 7 use SocialDept\AtpClient\Data\Responses\Ozone\Team\ListMembersResponse; 8 8 use SocialDept\AtpClient\Enums\Nsid\OzoneTeam; ··· 19 19 * 20 20 * @see https://docs.bsky.app/docs/api/tools-ozone-team-list-members 21 21 */ 22 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.getMember')] 22 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.getMember')] 23 23 public function getTeamMember(string $did): array 24 24 { 25 25 $response = $this->atp->client->get( ··· 37 37 * 38 38 * @see https://docs.bsky.app/docs/api/tools-ozone-team-list-members 39 39 */ 40 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.listMembers')] 40 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.listMembers')] 41 41 public function listTeamMembers(int $limit = 50, ?string $cursor = null): ListMembersResponse 42 42 { 43 43 $response = $this->atp->client->get( ··· 57 57 * 58 58 * @see https://docs.bsky.app/docs/api/tools-ozone-team-add-member 59 59 */ 60 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.addMember')] 60 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.addMember')] 61 61 public function addTeamMember(string $did, string $role): array 62 62 { 63 63 $response = $this->atp->client->post( ··· 77 77 * 78 78 * @see https://docs.bsky.app/docs/api/tools-ozone-team-update-member 79 79 */ 80 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.updateMember')] 80 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.updateMember')] 81 81 public function updateTeamMember( 82 82 string $did, 83 83 ?bool $disabled = null, ··· 101 101 * 102 102 * @see https://docs.bsky.app/docs/api/tools-ozone-team-delete-member 103 103 */ 104 - #[RequiresScope(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.deleteMember')] 104 + #[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.deleteMember')] 105 105 public function deleteTeamMember(string $did): void 106 106 { 107 107 $this->atp->client->post(
+13 -44
src/Console/MakeAtpClientCommand.php
··· 10 10 { 11 11 protected $signature = 'make:atp-client 12 12 {name : The name of the client class} 13 - {--public : Generate a public client extension instead of authenticated} 14 13 {--force : Overwrite existing file}'; 15 14 16 15 protected $description = 'Create a new ATP domain client extension'; ··· 23 22 public function handle(): int 24 23 { 25 24 $name = $this->argument('name'); 26 - $isPublic = $this->option('public'); 27 25 28 26 if (! Str::endsWith($name, 'Client')) { 29 27 $name .= 'Client'; 30 28 } 31 29 32 - $path = $this->getPath($name, $isPublic); 30 + $path = $this->getPath($name); 33 31 34 32 if ($this->files->exists($path) && ! $this->option('force')) { 35 33 $this->components->error("Client [{$name}] already exists!"); ··· 39 37 40 38 $this->makeDirectory($path); 41 39 42 - $stub = $isPublic ? $this->getPublicStub() : $this->getStub(); 43 - $content = $this->populateStub($stub, $name, $isPublic); 40 + $content = $this->populateStub($this->getStub(), $name); 44 41 45 42 $this->files->put($path, $content); 46 43 47 44 $this->components->info("Client [{$path}] created successfully."); 48 45 49 - $this->outputRegistrationHint($name, $isPublic); 46 + $this->outputRegistrationHint($name); 50 47 51 48 return self::SUCCESS; 52 49 } 53 50 54 - protected function getPath(string $name, bool $isPublic = false): string 51 + protected function getPath(string $name): string 55 52 { 56 - $basePath = $isPublic 57 - ? config('client.generators.client_public_path', 'app/Services/Clients/Public') 58 - : config('client.generators.client_path', 'app/Services/Clients'); 53 + $basePath = config('client.generators.client_path', 'app/Services/Clients'); 59 54 60 55 return base_path($basePath.'/'.$name.'.php'); 61 56 } ··· 67 62 } 68 63 } 69 64 70 - protected function getNamespace(bool $isPublic = false): string 65 + protected function getNamespace(): string 71 66 { 72 - $basePath = $isPublic 73 - ? config('client.generators.client_public_path', 'app/Services/Clients/Public') 74 - : config('client.generators.client_path', 'app/Services/Clients'); 67 + $basePath = config('client.generators.client_path', 'app/Services/Clients'); 75 68 76 69 return Str::of($basePath) 77 70 ->replace('/', '\\') ··· 80 73 ->toString(); 81 74 } 82 75 83 - protected function populateStub(string $stub, string $name, bool $isPublic = false): string 76 + protected function populateStub(string $stub, string $name): string 84 77 { 85 78 return str_replace( 86 79 ['{{ namespace }}', '{{ class }}'], 87 - [$this->getNamespace($isPublic), $name], 80 + [$this->getNamespace(), $name], 88 81 $stub 89 82 ); 90 83 } 91 84 92 - protected function outputRegistrationHint(string $name, bool $isPublic): void 85 + protected function outputRegistrationHint(string $name): void 93 86 { 94 87 $this->newLine(); 95 88 $this->components->info('Register the extension in your AppServiceProvider:'); 96 89 $this->newLine(); 97 90 98 - $namespace = $this->getNamespace($isPublic); 91 + $namespace = $this->getNamespace(); 99 92 $extensionName = Str::of($name)->before('Client')->camel()->toString(); 100 - $clientClass = $isPublic ? 'AtpPublicClient' : 'AtpClient'; 101 93 102 94 $this->line("use {$namespace}\\{$name};"); 103 - $this->line("use SocialDept\\AtpClient\\".($isPublic ? 'Client\\Public\\' : '').$clientClass.';'); 95 + $this->line("use SocialDept\\AtpClient\\AtpClient;"); 104 96 $this->newLine(); 105 97 $this->line("// In boot() method:"); 106 - $this->line("{$clientClass}::extend('{$extensionName}', fn({$clientClass} \$atp) => new {$name}(\$atp));"); 98 + $this->line("AtpClient::extend('{$extensionName}', fn(AtpClient \$atp) => new {$name}(\$atp));"); 107 99 } 108 100 109 101 protected function getStub(): string ··· 120 112 protected AtpClient $atp; 121 113 122 114 public function __construct(AtpClient $parent) 123 - { 124 - $this->atp = $parent; 125 - } 126 - 127 - // 128 - } 129 - STUB; 130 - } 131 - 132 - protected function getPublicStub(): string 133 - { 134 - return <<<'STUB' 135 - <?php 136 - 137 - namespace {{ namespace }}; 138 - 139 - use SocialDept\AtpClient\Client\Public\AtpPublicClient; 140 - 141 - class {{ class }} 142 - { 143 - protected AtpPublicClient $atp; 144 - 145 - public function __construct(AtpPublicClient $parent) 146 115 { 147 116 $this->atp = $parent; 148 117 }
+13 -37
src/Console/MakeAtpRequestCommand.php
··· 11 11 protected $signature = 'make:atp-request 12 12 {name : The name of the request client class} 13 13 {--domain=bsky : The domain to extend (bsky, atproto, chat, ozone)} 14 - {--public : Generate a public request client instead of authenticated} 15 14 {--force : Overwrite existing file}'; 16 15 17 16 protected $description = 'Create a new ATP request client extension for an existing domain'; ··· 27 26 { 28 27 $name = $this->argument('name'); 29 28 $domain = $this->option('domain'); 30 - $isPublic = $this->option('public'); 31 29 32 30 if (! in_array($domain, $this->validDomains)) { 33 31 $this->components->error("Invalid domain [{$domain}]. Valid domains: ".implode(', ', $this->validDomains)); ··· 39 37 $name .= 'Client'; 40 38 } 41 39 42 - $path = $this->getPath($name, $isPublic); 40 + $path = $this->getPath($name); 43 41 44 42 if ($this->files->exists($path) && ! $this->option('force')) { 45 43 $this->components->error("Request client [{$name}] already exists!"); ··· 49 47 50 48 $this->makeDirectory($path); 51 49 52 - $stub = $isPublic ? $this->getPublicStub() : $this->getStub(); 53 - $content = $this->populateStub($stub, $name, $isPublic); 50 + $content = $this->populateStub($this->getStub(), $name); 54 51 55 52 $this->files->put($path, $content); 56 53 57 54 $this->components->info("Request client [{$path}] created successfully."); 58 55 59 - $this->outputRegistrationHint($name, $domain, $isPublic); 56 + $this->outputRegistrationHint($name, $domain); 60 57 61 58 return self::SUCCESS; 62 59 } 63 60 64 - protected function getPath(string $name, bool $isPublic = false): string 61 + protected function getPath(string $name): string 65 62 { 66 - $basePath = $isPublic 67 - ? config('client.generators.request_public_path', 'app/Services/Clients/Public/Requests') 68 - : config('client.generators.request_path', 'app/Services/Clients/Requests'); 63 + $basePath = config('client.generators.request_path', 'app/Services/Clients/Requests'); 69 64 70 65 return base_path($basePath.'/'.$name.'.php'); 71 66 } ··· 77 72 } 78 73 } 79 74 80 - protected function getNamespace(bool $isPublic = false): string 75 + protected function getNamespace(): string 81 76 { 82 - $basePath = $isPublic 83 - ? config('client.generators.request_public_path', 'app/Services/Clients/Public/Requests') 84 - : config('client.generators.request_path', 'app/Services/Clients/Requests'); 77 + $basePath = config('client.generators.request_path', 'app/Services/Clients/Requests'); 85 78 86 79 return Str::of($basePath) 87 80 ->replace('/', '\\') ··· 90 83 ->toString(); 91 84 } 92 85 93 - protected function populateStub(string $stub, string $name, bool $isPublic = false): string 86 + protected function populateStub(string $stub, string $name): string 94 87 { 95 88 return str_replace( 96 89 ['{{ namespace }}', '{{ class }}'], 97 - [$this->getNamespace($isPublic), $name], 90 + [$this->getNamespace(), $name], 98 91 $stub 99 92 ); 100 93 } 101 94 102 - protected function outputRegistrationHint(string $name, string $domain, bool $isPublic): void 95 + protected function outputRegistrationHint(string $name, string $domain): void 103 96 { 104 97 $this->newLine(); 105 98 $this->components->info('Register the extension in your AppServiceProvider:'); 106 99 $this->newLine(); 107 100 108 - $namespace = $this->getNamespace($isPublic); 101 + $namespace = $this->getNamespace(); 109 102 $extensionName = Str::of($name)->before('Client')->camel()->toString(); 110 - $clientClass = $isPublic ? 'AtpPublicClient' : 'AtpClient'; 111 103 112 104 $this->line("use {$namespace}\\{$name};"); 113 - $this->line("use SocialDept\\AtpClient\\".($isPublic ? 'Client\\Public\\' : '').$clientClass.';'); 105 + $this->line("use SocialDept\\AtpClient\\AtpClient;"); 114 106 $this->newLine(); 115 107 $this->line("// In boot() method:"); 116 - $this->line("{$clientClass}::extendDomain('{$domain}', '{$extensionName}', fn(\$domain) => new {$name}(\$domain));"); 108 + $this->line("AtpClient::extendDomain('{$domain}', '{$extensionName}', fn(\$domain) => new {$name}(\$domain));"); 117 109 } 118 110 119 111 protected function getStub(): string ··· 126 118 use SocialDept\AtpClient\Client\Requests\Request; 127 119 128 120 class {{ class }} extends Request 129 - { 130 - // 131 - } 132 - STUB; 133 - } 134 - 135 - protected function getPublicStub(): string 136 - { 137 - return <<<'STUB' 138 - <?php 139 - 140 - namespace {{ namespace }}; 141 - 142 - use SocialDept\AtpClient\Client\Public\Requests\PublicRequest; 143 - 144 - class {{ class }} extends PublicRequest 145 121 { 146 122 // 147 123 }
+36
src/Data/Responses/Atproto/Sync/ListReposByCollectionResponse.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Data\Responses\Atproto\Sync; 4 + 5 + use Illuminate\Contracts\Support\Arrayable; 6 + use Illuminate\Support\Collection; 7 + 8 + /** 9 + * @implements Arrayable<string, mixed> 10 + */ 11 + class ListReposByCollectionResponse implements Arrayable 12 + { 13 + /** 14 + * @param Collection<int, array{did: string, rev: string}> $repos 15 + */ 16 + public function __construct( 17 + public readonly Collection $repos, 18 + public readonly ?string $cursor = null, 19 + ) {} 20 + 21 + public static function fromArray(array $data): self 22 + { 23 + return new self( 24 + repos: collect($data['repos'] ?? []), 25 + cursor: $data['cursor'] ?? null, 26 + ); 27 + } 28 + 29 + public function toArray(): array 30 + { 31 + return [ 32 + 'repos' => $this->repos->all(), 33 + 'cursor' => $this->cursor, 34 + ]; 35 + } 36 + }
+1
src/Enums/Nsid/AtprotoSync.php
··· 10 10 case GetBlob = 'com.atproto.sync.getBlob'; 11 11 case GetRepo = 'com.atproto.sync.getRepo'; 12 12 case ListRepos = 'com.atproto.sync.listRepos'; 13 + case ListReposByCollection = 'com.atproto.sync.listReposByCollection'; 13 14 case GetLatestCommit = 'com.atproto.sync.getLatestCommit'; 14 15 case GetRecord = 'com.atproto.sync.getRecord'; 15 16 case ListBlobs = 'com.atproto.sync.listBlobs';
+2 -3
src/Facades/Atp.php
··· 3 3 namespace SocialDept\AtpClient\Facades; 4 4 5 5 use Illuminate\Support\Facades\Facade; 6 + use SocialDept\AtpClient\AtpClient; 6 7 use SocialDept\AtpClient\Auth\OAuthEngine; 7 - use SocialDept\AtpClient\Client\AtpClient; 8 - use SocialDept\AtpClient\Client\Public\AtpPublicClient; 9 8 use SocialDept\AtpClient\Contracts\CredentialProvider; 10 9 11 10 /** 12 11 * @method static AtpClient as(string $actor) 13 12 * @method static AtpClient login(string $actor, string $password) 14 13 * @method static OAuthEngine oauth() 15 - * @method static AtpPublicClient public(?string $service = null) 14 + * @method static AtpClient public(?string $service = null) 16 15 * @method static void setDefaultProvider(CredentialProvider $provider) 17 16 * 18 17 * @see \SocialDept\AtpClient\AtpClientServiceProvider
+3 -3
src/Http/HasHttp.php
··· 16 16 17 17 trait HasHttp 18 18 { 19 - protected SessionManager $sessions; 19 + protected ?SessionManager $sessions = null; 20 20 21 - protected string $did; 21 + protected ?string $did = null; 22 22 23 - protected DPoPClient $dpopClient; 23 + protected ?DPoPClient $dpopClient = null; 24 24 25 25 protected ?ScopeChecker $scopeChecker = null; 26 26