Skip to content

Commit d045469

Browse files
authored
Merge pull request #45 from shahmal1yev/bss-101
[BSS-101] Implement GetTimeline
2 parents 237ad74 + 7bb3c9f commit d045469

File tree

10 files changed

+347
-2
lines changed

10 files changed

+347
-2
lines changed

.github/workflows/php.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: PHP Workflow
22

33
on:
44
push:
5-
branches: [ "main" ]
5+
branches: [ "1.x" ]
66
pull_request:
7-
branches: [ "main" ]
7+
branches: [ "1.x" ]
88

99
permissions:
1010
contents: read

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,54 @@ foreach ($response->posts() as $post) {
230230
}
231231
```
232232

233+
### GetTimeline
234+
235+
Get a view of the requesting account's home timeline.
236+
237+
```php
238+
$feed = $client->app()->bsky()->feed()->getTimeline()->forge()
239+
->limit(10)
240+
->send()
241+
->feed();
242+
243+
foreach($feed as $entry) {
244+
echo sprintf("Created by %s at %s" . PHP_EOL,
245+
$entry->post()->author()->handle(),
246+
$entry->post()->indexedAt()->format('d/m/Y H:i:s')
247+
);
248+
}
249+
```
250+
251+
### Call to non-implemented lexicons
252+
253+
_Note that this is not recommended._
254+
255+
It is possible to call non-implemented lexicons.
256+
257+
```php
258+
use Atproto\Client;
259+
use Atproto\Lexicons\Request;
260+
use Atproto\Support\Arr;
261+
262+
$client = new Client();
263+
$client->authenticate(getenv('BLUESKY_IDENTIFIER'), getenv('BLUESKY_PASSWORD'));
264+
265+
$request = new Request();
266+
$request->origin('https://bsky.social/xrpc/app.bsky.feed.getTimeline')
267+
->method('GET')
268+
->headers([
269+
'Accept' => 'application/json',
270+
'Content-Type' => 'application/json',
271+
'Authorization' => sprintf("Bearer %s", $client->authenticated()->accessJwt())
272+
]);
273+
274+
/** @var array $response */
275+
$response = $request->send();
276+
277+
echo Arr::get($response, 'cursor') . PHP_EOL;
278+
print_r($response);
279+
```
280+
233281
### Serialization
234282

235283
Any lexicon can be serialized
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Atproto\Lexicons\App\Bsky\Feed;
4+
5+
use Atproto\Client;
6+
use Atproto\Contracts\LexiconContract;
7+
use Atproto\Contracts\Lexicons\RequestContract;
8+
use Atproto\Contracts\Resources\ResponseContract;
9+
use Atproto\Exceptions\InvalidArgumentException;
10+
use Atproto\Lexicons\APIRequest;
11+
use Atproto\Lexicons\Traits\AuthenticatedEndpoint;
12+
use Atproto\Responses\App\Bsky\Feed\GetTimelineResponse;
13+
14+
class GetTimeline extends APIRequest implements LexiconContract
15+
{
16+
use AuthenticatedEndpoint;
17+
18+
public function __construct(Client $client)
19+
{
20+
parent::__construct($client);
21+
$this->update($client);
22+
23+
$this->limit(50);
24+
}
25+
26+
public function algorithm($algorithm = null)
27+
{
28+
if (is_null($algorithm)) {
29+
return $this->queryParameter('algorithm');
30+
}
31+
32+
$this->queryParameter('algorithm', $algorithm);
33+
34+
return $this;
35+
}
36+
37+
/**
38+
* @throws InvalidArgumentException
39+
*/
40+
public function limit(int $limit = null)
41+
{
42+
if (is_null($limit)) {
43+
return (int) $this->queryParameter('limit');
44+
}
45+
46+
if ($limit < 1 || $limit > 100) {
47+
throw new InvalidArgumentException("The limit must be between or equal to 1 and 100.");
48+
}
49+
50+
$this->queryParameter('limit', $limit);
51+
52+
return $this;
53+
}
54+
55+
public function cursor(string $cursor = null)
56+
{
57+
if (is_null($cursor)) {
58+
return $this->queryParameter('cursor');
59+
}
60+
61+
$this->queryParameter('cursor', $cursor);
62+
63+
return $this;
64+
}
65+
66+
public function response(array $data): ResponseContract
67+
{
68+
return new GetTimelineResponse($data);
69+
}
70+
71+
public function build(): RequestContract
72+
{
73+
return $this;
74+
}
75+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Atproto\Responses\App\Bsky\Feed;
4+
5+
use Atproto\Contracts\Resources\ResponseContract;
6+
use Atproto\Responses\BaseResponse;
7+
use Atproto\Responses\Objects\FeedObject;
8+
use Atproto\Traits\Castable;
9+
10+
class GetTimelineResponse implements ResponseContract
11+
{
12+
use BaseResponse;
13+
use Castable;
14+
15+
16+
protected function casts(): array
17+
{
18+
return [
19+
'feed' => FeedObject::class
20+
];
21+
}
22+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Atproto\Responses\Objects;
4+
5+
use Atproto\Contracts\Resources\ObjectContract;
6+
use Atproto\Traits\Castable;
7+
8+
class FeedItemObject implements ObjectContract
9+
{
10+
use BaseObject;
11+
use Castable;
12+
13+
public function __construct(array $content)
14+
{
15+
$this->content = $content;
16+
}
17+
18+
protected function casts(): array
19+
{
20+
return [
21+
'post' => PostObject::class,
22+
'reply' => ReplyObject::class,
23+
'reason' => ReasonObject::class,
24+
];
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Atproto\Responses\Objects;
4+
5+
use Atproto\Contracts\Resources\ObjectContract;
6+
use Closure;
7+
use GenericCollection\Exceptions\InvalidArgumentException;
8+
use GenericCollection\GenericCollection;
9+
10+
class FeedObject extends GenericCollection implements ObjectContract
11+
{
12+
use CollectionObject;
13+
14+
/**
15+
* @throws InvalidArgumentException
16+
*/
17+
protected function item($data): ObjectContract
18+
{
19+
return new FeedItemObject($data);
20+
}
21+
22+
protected function type(): Closure
23+
{
24+
return fn ($value): bool => $value instanceof FeedItemObject;
25+
}
26+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Atproto\Responses\Objects;
4+
5+
use Atproto\Contracts\Resources\ObjectContract;
6+
use Atproto\Traits\Castable;
7+
8+
class ReasonObject implements ObjectContract
9+
{
10+
use BaseObject;
11+
use Castable;
12+
13+
14+
protected function casts(): array
15+
{
16+
return [
17+
'by' => AuthorObject::class,
18+
'indexedAt' => DatetimeObject::class,
19+
];
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Atproto\Responses\Objects;
4+
5+
use Atproto\Contracts\Resources\ObjectContract;
6+
use Atproto\Traits\Castable;
7+
8+
class ReplyObject implements ObjectContract
9+
{
10+
use BaseObject;
11+
use Castable;
12+
13+
protected function casts(): array
14+
{
15+
return [
16+
'grandparentAuthor' => AuthorObject::class,
17+
'root' => PostObject::class,
18+
'parent' => PostObject::class,
19+
];
20+
}
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Tests\Feature\Lexicons\App\Bsky\Feed;
4+
5+
use Atproto\Client;
6+
use Atproto\Responses\Objects\PostObject;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class GetTimelineTest extends TestCase
10+
{
11+
private static Client $client;
12+
13+
public static function setUpBeforeClass(): void
14+
{
15+
static::$client = new Client();
16+
17+
static::$client->authenticate(getenv('BLUESKY_IDENTIFIER'), getenv('BLUESKY_PASSWORD'));
18+
}
19+
20+
public function testGetTimeline(): void
21+
{
22+
$client = static::$client;
23+
24+
$getTimeline = $client->app()->bsky()->feed()->getTimeline()->forge()
25+
->limit(20);
26+
27+
$response = $getTimeline->send();
28+
29+
$this->assertNotEmpty($feed = $response->feed());
30+
31+
foreach($feed as $entry) {
32+
$this->assertInstanceOf(PostObject::class, $post = $entry->post());
33+
$this->assertSame($client->authenticated()->did(), $post->author()->did());
34+
}
35+
}
36+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Tests\Unit\Lexicons\App\Bsky\Feed;
4+
5+
use Atproto\Client;
6+
use Atproto\Exceptions\InvalidArgumentException;
7+
use Atproto\Lexicons\App\Bsky\Feed\GetTimeline;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class GetTimelineTest extends TestCase
11+
{
12+
private static GetTimeline $instance;
13+
14+
public static function setUpBeforeClass(): void
15+
{
16+
static::$instance = (new Client())->app()->bsky()->feed()->getTimeline()->forge();
17+
18+
}
19+
20+
public function testLimitCanChangeTheLimit(): void
21+
{
22+
$getTimeline = static::$instance;
23+
24+
$this->assertSame(50, $getTimeline->limit()); // default value
25+
26+
$getTimeline->limit($expected = 10);
27+
$actual = $getTimeline->limit();
28+
29+
$this->assertSame($expected, $actual);
30+
}
31+
32+
/** @dataProvider provideInvalidLimitCases */
33+
public function testLimitThrowsExceptionWhenPassedInvalidArgument(int $invalidLimit): void
34+
{
35+
$this->expectException(InvalidArgumentException::class);
36+
$this->expectExceptionMessage("The limit must be between or equal to 1 and 100.");
37+
38+
static::$instance->limit($invalidLimit);
39+
}
40+
41+
public static function provideInvalidLimitCases(): array
42+
{
43+
return [
44+
[0],
45+
[-1],
46+
[101],
47+
[102]
48+
];
49+
}
50+
51+
public function testCursorCanChangeTheCursor(): void
52+
{
53+
$this->assertNull(static::$instance->cursor()); // it is not available by default
54+
55+
static::$instance->cursor($expected = 'cursor value');
56+
$actual = static::$instance->cursor();
57+
58+
$this->assertSame($expected, $actual);
59+
}
60+
61+
public function testAlgorithmCanChangeTheAlgorithm(): void
62+
{
63+
$this->assertNull(static::$instance->algorithm());
64+
65+
static::$instance->algorithm($expected = 'algorithm value');
66+
$actual = static::$instance->algorithm();
67+
68+
$this->assertSame($expected, $actual);
69+
}
70+
}

0 commit comments

Comments
 (0)