Skip to content

Commit 48ed027

Browse files
authored
show boosts of followed users (#2054)
1 parent 659f376 commit 48ed027

File tree

18 files changed

+581
-17
lines changed

18 files changed

+581
-17
lines changed

config/mbin_routes/combined_api.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ api_combined:
66

77
api_combined_user:
88
controller: App\Controller\Api\Combined\CombinedRetrieveApi::userCollection
9-
path: /api/combined/{contentType}
9+
path: /api/combined/{collectionType}
1010
requirements:
11-
contentType: subscribed|moderated|favourited
12-
methods: [ PUT ]
11+
collectionType: subscribed|moderated|favourited
12+
methods: [ GET ]
1313
format: json

config/mbin_routes/post_api.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ api_posts_subscribed:
44
methods: [ GET ]
55
format: json
66

7+
api_posts_subscribed_with_boost:
8+
controller: App\Controller\Api\Post\PostsRetrieveApi::subscribedWithBoosts
9+
path: /api/posts/subscribedWithBoosts
10+
methods: [ GET ]
11+
format: json
12+
713
api_posts_moderated:
814
controller: App\Controller\Api\Post\PostsRetrieveApi::moderated
915
path: /api/posts/moderated
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20260315190023 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'adds show_boosts_of_following';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
$this->addSql('ALTER TABLE "user" ADD show_boosts_of_following BOOLEAN DEFAULT false NOT NULL');
20+
}
21+
22+
public function down(Schema $schema): void
23+
{
24+
$this->addSql('ALTER TABLE "user" DROP show_boosts_of_following');
25+
}
26+
}

src/Controller/Api/Combined/CombinedRetrieveApi.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use App\Controller\Traits\PrivateContentTrait;
99
use App\DTO\ContentResponseDto;
1010
use App\Entity\Entry;
11+
use App\Entity\EntryComment;
1112
use App\Entity\Post;
13+
use App\Entity\PostComment;
1214
use App\Entity\User;
1315
use App\PageView\ContentPageView;
1416
use App\Repository\ContentRepository;
@@ -114,19 +116,27 @@ class CombinedRetrieveApi extends BaseApi
114116
in: 'query',
115117
schema: new OA\Schema(type: 'string', default: Criteria::AP_ALL, enum: Criteria::AP_OPTIONS)
116118
)]
119+
#[OA\Parameter(
120+
name: 'includeBoosts',
121+
description: 'if true then boosted content from followed users are included',
122+
in: 'query',
123+
schema: new OA\Schema(type: 'boolean', default: false)
124+
)]
117125
#[OA\Tag(name: 'combined')]
118126
public function collection(
119127
RateLimiterFactoryInterface $apiReadLimiter,
120128
RateLimiterFactoryInterface $anonymousApiReadLimiter,
121129
Security $security,
122130
ContentRepository $contentRepository,
131+
SqlHelpers $sqlHelpers,
123132
#[MapQueryParameter] ?int $p,
124133
#[MapQueryParameter] ?int $perPage,
125134
#[MapQueryParameter] ?string $sort,
126135
#[MapQueryParameter] ?string $time,
127136
#[MapQueryParameter] ?string $federation,
137+
#[MapQueryParameter] ?bool $includeBoosts,
128138
): JsonResponse {
129-
return $this->generateResponse($apiReadLimiter, $anonymousApiReadLimiter, $p, $security, $sort, $time, $federation, $perPage, $contentRepository);
139+
return $this->generateResponse($apiReadLimiter, $anonymousApiReadLimiter, $p, $security, $sort, $time, $federation, $includeBoosts, $perPage, $contentRepository, $sqlHelpers);
130140
}
131141

