+3
-5
composer.json
+3
-5
composer.json
···
45
45
}
46
46
},
47
47
"scripts": {
48
-
"test": "vendor/bin/pest",
49
-
"test-coverage": "vendor/bin/pest --coverage",
48
+
"test": "vendor/bin/phpunit",
49
+
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
50
50
"format": "vendor/bin/php-cs-fixer fix"
51
51
},
52
52
"extra": {
···
62
62
"minimum-stability": "dev",
63
63
"prefer-stable": true,
64
64
"config": {
65
-
"allow-plugins": {
66
-
"pestphp/pest-plugin": false
67
-
}
65
+
"sort-packages": true
68
66
}
69
67
}
+257
src/Builders/PostBuilder.php
+257
src/Builders/PostBuilder.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpClient\Builders;
4
+
5
+
use Closure;
6
+
use DateTimeInterface;
7
+
use SocialDept\AtpClient\Builders\Concerns\BuildsRichText;
8
+
use SocialDept\AtpClient\Builders\Embeds\ImagesBuilder;
9
+
use SocialDept\AtpClient\Client\Records\PostRecordClient;
10
+
use SocialDept\AtpClient\Contracts\Recordable;
11
+
use SocialDept\AtpClient\Data\StrongRef;
12
+
use SocialDept\AtpClient\Enums\Nsid\BskyFeed;
13
+
14
+
class PostBuilder implements Recordable
15
+
{
16
+
use BuildsRichText;
17
+
18
+
protected ?array $embed = null;
19
+
20
+
protected ?array $reply = null;
21
+
22
+
protected ?array $langs = null;
23
+
24
+
protected ?DateTimeInterface $createdAt = null;
25
+
26
+
protected ?PostRecordClient $client = null;
27
+
28
+
/**
29
+
* Create a new post builder instance
30
+
*/
31
+
public static function make(): self
32
+
{
33
+
return new self;
34
+
}
35
+
36
+
/**
37
+
* Add images embed
38
+
*
39
+
* @param Closure|array $images Closure receiving ImagesBuilder, or array of image data
40
+
*/
41
+
public function images(Closure|array $images): self
42
+
{
43
+
if ($images instanceof Closure) {
44
+
$builder = ImagesBuilder::make();
45
+
$images($builder);
46
+
$this->embed = $builder->toArray();
47
+
} else {
48
+
$this->embed = [
49
+
'$type' => 'app.bsky.embed.images',
50
+
'images' => array_map(fn ($img) => $this->normalizeImageData($img), $images),
51
+
];
52
+
}
53
+
54
+
return $this;
55
+
}
56
+
57
+
/**
58
+
* Add external link embed (link card)
59
+
*
60
+
* @param string $uri URL of the external content
61
+
* @param string $title Title of the link card
62
+
* @param string $description Description text
63
+
* @param mixed|null $thumb Optional thumbnail blob
64
+
*/
65
+
public function external(string $uri, string $title, string $description, mixed $thumb = null): self
66
+
{
67
+
$external = [
68
+
'uri' => $uri,
69
+
'title' => $title,
70
+
'description' => $description,
71
+
];
72
+
73
+
if ($thumb !== null) {
74
+
$external['thumb'] = $this->normalizeBlob($thumb);
75
+
}
76
+
77
+
$this->embed = [
78
+
'$type' => 'app.bsky.embed.external',
79
+
'external' => $external,
80
+
];
81
+
82
+
return $this;
83
+
}
84
+
85
+
/**
86
+
* Add video embed
87
+
*
88
+
* @param mixed $blob Video blob reference
89
+
* @param string|null $alt Alt text for the video
90
+
* @param array|null $captions Optional captions array
91
+
*/
92
+
public function video(mixed $blob, ?string $alt = null, ?array $captions = null): self
93
+
{
94
+
$video = [
95
+
'$type' => 'app.bsky.embed.video',
96
+
'video' => $this->normalizeBlob($blob),
97
+
];
98
+
99
+
if ($alt !== null) {
100
+
$video['alt'] = $alt;
101
+
}
102
+
103
+
if ($captions !== null) {
104
+
$video['captions'] = $captions;
105
+
}
106
+
107
+
$this->embed = $video;
108
+
109
+
return $this;
110
+
}
111
+
112
+
/**
113
+
* Add quote embed (embed another post)
114
+
*/
115
+
public function quote(StrongRef $post): self
116
+
{
117
+
$this->embed = [
118
+
'$type' => 'app.bsky.embed.record',
119
+
'record' => $post->toArray(),
120
+
];
121
+
122
+
return $this;
123
+
}
124
+
125
+
/**
126
+
* Set as a reply to another post
127
+
*
128
+
* @param StrongRef $parent The post being replied to
129
+
* @param StrongRef|null $root The root post of the thread (defaults to parent if not provided)
130
+
*/
131
+
public function replyTo(StrongRef $parent, ?StrongRef $root = null): self
132
+
{
133
+
$this->reply = [
134
+
'parent' => $parent->toArray(),
135
+
'root' => ($root ?? $parent)->toArray(),
136
+
];
137
+
138
+
return $this;
139
+
}
140
+
141
+
/**
142
+
* Set the post languages
143
+
*
144
+
* @param array $langs Array of BCP-47 language codes
145
+
*/
146
+
public function langs(array $langs): self
147
+
{
148
+
$this->langs = $langs;
149
+
150
+
return $this;
151
+
}
152
+
153
+
/**
154
+
* Set the creation timestamp
155
+
*/
156
+
public function createdAt(DateTimeInterface $date): self
157
+
{
158
+
$this->createdAt = $date;
159
+
160
+
return $this;
161
+
}
162
+
163
+
/**
164
+
* Bind to a PostRecordClient for creating the post
165
+
*/
166
+
public function for(PostRecordClient $client): self
167
+
{
168
+
$this->client = $client;
169
+
170
+
return $this;
171
+
}
172
+
173
+
/**
174
+
* Create the post (requires client binding via for() or build())
175
+
*
176
+
* @throws \RuntimeException If no client is bound
177
+
*/
178
+
public function create(): StrongRef
179
+
{
180
+
if ($this->client === null) {
181
+
throw new \RuntimeException(
182
+
'No client bound. Use ->for($client) or create via $client->bsky->post->build()'
183
+
);
184
+
}
185
+
186
+
return $this->client->create($this);
187
+
}
188
+
189
+
/**
190
+
* Convert to array for XRPC (implements Recordable)
191
+
*/
192
+
public function toArray(): array
193
+
{
194
+
$record = $this->getTextAndFacets();
195
+
196
+
if ($this->embed !== null) {
197
+
$record['embed'] = $this->embed;
198
+
}
199
+
200
+
if ($this->reply !== null) {
201
+
$record['reply'] = $this->reply;
202
+
}
203
+
204
+
if ($this->langs !== null) {
205
+
$record['langs'] = $this->langs;
206
+
}
207
+
208
+
$record['createdAt'] = ($this->createdAt ?? now())->format('c');
209
+
$record['$type'] = $this->getType();
210
+
211
+
return $record;
212
+
}
213
+
214
+
/**
215
+
* Get the record type (implements Recordable)
216
+
*/
217
+
public function getType(): string
218
+
{
219
+
return BskyFeed::Post->value;
220
+
}
221
+
222
+
/**
223
+
* Normalize image data from array format
224
+
*/
225
+
protected function normalizeImageData(array $data): array
226
+
{
227
+
$image = [
228
+
'image' => $this->normalizeBlob($data['blob'] ?? $data['image']),
229
+
'alt' => $data['alt'] ?? '',
230
+
];
231
+
232
+
if (isset($data['aspectRatio'])) {
233
+
$ratio = $data['aspectRatio'];
234
+
$image['aspectRatio'] = is_array($ratio) && isset($ratio['width'])
235
+
? $ratio
236
+
: ['width' => $ratio[0], 'height' => $ratio[1]];
237
+
}
238
+
239
+
return $image;
240
+
}
241
+
242
+
/**
243
+
* Normalize blob to array format
244
+
*/
245
+
protected function normalizeBlob(mixed $blob): array
246
+
{
247
+
if (is_array($blob)) {
248
+
return $blob;
249
+
}
250
+
251
+
if (method_exists($blob, 'toArray')) {
252
+
return $blob->toArray();
253
+
}
254
+
255
+
return (array) $blob;
256
+
}
257
+
}
+9
-11
src/Client/Records/FollowRecordClient.php
+9
-11
src/Client/Records/FollowRecordClient.php
···
5
5
use DateTimeInterface;
6
6
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
7
7
use SocialDept\AtpClient\Client\Requests\Request;
8
-
use SocialDept\AtpClient\Data\StrongRef;
8
+
use SocialDept\AtpClient\Data\Record;
9
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\CreateRecordResponse;
10
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\DeleteRecordResponse;
9
11
use SocialDept\AtpClient\Enums\Nsid\BskyGraph;
10
12
use SocialDept\AtpClient\Enums\Scope;
11
13
···
21
23
public function create(
22
24
string $subject,
23
25
?DateTimeInterface $createdAt = null
24
-
): StrongRef {
26
+
): CreateRecordResponse {
25
27
$record = [
26
28
'$type' => BskyGraph::Follow->value,
27
29
'subject' => $subject, // DID
28
30
'createdAt' => ($createdAt ?? now())->format('c'),
29
31
];
30
32
31
-
$response = $this->atp->atproto->repo->createRecord(
32
-
repo: $this->atp->client->session()->did(),
33
+
return $this->atp->atproto->repo->createRecord(
33
34
collection: BskyGraph::Follow,
34
35
record: $record
35
36
);
36
-
37
-
return StrongRef::fromResponse($response->json());
38
37
}
39
38
40
39
/**
···
44
43
*/
45
44
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')]
46
45
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.graph.follow?action=delete')]
47
-
public function delete(string $rkey): void
46
+
public function delete(string $rkey): DeleteRecordResponse
48
47
{
49
-
$this->atp->atproto->repo->deleteRecord(
50
-
repo: $this->atp->client->session()->did(),
48
+
return $this->atp->atproto->repo->deleteRecord(
51
49
collection: BskyGraph::Follow,
52
50
rkey: $rkey
53
51
);
···
59
57
* @requires transition:generic (rpc:com.atproto.repo.getRecord)
60
58
*/
61
59
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')]
62
-
public function get(string $rkey, ?string $cid = null): array
60
+
public function get(string $rkey, ?string $cid = null): Record
63
61
{
64
62
$response = $this->atp->atproto->repo->getRecord(
65
63
repo: $this->atp->client->session()->did(),
···
68
66
cid: $cid
69
67
);
70
68
71
-
return $response->json('value');
69
+
return Record::fromArrayRaw($response->toArray());
72
70
}
73
71
}
+9
-10
src/Client/Records/LikeRecordClient.php
+9
-10
src/Client/Records/LikeRecordClient.php
···
5
5
use DateTimeInterface;
6
6
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
7
7
use SocialDept\AtpClient\Client\Requests\Request;
8
+
use SocialDept\AtpClient\Data\Record;
9
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\CreateRecordResponse;
10
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\DeleteRecordResponse;
8
11
use SocialDept\AtpClient\Data\StrongRef;
9
12
use SocialDept\AtpClient\Enums\Nsid\BskyFeed;
10
13
use SocialDept\AtpClient\Enums\Scope;
···
21
24
public function create(
22
25
StrongRef $subject,
23
26
?DateTimeInterface $createdAt = null
24
-
): StrongRef {
27
+
): CreateRecordResponse {
25
28
$record = [
26
29
'$type' => BskyFeed::Like->value,
27
30
'subject' => $subject->toArray(),
28
31
'createdAt' => ($createdAt ?? now())->format('c'),
29
32
];
30
33
31
-
$response = $this->atp->atproto->repo->createRecord(
32
-
repo: $this->atp->client->session()->did(),
34
+
return $this->atp->atproto->repo->createRecord(
33
35
collection: BskyFeed::Like,
34
36
record: $record
35
37
);
36
-
37
-
return StrongRef::fromResponse($response->json());
38
38
}
39
39
40
40
/**
···
44
44
*/
45
45
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')]
46
46
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.like?action=delete')]
47
-
public function delete(string $rkey): void
47
+
public function delete(string $rkey): DeleteRecordResponse
48
48
{
49
-
$this->atp->atproto->repo->deleteRecord(
50
-
repo: $this->atp->client->session()->did(),
49
+
return $this->atp->atproto->repo->deleteRecord(
51
50
collection: BskyFeed::Like,
52
51
rkey: $rkey
53
52
);
···
59
58
* @requires transition:generic (rpc:com.atproto.repo.getRecord)
60
59
*/
61
60
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')]
62
-
public function get(string $rkey, ?string $cid = null): array
61
+
public function get(string $rkey, ?string $cid = null): Record
63
62
{
64
63
$response = $this->atp->atproto->repo->getRecord(
65
64
repo: $this->atp->client->session()->did(),
···
68
67
cid: $cid
69
68
);
70
69
71
-
return $response->json('value');
70
+
return Record::fromArrayRaw($response->toArray());
72
71
}
73
72
}
+33
-151
src/Client/Records/PostRecordClient.php
+33
-151
src/Client/Records/PostRecordClient.php
···
4
4
5
5
use DateTimeInterface;
6
6
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
7
+
use SocialDept\AtpClient\Builders\PostBuilder;
7
8
use SocialDept\AtpClient\Client\Requests\Request;
8
9
use SocialDept\AtpClient\Contracts\Recordable;
10
+
use SocialDept\AtpClient\Data\Record;
11
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\CreateRecordResponse;
12
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\DeleteRecordResponse;
13
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\PutRecordResponse;
9
14
use SocialDept\AtpClient\Data\StrongRef;
10
15
use SocialDept\AtpClient\Enums\Nsid\BskyFeed;
11
16
use SocialDept\AtpClient\Enums\Scope;
···
15
20
class PostRecordClient extends Request
16
21
{
17
22
/**
23
+
* Create a new post builder bound to this client
24
+
*/
25
+
public function build(): PostBuilder
26
+
{
27
+
return PostBuilder::make()->for($this);
28
+
}
29
+
30
+
/**
18
31
* Create a post
19
32
*
20
33
* @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create)
···
28
41
?array $reply = null,
29
42
?array $langs = null,
30
43
?DateTimeInterface $createdAt = null
31
-
): StrongRef {
44
+
): CreateRecordResponse {
32
45
// Handle different input types
33
46
if (is_string($content)) {
34
47
$record = [
···
41
54
$record = $content;
42
55
}
43
56
44
-
// Add optional fields
45
-
if ($embed) {
46
-
$record['embed'] = $embed;
47
-
}
48
-
if ($reply) {
49
-
$record['reply'] = $reply;
50
-
}
51
-
if ($langs) {
52
-
$record['langs'] = $langs;
57
+
// Add optional fields (only for non-Recordable inputs)
58
+
if (! ($content instanceof Recordable)) {
59
+
if ($embed) {
60
+
$record['embed'] = $embed;
61
+
}
62
+
if ($reply) {
63
+
$record['reply'] = $reply;
64
+
}
65
+
if ($langs) {
66
+
$record['langs'] = $langs;
67
+
}
53
68
}
69
+
54
70
if (! isset($record['createdAt'])) {
55
71
$record['createdAt'] = ($createdAt ?? now())->format('c');
56
72
}
···
60
76
$record['$type'] = BskyFeed::Post->value;
61
77
}
62
78
63
-
$response = $this->atp->atproto->repo->createRecord(
64
-
repo: $this->atp->client->session()->did(),
79
+
return $this->atp->atproto->repo->createRecord(
65
80
collection: BskyFeed::Post,
66
81
record: $record
67
82
);
68
-
69
-
return StrongRef::fromResponse($response->json());
70
83
}
71
84
72
85
/**
···
76
89
*/
77
90
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
78
91
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=update')]
79
-
public function update(string $rkey, array $record): StrongRef
92
+
public function update(string $rkey, array $record): PutRecordResponse
80
93
{
81
94
// Ensure $type is set
82
95
if (! isset($record['$type'])) {
83
96
$record['$type'] = BskyFeed::Post->value;
84
97
}
85
98
86
-
$response = $this->atp->atproto->repo->putRecord(
87
-
repo: $this->atp->client->session()->did(),
99
+
return $this->atp->atproto->repo->putRecord(
88
100
collection: BskyFeed::Post,
89
101
rkey: $rkey,
90
102
record: $record
91
103
);
92
-
93
-
return StrongRef::fromResponse($response->toArray());
94
104
}
95
105
96
106
/**
···
100
110
*/
101
111
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.deleteRecord')]
102
112
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=delete')]
103
-
public function delete(string $rkey): void
113
+
public function delete(string $rkey): DeleteRecordResponse
104
114
{
105
-
$this->atp->atproto->repo->deleteRecord(
106
-
repo: $this->atp->client->session()->did(),
115
+
return $this->atp->atproto->repo->deleteRecord(
107
116
collection: BskyFeed::Post,
108
117
rkey: $rkey
109
118
);
···
115
124
* @requires transition:generic (rpc:com.atproto.repo.getRecord)
116
125
*/
117
126
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')]
118
-
public function get(string $rkey, ?string $cid = null): PostView
127
+
public function get(string $rkey, ?string $cid = null): Record
119
128
{
120
129
$response = $this->atp->atproto->repo->getRecord(
121
130
repo: $this->atp->client->session()->did(),
···
124
133
cid: $cid
125
134
);
126
135
127
-
return PostView::fromArray($response->value);
128
-
}
129
-
130
-
/**
131
-
* Create a reply to another post
132
-
*
133
-
* @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create)
134
-
*/
135
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')]
136
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')]
137
-
public function reply(
138
-
StrongRef $parent,
139
-
StrongRef $root,
140
-
string|array|Recordable $content,
141
-
?array $facets = null,
142
-
?array $embed = null,
143
-
?array $langs = null,
144
-
?DateTimeInterface $createdAt = null
145
-
): StrongRef {
146
-
$reply = [
147
-
'parent' => $parent->toArray(),
148
-
'root' => $root->toArray(),
149
-
];
150
-
151
-
return $this->create(
152
-
content: $content,
153
-
facets: $facets,
154
-
embed: $embed,
155
-
reply: $reply,
156
-
langs: $langs,
157
-
createdAt: $createdAt
158
-
);
159
-
}
160
-
161
-
/**
162
-
* Create a quote post (post with embedded post)
163
-
*
164
-
* @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create)
165
-
*/
166
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')]
167
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')]
168
-
public function quote(
169
-
StrongRef $quotedPost,
170
-
string|array|Recordable $content,
171
-
?array $facets = null,
172
-
?array $langs = null,
173
-
?DateTimeInterface $createdAt = null
174
-
): StrongRef {
175
-
$embed = [
176
-
'$type' => 'app.bsky.embed.record',
177
-
'record' => $quotedPost->toArray(),
178
-
];
179
-
180
-
return $this->create(
181
-
content: $content,
182
-
facets: $facets,
183
-
embed: $embed,
184
-
langs: $langs,
185
-
createdAt: $createdAt
186
-
);
187
-
}
188
-
189
-
/**
190
-
* Create a post with images
191
-
*
192
-
* @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create)
193
-
*/
194
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')]
195
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')]
196
-
public function withImages(
197
-
string|array|Recordable $content,
198
-
array $images,
199
-
?array $facets = null,
200
-
?array $langs = null,
201
-
?DateTimeInterface $createdAt = null
202
-
): StrongRef {
203
-
$embed = [
204
-
'$type' => 'app.bsky.embed.images',
205
-
'images' => $images,
206
-
];
207
-
208
-
return $this->create(
209
-
content: $content,
210
-
facets: $facets,
211
-
embed: $embed,
212
-
langs: $langs,
213
-
createdAt: $createdAt
214
-
);
136
+
return Record::fromArrayRaw($response->toArray());
215
137
}
216
138
217
-
/**
218
-
* Create a post with external link embed
219
-
*
220
-
* @requires transition:generic OR (rpc:com.atproto.repo.createRecord AND repo:app.bsky.feed.post?action=create)
221
-
*/
222
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.createRecord')]
223
-
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.feed.post?action=create')]
224
-
public function withLink(
225
-
string|array|Recordable $content,
226
-
string $uri,
227
-
string $title,
228
-
string $description,
229
-
?string $thumbBlob = null,
230
-
?array $facets = null,
231
-
?array $langs = null,
232
-
?DateTimeInterface $createdAt = null
233
-
): StrongRef {
234
-
$external = [
235
-
'uri' => $uri,
236
-
'title' => $title,
237
-
'description' => $description,
238
-
];
239
-
240
-
if ($thumbBlob) {
241
-
$external['thumb'] = $thumbBlob;
242
-
}
243
-
244
-
$embed = [
245
-
'$type' => 'app.bsky.embed.external',
246
-
'external' => $external,
247
-
];
248
-
249
-
return $this->create(
250
-
content: $content,
251
-
facets: $facets,
252
-
embed: $embed,
253
-
langs: $langs,
254
-
createdAt: $createdAt
255
-
);
256
-
}
257
139
}
+11
-13
src/Client/Records/ProfileRecordClient.php
+11
-13
src/Client/Records/ProfileRecordClient.php
···
4
4
5
5
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
6
6
use SocialDept\AtpClient\Client\Requests\Request;
7
-
use SocialDept\AtpClient\Data\StrongRef;
7
+
use SocialDept\AtpClient\Data\Record;
8
+
use SocialDept\AtpClient\Data\Responses\Atproto\Repo\PutRecordResponse;
8
9
use SocialDept\AtpClient\Enums\Nsid\BskyActor;
9
10
use SocialDept\AtpClient\Enums\Scope;
10
11
···
17
18
*/
18
19
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
19
20
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')]
20
-
public function update(array $profile): StrongRef
21
+
public function update(array $profile): PutRecordResponse
21
22
{
22
23
// Ensure $type is set
23
24
if (! isset($profile['$type'])) {
24
25
$profile['$type'] = BskyActor::Profile->value;
25
26
}
26
27
27
-
$response = $this->atp->atproto->repo->putRecord(
28
-
repo: $this->atp->client->session()->did(),
28
+
return $this->atp->atproto->repo->putRecord(
29
29
collection: BskyActor::Profile,
30
30
rkey: 'self', // Profile records always use 'self' as rkey
31
31
record: $profile
32
32
);
33
-
34
-
return StrongRef::fromResponse($response->json());
35
33
}
36
34
37
35
/**
···
40
38
* @requires transition:generic (rpc:com.atproto.repo.getRecord)
41
39
*/
42
40
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.getRecord')]
43
-
public function get(): array
41
+
public function get(): Record
44
42
{
45
43
$response = $this->atp->atproto->repo->getRecord(
46
44
repo: $this->atp->client->session()->did(),
···
48
46
rkey: 'self'
49
47
);
50
48
51
-
return $response->json('value');
49
+
return Record::fromArrayRaw($response->toArray());
52
50
}
53
51
54
52
/**
···
58
56
*/
59
57
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
60
58
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')]
61
-
public function updateDisplayName(string $displayName): StrongRef
59
+
public function updateDisplayName(string $displayName): PutRecordResponse
62
60
{
63
61
$profile = $this->getOrCreateProfile();
64
62
$profile['displayName'] = $displayName;
···
73
71
*/
74
72
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
75
73
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')]
76
-
public function updateDescription(string $description): StrongRef
74
+
public function updateDescription(string $description): PutRecordResponse
77
75
{
78
76
$profile = $this->getOrCreateProfile();
79
77
$profile['description'] = $description;
···
88
86
*/
89
87
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
90
88
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')]
91
-
public function updateAvatar(array $avatarBlob): StrongRef
89
+
public function updateAvatar(array $avatarBlob): PutRecordResponse
92
90
{
93
91
$profile = $this->getOrCreateProfile();
94
92
$profile['avatar'] = $avatarBlob;
···
103
101
*/
104
102
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:com.atproto.repo.putRecord')]
105
103
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'repo:app.bsky.actor.profile?action=update')]
106
-
public function updateBanner(array $bannerBlob): StrongRef
104
+
public function updateBanner(array $bannerBlob): PutRecordResponse
107
105
{
108
106
$profile = $this->getOrCreateProfile();
109
107
$profile['banner'] = $bannerBlob;
···
117
115
protected function getOrCreateProfile(): array
118
116
{
119
117
try {
120
-
return $this->get();
118
+
return $this->get()->value;
121
119
} catch (\Exception $e) {
122
120
// Profile doesn't exist, return empty structure
123
121
return [
+7
-3
src/Client/Requests/Atproto/IdentityRequestClient.php
+7
-3
src/Client/Requests/Atproto/IdentityRequestClient.php
···
5
5
use SocialDept\AtpClient\Attributes\PublicEndpoint;
6
6
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
7
7
use SocialDept\AtpClient\Client\Requests\Request;
8
+
use SocialDept\AtpClient\Data\Responses\Atproto\Identity\ResolveHandleResponse;
9
+
use SocialDept\AtpClient\Data\Responses\EmptyResponse;
8
10
use SocialDept\AtpClient\Enums\Nsid\AtprotoIdentity;
9
11
use SocialDept\AtpClient\Enums\Scope;
10
12
···
16
18
* @see https://docs.bsky.app/docs/api/com-atproto-identity-resolve-handle
17
19
*/
18
20
#[PublicEndpoint]
19
-
public function resolveHandle(string $handle): string
21
+
public function resolveHandle(string $handle): ResolveHandleResponse
20
22
{
21
23
$response = $this->atp->client->get(
22
24
endpoint: AtprotoIdentity::ResolveHandle,
23
25
params: compact('handle')
24
26
);
25
27
26
-
return $response->json()['did'];
28
+
return ResolveHandleResponse::fromArray($response->json());
27
29
}
28
30
29
31
/**
···
34
36
* @see https://docs.bsky.app/docs/api/com-atproto-identity-update-handle
35
37
*/
36
38
#[ScopedEndpoint(Scope::Atproto, granular: 'identity:handle')]
37
-
public function updateHandle(string $handle): void
39
+
public function updateHandle(string $handle): EmptyResponse
38
40
{
39
41
$this->atp->client->post(
40
42
endpoint: AtprotoIdentity::UpdateHandle,
41
43
body: compact('handle')
42
44
);
45
+
46
+
return new EmptyResponse;
43
47
}
44
48
}
+3
-3
src/Client/Requests/Atproto/RepoRequestClient.php
+3
-3
src/Client/Requests/Atproto/RepoRequestClient.php
···
32
32
*/
33
33
#[ScopedEndpoint(Scope::TransitionGeneric, description: 'Create records in repository')]
34
34
public function createRecord(
35
-
string $repo,
36
35
string|BackedEnum $collection,
37
36
array $record,
38
37
?string $rkey = null,
39
38
bool $validate = true,
40
39
?string $swapCommit = null
41
40
): CreateRecordResponse {
41
+
$repo = $this->atp->client->session()->did();
42
42
$collection = $collection instanceof BackedEnum ? $collection->value : $collection;
43
43
$this->checkCollectionScope($collection, 'create');
44
44
···
62
62
*/
63
63
#[ScopedEndpoint(Scope::TransitionGeneric, description: 'Delete records from repository')]
64
64
public function deleteRecord(
65
-
string $repo,
66
65
string|BackedEnum $collection,
67
66
string $rkey,
68
67
?string $swapRecord = null,
69
68
?string $swapCommit = null
70
69
): DeleteRecordResponse {
70
+
$repo = $this->atp->client->session()->did();
71
71
$collection = $collection instanceof BackedEnum ? $collection->value : $collection;
72
72
$this->checkCollectionScope($collection, 'delete');
73
73
···
91
91
*/
92
92
#[ScopedEndpoint(Scope::TransitionGeneric, description: 'Update records in repository')]
93
93
public function putRecord(
94
-
string $repo,
95
94
string|BackedEnum $collection,
96
95
string $rkey,
97
96
array $record,
···
99
98
?string $swapRecord = null,
100
99
?string $swapCommit = null
101
100
): PutRecordResponse {
101
+
$repo = $this->atp->client->session()->did();
102
102
$collection = $collection instanceof BackedEnum ? $collection->value : $collection;
103
103
$this->checkCollectionScope($collection, 'update');
104
104
+1
-1
src/Client/Requests/Bsky/ActorRequestClient.php
+1
-1
src/Client/Requests/Bsky/ActorRequestClient.php
+4
-1
src/Client/Requests/Chat/ActorRequestClient.php
+4
-1
src/Client/Requests/Chat/ActorRequestClient.php
···
4
4
5
5
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
6
6
use SocialDept\AtpClient\Client\Requests\Request;
7
+
use SocialDept\AtpClient\Data\Responses\EmptyResponse;
7
8
use SocialDept\AtpClient\Enums\Nsid\ChatActor;
8
9
use SocialDept\AtpClient\Enums\Scope;
9
10
use SocialDept\AtpClient\Http\Response;
···
48
49
* @see https://docs.bsky.app/docs/api/chat-bsky-actor-delete-account
49
50
*/
50
51
#[ScopedEndpoint(Scope::TransitionChat, granular: 'rpc:chat.bsky.actor.deleteAccount')]
51
-
public function deleteAccount(): void
52
+
public function deleteAccount(): EmptyResponse
52
53
{
53
54
$this->atp->client->post(
54
55
endpoint: ChatActor::DeleteAccount
55
56
);
57
+
58
+
return new EmptyResponse;
56
59
}
57
60
}
+11
-13
src/Client/Requests/Ozone/TeamRequestClient.php
+11
-13
src/Client/Requests/Ozone/TeamRequestClient.php
···
4
4
5
5
use SocialDept\AtpClient\Attributes\ScopedEndpoint;
6
6
use SocialDept\AtpClient\Client\Requests\Request;
7
+
use SocialDept\AtpClient\Data\Responses\EmptyResponse;
7
8
use SocialDept\AtpClient\Data\Responses\Ozone\Team\ListMembersResponse;
9
+
use SocialDept\AtpClient\Data\Responses\Ozone\Team\MemberResponse;
8
10
use SocialDept\AtpClient\Enums\Nsid\OzoneTeam;
9
11
use SocialDept\AtpClient\Enums\Scope;
10
12
···
15
17
*
16
18
* @requires transition:generic (rpc:tools.ozone.team.getMember)
17
19
*
18
-
* @return array<string, mixed> Team member object
19
-
*
20
20
* @see https://docs.bsky.app/docs/api/tools-ozone-team-list-members
21
21
*/
22
22
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.getMember')]
23
-
public function getTeamMember(string $did): array
23
+
public function getTeamMember(string $did): MemberResponse
24
24
{
25
25
$response = $this->atp->client->get(
26
26
endpoint: OzoneTeam::GetMember,
27
27
params: compact('did')
28
28
);
29
29
30
-
return $response->json();
30
+
return MemberResponse::fromArray($response->json());
31
31
}
32
32
33
33
/**
···
53
53
*
54
54
* @requires transition:generic (rpc:tools.ozone.team.addMember)
55
55
*
56
-
* @return array<string, mixed> Team member object
57
-
*
58
56
* @see https://docs.bsky.app/docs/api/tools-ozone-team-add-member
59
57
*/
60
58
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.addMember')]
61
-
public function addTeamMember(string $did, string $role): array
59
+
public function addTeamMember(string $did, string $role): MemberResponse
62
60
{
63
61
$response = $this->atp->client->post(
64
62
endpoint: OzoneTeam::AddMember,
65
63
body: compact('did', 'role')
66
64
);
67
65
68
-
return $response->json();
66
+
return MemberResponse::fromArray($response->json());
69
67
}
70
68
71
69
/**
···
73
71
*
74
72
* @requires transition:generic (rpc:tools.ozone.team.updateMember)
75
73
*
76
-
* @return array<string, mixed> Team member object
77
-
*
78
74
* @see https://docs.bsky.app/docs/api/tools-ozone-team-update-member
79
75
*/
80
76
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.updateMember')]
···
82
78
string $did,
83
79
?bool $disabled = null,
84
80
?string $role = null
85
-
): array {
81
+
): MemberResponse {
86
82
$response = $this->atp->client->post(
87
83
endpoint: OzoneTeam::UpdateMember,
88
84
body: array_filter(
···
91
87
)
92
88
);
93
89
94
-
return $response->json();
90
+
return MemberResponse::fromArray($response->json());
95
91
}
96
92
97
93
/**
···
102
98
* @see https://docs.bsky.app/docs/api/tools-ozone-team-delete-member
103
99
*/
104
100
#[ScopedEndpoint(Scope::TransitionGeneric, granular: 'rpc:tools.ozone.team.deleteMember')]
105
-
public function deleteTeamMember(string $did): void
101
+
public function deleteTeamMember(string $did): EmptyResponse
106
102
{
107
103
$this->atp->client->post(
108
104
endpoint: OzoneTeam::DeleteMember,
109
105
body: compact('did')
110
106
);
107
+
108
+
return new EmptyResponse;
111
109
}
112
110
}
+29
src/Data/Responses/Atproto/Identity/ResolveHandleResponse.php
+29
src/Data/Responses/Atproto/Identity/ResolveHandleResponse.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpClient\Data\Responses\Atproto\Identity;
4
+
5
+
use Illuminate\Contracts\Support\Arrayable;
6
+
7
+
/**
8
+
* @implements Arrayable<string, string>
9
+
*/
10
+
class ResolveHandleResponse implements Arrayable
11
+
{
12
+
public function __construct(
13
+
public readonly string $did,
14
+
) {}
15
+
16
+
public static function fromArray(array $data): self
17
+
{
18
+
return new self(
19
+
did: $data['did'],
20
+
);
21
+
}
22
+
23
+
public function toArray(): array
24
+
{
25
+
return [
26
+
'did' => $this->did,
27
+
];
28
+
}
29
+
}
+25
src/Data/Responses/EmptyResponse.php
+25
src/Data/Responses/EmptyResponse.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpClient\Data\Responses;
4
+
5
+
use Illuminate\Contracts\Support\Arrayable;
6
+
7
+
/**
8
+
* Response class for endpoints that return empty objects.
9
+
*
10
+
* @implements Arrayable<string, mixed>
11
+
*/
12
+
class EmptyResponse implements Arrayable
13
+
{
14
+
public function __construct() {}
15
+
16
+
public static function fromArray(array $data): self
17
+
{
18
+
return new self;
19
+
}
20
+
21
+
public function toArray(): array
22
+
{
23
+
return [];
24
+
}
25
+
}
+44
src/Data/Responses/Ozone/Team/MemberResponse.php
+44
src/Data/Responses/Ozone/Team/MemberResponse.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpClient\Data\Responses\Ozone\Team;
4
+
5
+
use Illuminate\Contracts\Support\Arrayable;
6
+
7
+
/**
8
+
* @implements Arrayable<string, mixed>
9
+
*/
10
+
class MemberResponse implements Arrayable
11
+
{
12
+
public function __construct(
13
+
public readonly string $did,
14
+
public readonly bool $disabled,
15
+
public readonly ?string $role = null,
16
+
public readonly ?string $createdAt = null,
17
+
public readonly ?string $updatedAt = null,
18
+
public readonly ?string $lastUpdatedBy = null,
19
+
) {}
20
+
21
+
public static function fromArray(array $data): self
22
+
{
23
+
return new self(
24
+
did: $data['did'],
25
+
disabled: $data['disabled'] ?? false,
26
+
role: $data['role'] ?? null,
27
+
createdAt: $data['createdAt'] ?? null,
28
+
updatedAt: $data['updatedAt'] ?? null,
29
+
lastUpdatedBy: $data['lastUpdatedBy'] ?? null,
30
+
);
31
+
}
32
+
33
+
public function toArray(): array
34
+
{
35
+
return array_filter([
36
+
'did' => $this->did,
37
+
'disabled' => $this->disabled,
38
+
'role' => $this->role,
39
+
'createdAt' => $this->createdAt,
40
+
'updatedAt' => $this->updatedAt,
41
+
'lastUpdatedBy' => $this->lastUpdatedBy,
42
+
], fn ($v) => $v !== null);
43
+
}
44
+
}