feat(backend): implement liked collection of ActivityPub actors

This commit is contained in:
Daiki Mizukami 2024-08-08 22:24:51 +09:00
parent f50941389d
commit 4ad43bca42
No known key found for this signature in database
GPG key ID: 10478E598B944AA2
2 changed files with 96 additions and 1 deletions

View file

@ -486,6 +486,7 @@ export class ApRendererService {
outbox: `${id}/outbox`,
followers: `${id}/followers`,
following: `${id}/following`,
liked: `${id}/liked`,
featured: `${id}/collections/featured`,
sharedInbox: `${this.config.url}/inbox`,
endpoints: { sharedInbox: `${this.config.url}/inbox` },

View file

@ -13,7 +13,7 @@ import accepts from 'accepts';
import vary from 'vary';
import secureJson from 'secure-json-parse';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiNoteReaction } from '@/models/_.js';
import * as url from '@/misc/prelude/url.js';
import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@ -351,6 +351,94 @@ export class ActivityPubServerService {
}
}
@bindThis
private async liked(
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
reply: FastifyReply,
) {
const userId = request.params.user;
const cursor = request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
reply.code(400);
return;
}
const page = request.query.page === 'true';
const user = await this.usersRepository.findOneBy({
id: userId,
host: IsNull(),
});
if (user == null) {
reply.code(404);
return;
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (!profile.publicReactions) {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
}
const limit = 10;
const partOf = `${this.config.url}/users/${userId}/liked`;
const query = {
userId: user.id,
} as FindOptionsWhere<MiNoteReaction>;
if (page) {
// カーソルが指定されている場合
if (cursor) {
query.id = LessThan(cursor);
}
const [reactions, reactionsCount] = await Promise.all([
this.noteReactionsRepository.find({
where: query,
take: limit + 1,
order: { id: -1 },
}),
this.noteReactionsRepository.count({ where: query }),
]);
// 「次のページ」があるかどうか
const inStock = reactions.length === limit + 1;
if (inStock) reactions.pop();
const renderedLikes = await Promise.all(reactions.map(reaction => this.apRendererService.renderLike(reaction, { uri: null })));
const rendered = this.apRendererService.renderOrderedCollectionPage(
`${partOf}?${url.query({
page: 'true',
cursor,
})}`,
reactionsCount, renderedLikes, partOf,
undefined,
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: reactions.at(-1)!.id,
})}` : undefined,
);
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
} else {
// index page
const reactionsCount = await this.noteReactionsRepository.count({ where: query });
const rendered = this.apRendererService.renderOrderedCollection(
partOf,
reactionsCount,
`${partOf}?page=true`,
);
reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
}
@bindThis
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
const userId = request.params.user;
@ -618,6 +706,12 @@ export class ActivityPubServerService {
Querystring: { cursor?: string; page?: string; };
}>('/users/:user/following', async (request, reply) => await this.following(request, reply));
// liked
fastify.get<{
Params: { user: string; };
Querystring: { cursor?: string; page?: string; };
}>('/users/:user/liked', async (request, reply) => await this.liked(request, reply));
// featured
fastify.get<{ Params: { user: string; }; }>('/users/:user/collections/featured', async (request, reply) => await this.featured(request, reply));