132142
#[OA\Response(
@@ -167,6 +177,12 @@ public function collection(
167177
],
168178
content: new OA\JsonContent(ref: new Model(type: TooManyRequestsErrorSchema::class))
169179
)]
180+
#[OA\Parameter(
181+
name: 'collectionType',
182+
description: 'the type of collection to get',
183+
in: 'path',
184+
schema: new OA\Schema(type: 'string', enum: ['subscribed', 'moderated', 'favourited'])
185+
)]
170186
#[OA\Parameter(
171187
name: 'p',
172188
description: 'Page of content to retrieve',
@@ -214,6 +230,12 @@ public function collection(
214230
in: 'query',
215231
schema: new OA\Schema(type: 'string', default: Criteria::AP_ALL, enum: Criteria::AP_OPTIONS)
216232
)]
233+
#[OA\Parameter(
234+
name: 'includeBoosts',
235+
description: 'if true then boosted content from followed users are included',
236+
in: 'query',
237+
schema: new OA\Schema(type: 'boolean', default: false)
238+
)]
217239
#[OA\Tag(name: 'combined')]
218240
#[\Nelmio\ApiDocBundle\Attribute\Security(name: 'oauth2', scopes: ['read'])]
219241
#[IsGranted('ROLE_OAUTH2_READ')]
@@ -222,14 +244,16 @@ public function userCollection(
222244
RateLimiterFactoryInterface $anonymousApiReadLimiter,
223245
Security $security,
224246
ContentRepository $contentRepository,
247+
SqlHelpers $sqlHelpers,
225248
#[MapQueryParameter] ?int $p,
226249
#[MapQueryParameter] ?int $perPage,
227250
#[MapQueryParameter] ?string $sort,
228251
#[MapQueryParameter] ?string $time,
229252
#[MapQueryParameter] ?string $federation,
253+
#[MapQueryParameter] ?bool $includeBoosts,
230254
string $collectionType,
231255
): JsonResponse {
232-
return $this->generateResponse($apiReadLimiter, $anonymousApiReadLimiter, $p, $security, $sort, $time, $federation, $perPage, $contentRepository, $collectionType);
256+
return $this->generateResponse($apiReadLimiter, $anonymousApiReadLimiter, $p, $security, $sort, $time, $federation, $includeBoosts, $perPage, $contentRepository, $sqlHelpers, $collectionType);
233257
}
234258

235259
private function generateResponse(
@@ -240,21 +264,23 @@ private function generateResponse(
240264
?string $sort,
241265
?string $time,
242266
?string $federation,
267+
?bool $includeBoosts,
243268
?int $perPage,
244269
ContentRepository $contentRepository,
245-
?string $collectionType = null,
246270
SqlHelpers $sqlHelpers,
271+
?string $collectionType = null,
247272
): JsonResponse {
248273
$headers = $this->rateLimit($apiReadLimiter, $anonymousApiReadLimiter);
249274
$criteria = new ContentPageView($p ?? 1, $security);
250275
$criteria->sortOption = $sort ?? Criteria::SORT_HOT;
251276
$criteria->time = $criteria->resolveTime($time ?? Criteria::TIME_ALL);
252277
$criteria->setFederation($federation ?? Criteria::AP_ALL);
253278
$this->handleLanguageCriteria($criteria);
254-
$criteria->content = Criteria::CONTENT_THREADS;
279+
$criteria->content = Criteria::CONTENT_COMBINED;
255280
$criteria->perPage = $perPage;
256281
$user = $security->getUser();
257282
if ($user instanceof User) {
283+
$criteria->includeBoosts = $includeBoosts ?? $user->showBoostsOfFollowing;
258284
$criteria->fetchCachedItems($sqlHelpers, $user);
259285
}
260286

@@ -281,6 +307,12 @@ private function generateResponse(
281307
} elseif ($item instanceof Post) {
282308
$this->handlePrivateContent($item);
283309
$result[] = new ContentResponseDto(post: $this->serializePost($this->postFactory->createDto($item), $this->tagLinkRepository->getTagsOfContent($item)));
310+
} elseif ($item instanceof EntryComment) {
311+
$this->handlePrivateContent($item);
312+
$result[] = new ContentResponseDto(entryComment: $this->serializeEntryComment($this->entryCommentFactory->createDto($item), $this->tagLinkRepository->getTagsOfContent($item)));
313+
} elseif ($item instanceof PostComment) {
314+
$this->handlePrivateContent($item);
315+
$result[] = new ContentResponseDto(postComment: $this->serializePostComment($this->postCommentFactory->createDto($item), $this->tagLinkRepository->getTagsOfContent($item)));
284316
}
285317
}
286318

src/Controller/Api/Post/PostsRetrieveApi.php

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
namespace App\Controller\Api\Post;
66

77
use App\Controller\Traits\PrivateContentTrait;
8+
use App\DTO\ContentResponseDto;
89
use App\DTO\PostResponseDto;
910
use App\Entity\Magazine;
1011
use App\Entity\Post;
12+
use App\Entity\PostComment;
1113
use App\Entity\User;
1214
use App\Event\Post\PostHasBeenSeenEvent;
1315
use App\Factory\PostFactory;
@@ -308,7 +310,6 @@ public function collection(
308310
#[IsGranted('ROLE_OAUTH2_READ')]
309311
public function subscribed(
310312
ContentRepository $repository,
311-
PostFactory $factory,
312313
RateLimiterFactoryInterface $apiReadLimiter,
313314
RateLimiterFactoryInterface $anonymousApiReadLimiter,
314315
SymfonySecurity $security,
@@ -344,7 +345,155 @@ public function subscribed(
344345
try {
345346
\assert($value instanceof Post);
346347
$this->handlePrivateContent($value);
347-
array_push($dtos, $this->serializePost($factory->createDto($value), $this->tagLinkRepository->getTagsOfContent($value)));
348+
$dtos[] = $this->serializePost($this->postFactory->createDto($value), $this->tagLinkRepository->getTagsOfContent($value));
349+
} catch (\Exception $e) {
350+
continue;
351+
}
352+
}
353+
354+
return new JsonResponse(
355+
$this->serializePaginated($dtos, $posts),
356+
headers: $headers
357+
);
358+
}
359+
360+
#[OA\Response(
361+
response: 200,
362+
description: 'A paginated list of posts from user\'s subscribed magazines',
363+
content: new OA\JsonContent(
364+
type: 'object',
365+
properties: [
366+
new OA\Property(
367+
property: 'items',
368+
type: 'array',
369+
items: new OA\Items(ref: new Model(type: ContentResponseDto::class))
370+
),
371+
new OA\Property(
372+
property: 'pagination',
373+
ref: new Model(type: PaginationSchema::class)
374+
),
375+
]
376+
),
377+
headers: [
378+
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
379+
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
380+
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
381+
]
382+
)]
383+
#[OA\Response(
384+
response: 401,
385+
description: 'Permission denied due to missing or expired token',
386+
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\UnauthorizedErrorSchema::class))
387+
)]
388+
#[OA\Response(
389+
response: 429,
390+
description: 'You are being rate limited',
391+
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\TooManyRequestsErrorSchema::class)),
392+
headers: [
393+
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
394+
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
395+
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
396+
]
397+
)]
398+
#[OA\Parameter(
399+
name: 'p',
400+
description: 'Page of posts to retrieve',
401+
in: 'query',
402+
schema: new OA\Schema(type: 'integer', default: 1, minimum: 1)
403+
)]
404+
#[OA\Parameter(
405+
name: 'perPage',
406+
description: 'Number of posts to retrieve per page',
407+
in: 'query',
408+
schema: new OA\Schema(type: 'integer', default: PostRepository::PER_PAGE, minimum: self::MIN_PER_PAGE, maximum: self::MAX_PER_PAGE)
409+
)]
410+
#[OA\Parameter(
411+
name: 'sort',
412+
description: 'Sort method to use when retrieving posts',
413+
in: 'query',
414+
schema: new OA\Schema(type: 'string', default: Criteria::SORT_HOT, enum: Criteria::SORT_OPTIONS)
415+
)]
416+
#[OA\Parameter(
417+
name: 'time',
418+
description: 'Max age of retrieved posts',
419+
in: 'query',
420+
schema: new OA\Schema(type: 'string', default: Criteria::TIME_ALL, enum: Criteria::TIME_ROUTES_EN)
421+
)]
422+
#[OA\Parameter(
423+
name: 'lang[]',
424+
description: 'Language(s) of posts to return',
425+
in: 'query',
426+
explode: true,
427+
allowReserved: true,
428+
schema: new OA\Schema(
429+
type: 'array',
430+
items: new OA\Items(type: 'string', default: null, minLength: 2, maxLength: 3)
431+
)
432+
)]
433+
#[OA\Parameter(
434+
name: 'usePreferredLangs',
435+
description: 'Filter by a user\'s preferred languages? (Requires authentication and takes precedence over lang[])',
436+
in: 'query',
437+
schema: new OA\Schema(type: 'boolean', default: false),
438+
)]
439+
#[OA\Parameter(
440+
name: 'federation',
441+
description: 'What type of federated entries to retrieve',
442+
in: 'query',
443+
schema: new OA\Schema(type: 'string', default: Criteria::AP_ALL, enum: Criteria::AP_OPTIONS)
444+
)]
445+
#[OA\Parameter(
446+
name: 'includeBoosts',
447+
description: 'if true then boosted content from followed users are included',
448+
in: 'query',
449+
schema: new OA\Schema(type: 'boolean', default: false)
450+
)]
451+
#[OA\Tag(name: 'post')]
452+
#[Security(name: 'oauth2', scopes: ['read'])]
453+
#[IsGranted('ROLE_OAUTH2_READ')]
454+
public function subscribedWithBoosts(
455+
ContentRepository $repository,
456+
RateLimiterFactoryInterface $apiReadLimiter,
457+
RateLimiterFactoryInterface $anonymousApiReadLimiter,
458+
SymfonySecurity $security,
459+
SqlHelpers $sqlHelpers,
460+
#[MapQueryParameter] ?int $p,
461+
#[MapQueryParameter] ?int $perPage,
462+
#[MapQueryParameter] ?string $sort,
463+
#[MapQueryParameter] ?string $time,
464+
#[MapQueryParameter] ?string $federation,
465+
): JsonResponse {
466+
$headers = $this->rateLimit($apiReadLimiter, $anonymousApiReadLimiter);
467+
468+
$criteria = new PostPageView($p ?? 1, $security);
469+
$criteria->sortOption = $sort ?? Criteria::SORT_HOT;
470+
$criteria->time = $criteria->resolveTime($time ?? Criteria::TIME_ALL);
471+
$criteria->perPage = self::constrainPerPage($perPage ?? ContentRepository::PER_PAGE);
472+
$criteria->setFederation($federation ?? Criteria::AP_ALL);
473+
474+
$criteria->subscribed = true;
475+
$criteria->includeBoosts = true;
476+
$criteria->setContent(Criteria::CONTENT_MICROBLOG);
477+
478+
$this->handleLanguageCriteria($criteria);
479+
480+
$user = $this->getUserOrThrow();
481+
$criteria->fetchCachedItems($sqlHelpers, $user);
482+
483+
$posts = $repository->findByCriteria($criteria);
484+
485+
$dtos = [];
486+
foreach ($posts->getCurrentPageResults() as $value) {
487+
try {
488+
if ($value instanceof Post) {
489+
$this->handlePrivateContent($value);
490+
$dtos[] = new ContentResponseDto(post: $this->serializePost($this->postFactory->createDto($value), $this->tagLinkRepository->getTagsOfContent($value)));
491+
} elseif ($value instanceof PostComment) {
492+
$this->handlePrivateContent($value);
493+
$dtos[] = new ContentResponseDto(postComment: $this->serializePostComment($this->postCommentFactory->createDto($value), $this->tagLinkRepository->getTagsOfContent($value)));
494+
} else {
495+
throw new \AssertionError('got unexpected type '.\get_class($value));
496+
}
348497
} catch (\Exception $e) {
349498
continue;
350499
}

src/Controller/Entry/EntryFrontController.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,15 @@ private function handleSubscription(string $subscription, &$criteria)
201201
}
202202
}
203203

204-
private function setUserPreferences(?User $user, &$criteria)
204+
private function setUserPreferences(?User $user, Criteria &$criteria): void
205205
{
206-
if (null !== $user && 0 < \count($user->preferredLanguages)) {
206+
if (null === $user) {
207+
return;
208+
}
209+
210+
$criteria->includeBoosts = $user->showBoostsOfFollowing;
211+
212+
if (0 < \count($user->preferredLanguages)) {
207213
$criteria->languages = $user->preferredLanguages;
208214
}
209215
}

src/DTO/ContentResponseDto.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class ContentResponseDto
1515
public function __construct(
1616
public ?EntryResponseDto $entry = null,
1717
public ?PostResponseDto $post = null,
18+
public ?EntryCommentResponseDto $entryComment = null,
19+
public ?PostCommentResponseDto $postComment = null,
1820
) {
1921
}
2022
}

src/DTO/UserSettingsDto.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function __construct(
3232
public ?string $frontDefaultSort = null,
3333
#[OA\Property(type: 'string', enum: EntryCommentPageView::SORT_OPTIONS)]
3434
public ?string $commentDefaultSort = null,
35+
public ?bool $showFollowingBoosts = null,
3536
#[OA\Property(type: 'array', items: new OA\Items(type: 'string'))]
3637
public ?array $featuredMagazines = null,
3738
#[OA\Property(type: 'array', items: new OA\Items(type: 'string'))]

src/Entity/User.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Visibil
109109
public int $followersCount = 0;
110110
#[Column(type: 'string', nullable: false, options: ['default' => self::HOMEPAGE_ALL])]
111111
public string $homepage = self::HOMEPAGE_ALL;
112+
#[Column(type: 'boolean', nullable: false, options: ['default' => false])]
113+
public bool $showBoostsOfFollowing = false;
112114
#[Column(type: 'enumSortOptions', nullable: false, options: ['default' => ESortOptions::Hot->value])]
113115
public string $frontDefaultSort = ESortOptions::Hot->value;
114116
#[Column(type: 'enumFrontContentOptions', nullable: true)]

0 commit comments

Comments
 (0)