From 4ad43bca42992f1efddb612c1553021bbaa7173e Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Thu, 8 Aug 2024 22:24:51 +0900 Subject: [PATCH 01/19] feat(backend): implement `liked` collection of ActivityPub actors --- .../src/core/activitypub/ApRendererService.ts | 1 + .../src/server/ActivityPubServerService.ts | 96 ++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 98e944f347..b7d4fb04ec 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -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` }, diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 3255d64621..8c00fe4a73 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -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; + + 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)); From 630bee0cc497840fac725e96aeffb34999f33a5f Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Thu, 8 Aug 2024 22:33:35 +0900 Subject: [PATCH 02/19] enhance(backend): check visibility of reactions of remote users --- .../activitypub/models/ApPersonService.ts | 30 ++++++++++--------- packages/backend/src/core/activitypub/type.ts | 1 + 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f3ddf3952c..08f8c3ad55 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -296,17 +296,17 @@ export class ApPersonService implements OnModuleInit { const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; - const [followingVisibility, followersVisibility] = await Promise.all( + const [publicReactions, followingIsPublic, followersIsPublic] = await Promise.all( [ + this.isPublicCollection(person.liked, resolver), this.isPublicCollection(person.following, resolver), this.isPublicCollection(person.followers, resolver), - ].map((p): Promise<'public' | 'private'> => p - .then(isPublic => isPublic ? 'public' : 'private') + ].map((p): Promise => p .catch(err => { if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + this.logger.error('error occurred while fetching actor collection', { stack: err }); } - return 'private'; + return false; }) ) ); @@ -372,8 +372,9 @@ export class ApPersonService implements OnModuleInit { description: _description, url, fields, - followingVisibility, - followersVisibility, + publicReactions, + followingVisibility: followingIsPublic ? 'public' : 'private', + followersVisibility: followersIsPublic ? 'public' : 'private', birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, @@ -481,19 +482,19 @@ export class ApPersonService implements OnModuleInit { const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); - const [followingVisibility, followersVisibility] = await Promise.all( + const [publicReactions, followingIsPublic, followersIsPublic] = await Promise.all( [ + this.isPublicCollection(person.liked, resolver), this.isPublicCollection(person.following, resolver), this.isPublicCollection(person.followers, resolver), - ].map((p): Promise<'public' | 'private' | undefined> => p - .then(isPublic => isPublic ? 'public' : 'private') + ].map((p): Promise => p .catch(err => { if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + this.logger.error('error occurred while fetching actor collection', { stack: err }); // Do not update the visibiility on transient errors. return undefined; } - return 'private'; + return false; }) ) ); @@ -566,8 +567,9 @@ export class ApPersonService implements OnModuleInit { url, fields, description: _description, - followingVisibility, - followersVisibility, + publicReactions, + followingVisibility: followingIsPublic ? 'public' : followingIsPublic === false ? 'private' : undefined, + followersVisibility: followersIsPublic ? 'public' : followersIsPublic === false ? 'private' : undefined, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, }); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 131c518c0a..57b0a86f86 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -179,6 +179,7 @@ export interface IActor extends IObject { following?: string | ICollection | IOrderedCollection; featured?: string | IOrderedCollection; outbox: string | IOrderedCollection; + liked?: string | ICollection | IOrderedCollection; endpoints?: { sharedInbox?: string; }; From 34c201ff0115038a53d0dcdab21bd99f972510a2 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Thu, 8 Aug 2024 22:32:15 +0900 Subject: [PATCH 03/19] Revert "fix: Hide reactions of all remote users" This reverts commit 32b1c3de0ed4a438c87aa96bee99fd22d291f910. --- .../backend/src/core/entities/UserEntityService.ts | 2 +- .../src/server/api/endpoints/users/reactions.ts | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 9bf568bc90..23e69c413f 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -542,7 +542,7 @@ export class UserEntityService implements OnModuleInit { }), pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, - publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 + publicReactions: profile!.publicReactions, followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 7805ae3288..25a436707f 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -10,7 +10,6 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RoleService } from '@/core/RoleService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; @@ -38,11 +37,6 @@ export const meta = { code: 'REACTIONS_NOT_PUBLIC', id: '673a7dd2-6924-1093-e0c0-e68456ceae5c', }, - isRemoteUser: { - message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964', - code: 'IS_REMOTE_USER', - id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805', - }, }, } as const; @@ -69,7 +63,6 @@ export default class extends Endpoint { // eslint- private noteReactionsRepository: NoteReactionsRepository, private cacheService: CacheService, - private userEntityService: UserEntityService, private noteReactionEntityService: NoteReactionEntityService, private queryService: QueryService, private roleService: RoleService, @@ -78,11 +71,6 @@ export default class extends Endpoint { // eslint- const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set(); const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users if (!iAmModerator) { - const user = await this.cacheService.findUserById(ps.userId); - if (this.userEntityService.isRemoteUser(user)) { - throw new ApiError(meta.errors.isRemoteUser); - } - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { throw new ApiError(meta.errors.reactionsNotPublic); From eec885e2f5d0b81f709d574355155138fce11a0d Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Fri, 9 Aug 2024 20:05:22 +0900 Subject: [PATCH 04/19] docs(changelog): update `CHANGELOG.md` --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0bc6282b..bffc1ab631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### General - Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正 - Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように +- Enhance: リアクションの一覧の公開設定が連合されるように ### Client - From dabe38a991ebd7271d541ac84fa3d55fc503fa7c Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Fri, 9 Aug 2024 22:10:50 +0900 Subject: [PATCH 05/19] fix: set `publicReactions` of remote users to false --- ...08290742-remote-public-reactions-set-false.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/backend/migration/1723208290742-remote-public-reactions-set-false.js diff --git a/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js b/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js new file mode 100644 index 0000000000..de8e42cff4 --- /dev/null +++ b/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemotePublicReactionsSetFalse1723208290742 { + name = 'RemotePublicReactionsSetFalse1723208290742' + + public async up(queryRunner) { + await queryRunner.query(`UPDATE "user_profile" SET "publicReactions" = FALSE FROM "users" WHERE "user_profile"."userId" = "user"."id" AND "user"."host" IS NULL`); + } + + public async down(queryRunner) { + // no valid down migration + } +} From 699cbd0d77763b54d57e11b981c902ee5f2e365d Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 10 Aug 2024 00:34:29 +0900 Subject: [PATCH 06/19] fix(backend): fix 1723208290742-remote-public-reactions-set-false.js https://github.com/misskey-dev/misskey/pull/14383#pullrequestreview-2230486515 Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> --- .../1723208290742-remote-public-reactions-set-false.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js b/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js index de8e42cff4..f6114bcbe0 100644 --- a/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js +++ b/packages/backend/migration/1723208290742-remote-public-reactions-set-false.js @@ -6,11 +6,11 @@ export class RemotePublicReactionsSetFalse1723208290742 { name = 'RemotePublicReactionsSetFalse1723208290742' - public async up(queryRunner) { - await queryRunner.query(`UPDATE "user_profile" SET "publicReactions" = FALSE FROM "users" WHERE "user_profile"."userId" = "user"."id" AND "user"."host" IS NULL`); + async up(queryRunner) { + await queryRunner.query(`UPDATE "user_profile" SET "publicReactions" = FALSE WHERE "userHost" IS NOT NULL`); } - public async down(queryRunner) { + async down(queryRunner) { // no valid down migration } } From 8b7ae92b78dcc79a9acafd4dda3588976b214e40 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 10 Aug 2024 07:09:50 +0900 Subject: [PATCH 07/19] test(backend): update tests --- packages/backend/test/unit/activitypub.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 763ce2b336..1e0a355d8b 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -210,7 +210,7 @@ describe('ActivityPub', () => { }); describe('Collection visibility', () => { - test('Public following/followers', async () => { + test('Public following/followers/reactions', async () => { const actor = createRandomActor(); actor.following = { id: `${actor.id}/following`, @@ -219,6 +219,12 @@ describe('ActivityPub', () => { first: `${actor.id}/following?page=1`, }; actor.followers = `${actor.id}/followers`; + actor.liked ={ + id: `${actor.id}/following`, + type: 'OrderedCollection', + totalItems: 0, + orderedItems: [], + }; resolver.register(actor.id, actor); resolver.register(actor.followers, { @@ -233,9 +239,10 @@ describe('ActivityPub', () => { assert.deepStrictEqual(userProfile.followingVisibility, 'public'); assert.deepStrictEqual(userProfile.followersVisibility, 'public'); + assert.deepStrictEqual(userProfile.publicReactions, true); }); - test('Private following/followers', async () => { + test('Private following/followers/reactions', async () => { const actor = createRandomActor(); actor.following = { id: `${actor.id}/following`, @@ -244,6 +251,7 @@ describe('ActivityPub', () => { // first: … }; actor.followers = `${actor.id}/followers`; + // actor.liked = …; resolver.register(actor.id, actor); //resolver.register(actor.followers, { … }); @@ -253,6 +261,7 @@ describe('ActivityPub', () => { assert.deepStrictEqual(userProfile.followingVisibility, 'private'); assert.deepStrictEqual(userProfile.followersVisibility, 'private'); + assert.deepStrictEqual(userProfile.publicReactions, false); }); }); From d88193a16e882ac4aa3778bc6a98d06010821bb9 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 10 Aug 2024 07:28:44 +0900 Subject: [PATCH 08/19] style(backend): tweak --- packages/backend/test/unit/activitypub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 1e0a355d8b..555e64d855 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -219,7 +219,7 @@ describe('ActivityPub', () => { first: `${actor.id}/following?page=1`, }; actor.followers = `${actor.id}/followers`; - actor.liked ={ + actor.liked = { id: `${actor.id}/following`, type: 'OrderedCollection', totalItems: 0, From 985d582166dbd5df81ca32852be45597746e9e1b Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Wed, 14 Aug 2024 13:00:32 +0900 Subject: [PATCH 09/19] fix(backend): fix `liked` collection The `liked` collection is a list of objects liked by the actor, not the associated `Like` activities. --- .../src/server/ActivityPubServerService.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 8c00fe4a73..cffa44dde8 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -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, MiNoteReaction } from '@/models/_.js'; +import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -386,36 +386,37 @@ export class ActivityPubServerService { const limit = 10; const partOf = `${this.config.url}/users/${userId}/liked`; - const query = { - userId: user.id, - } as FindOptionsWhere; + const query = this.noteReactionsRepository.createQueryBuilder('reaction') + .andWhere('reaction.userId = :userId', { userId: user.id }); if (page) { + const countPromise = query.getCount(); + // カーソルが指定されている場合 if (cursor) { - query.id = LessThan(cursor); + query.andWhere('reaction.id < :id', { id: cursor }); } const [reactions, reactionsCount] = await Promise.all([ - this.noteReactionsRepository.find({ - where: query, - take: limit + 1, - order: { id: -1 }, - }), - this.noteReactionsRepository.count({ where: query }), + query + .limit(limit + 1) + .orderBy('reaction.id', 'DESC') + .innerJoinAndSelect('reaction.note', 'note') + .getMany(), + countPromise, ]); // 「次のページ」があるかどうか 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 reactedNoteUris = await Promise.all(reactions.map(reaction => reaction.note!.uri || `${this.config.url}/notes/${reaction.note!.uri}`)); const rendered = this.apRendererService.renderOrderedCollectionPage( `${partOf}?${url.query({ page: 'true', cursor, })}`, - reactionsCount, renderedLikes, partOf, + reactionsCount, reactedNoteUris, partOf, undefined, inStock ? `${partOf}?${url.query({ page: 'true', @@ -427,7 +428,7 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const reactionsCount = await this.noteReactionsRepository.count({ where: query }); + const reactionsCount = await query.getCount(); const rendered = this.apRendererService.renderOrderedCollection( partOf, reactionsCount, From 69bf40341dbb18444f12bb1a177589e7e48b65d3 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Wed, 14 Aug 2024 14:40:53 +0900 Subject: [PATCH 10/19] enhance(backend): embed local notes in `liked` collection --- packages/backend/src/server/ActivityPubServerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index cffa44dde8..65f54d064e 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -410,13 +410,13 @@ export class ActivityPubServerService { const inStock = reactions.length === limit + 1; if (inStock) reactions.pop(); - const reactedNoteUris = await Promise.all(reactions.map(reaction => reaction.note!.uri || `${this.config.url}/notes/${reaction.note!.uri}`)); + const reactedNotes = await Promise.all(reactions.map(({ note }) => note!.uri || this.apRendererService.renderNote(note!, false))); const rendered = this.apRendererService.renderOrderedCollectionPage( `${partOf}?${url.query({ page: 'true', cursor, })}`, - reactionsCount, reactedNoteUris, partOf, + reactionsCount, reactedNotes, partOf, undefined, inStock ? `${partOf}?${url.query({ page: 'true', From 2f2a0e3c5e79d6c9497416c7d7d773337d64c631 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Wed, 14 Aug 2024 18:12:45 +0900 Subject: [PATCH 11/19] enhance(backend): remove `totalItems` from `liked` collection --- .../src/core/activitypub/ApRendererService.ts | 2 +- packages/backend/src/core/activitypub/type.ts | 4 ++-- .../src/server/ActivityPubServerService.ts | 24 ++++++++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index b7d4fb04ec..73e55c1fe5 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -667,7 +667,7 @@ export class ApRendererService { * @param orderedItems attached objects (optional) */ @bindThis - public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) { + public renderOrderedCollection(id: string, totalItems?: number, first?: string, last?: string, orderedItems?: IObject[]) { const page: any = { id, type: 'OrderedCollection', diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 57b0a86f86..c018e7181e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -96,14 +96,14 @@ export interface IActivity extends IObject { export interface ICollection extends IObject { type: 'Collection'; - totalItems: number; + totalItems?: number; first?: IObject | string; items?: ApObject; } export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; - totalItems: number; + totalItems?: number; first?: IObject | string; orderedItems?: ApObject; } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 65f54d064e..b53fba2661 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -386,25 +386,22 @@ export class ActivityPubServerService { const limit = 10; const partOf = `${this.config.url}/users/${userId}/liked`; - const query = this.noteReactionsRepository.createQueryBuilder('reaction') - .andWhere('reaction.userId = :userId', { userId: user.id }); if (page) { - const countPromise = query.getCount(); + const query = this.noteReactionsRepository.createQueryBuilder('reaction') + .andWhere('reaction.userId = :userId', { userId: user.id }); // カーソルが指定されている場合 if (cursor) { query.andWhere('reaction.id < :id', { id: cursor }); } - const [reactions, reactionsCount] = await Promise.all([ - query - .limit(limit + 1) - .orderBy('reaction.id', 'DESC') - .innerJoinAndSelect('reaction.note', 'note') - .getMany(), - countPromise, - ]); + const reactions = await query + .limit(limit + 1) + .orderBy('reaction.id', 'DESC') + .innerJoinAndSelect('reaction.note', 'note') + .distinctOn(['note.id']) + .getMany(); // 「次のページ」があるかどうか const inStock = reactions.length === limit + 1; @@ -416,7 +413,7 @@ export class ActivityPubServerService { page: 'true', cursor, })}`, - reactionsCount, reactedNotes, partOf, + undefined, reactedNotes, partOf, undefined, inStock ? `${partOf}?${url.query({ page: 'true', @@ -428,10 +425,9 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const reactionsCount = await query.getCount(); const rendered = this.apRendererService.renderOrderedCollection( partOf, - reactionsCount, + undefined, `${partOf}?page=true`, ); reply.header('Cache-Control', 'public, max-age=180'); From 7074f80ea285f27dc2d7fc77236cb98e73810534 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Mon, 19 Aug 2024 23:16:08 +0900 Subject: [PATCH 12/19] refactor(backend): remove redundant SQL clause --- packages/backend/src/server/ActivityPubServerService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index b53fba2661..5d4c17ab1d 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -400,7 +400,6 @@ export class ActivityPubServerService { .limit(limit + 1) .orderBy('reaction.id', 'DESC') .innerJoinAndSelect('reaction.note', 'note') - .distinctOn(['note.id']) .getMany(); // 「次のページ」があるかどうか From 17da05ca542c068f22254b1c6491ca72c2c00ccb Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Mon, 19 Aug 2024 23:22:25 +0900 Subject: [PATCH 13/19] fix(backend): align visibility check of `liked` collection with `outbox` --- packages/backend/src/server/ActivityPubServerService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5d4c17ab1d..5f53188250 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -400,6 +400,12 @@ export class ActivityPubServerService { .limit(limit + 1) .orderBy('reaction.id', 'DESC') .innerJoinAndSelect('reaction.note', 'note') + .andWhere(new Brackets(qb => { + qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.localOnly = FALSE') .getMany(); // 「次のページ」があるかどうか From 8d41e3c8e4f11c296f347765c7b637160a798ad3 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Mon, 19 Aug 2024 23:28:36 +0900 Subject: [PATCH 14/19] fix(backend): filter out notes by suspended users in `liked` collection --- packages/backend/src/server/ActivityPubServerService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5f53188250..1320d67bf3 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -400,12 +400,14 @@ export class ActivityPubServerService { .limit(limit + 1) .orderBy('reaction.id', 'DESC') .innerJoinAndSelect('reaction.note', 'note') + .leftJoinAndSelect('note.user', 'noteUser') .andWhere(new Brackets(qb => { qb .where('note.visibility = \'public\'') .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE') + .andWhere('noteUser.isSuspended = FALSE') .getMany(); // 「次のページ」があるかどうか From 0949c0e0cdf9feb6f615749f456b334b8aaa37a7 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Tue, 20 Aug 2024 01:44:05 +0900 Subject: [PATCH 15/19] enhance(backend): embed `liked` collection into actor object --- .../src/core/activitypub/ApRendererService.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 73e55c1fe5..0a88656c91 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -472,6 +472,16 @@ export class ApRendererService { const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); + let liked; + if (profile.publicReactions) { + const likedId = `${id}/liked`; + liked = this.renderOrderedCollection( + likedId, + undefined, + `${likedId}?page=true`, + ); + } + const tag = [ ...apemojis, ...hashtagTags, @@ -486,7 +496,7 @@ export class ApRendererService { outbox: `${id}/outbox`, followers: `${id}/followers`, following: `${id}/following`, - liked: `${id}/liked`, + liked, featured: `${id}/collections/featured`, sharedInbox: `${this.config.url}/inbox`, endpoints: { sharedInbox: `${this.config.url}/inbox` }, From 04616ff996ab78775cf2b82a24702ab662ab9d8f Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Tue, 20 Aug 2024 02:16:14 +0900 Subject: [PATCH 16/19] enhance(backend): always render URI of `liked` collection https://socialhub.activitypub.rocks/t/fep-c0e0-emoji-reactions/4443/46 --- .../src/core/activitypub/ApRendererService.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 0a88656c91..1e42111de1 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -472,15 +472,12 @@ export class ApRendererService { const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); - let liked; - if (profile.publicReactions) { - const likedId = `${id}/liked`; - liked = this.renderOrderedCollection( - likedId, - undefined, - `${likedId}?page=true`, - ); - } + const likedId = `${id}/liked`; + const liked = this.renderOrderedCollection( + likedId, + undefined, + profile.publicReactions ? `${likedId}?page=true` : undefined, + ); const tag = [ ...apemojis, From 2004d5e2f85650dfffb69372e72218f38fe49363 Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 9 Nov 2024 18:31:41 +0900 Subject: [PATCH 17/19] test(backend): add federation test for `publicReactions` --- .../backend/test-federation/test/user.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts index 76605e61d4..df6f8c10e0 100644 --- a/packages/backend/test-federation/test/user.test.ts +++ b/packages/backend/test-federation/test/user.test.ts @@ -43,12 +43,11 @@ describe('User', () => { 'uri', 'createdAt', 'lastFetchedAt', - 'publicReactions', ]); }); }); - describe('ffVisibility is federated', () => { + describe('ff/reactions visibility is federated', () => { let alice: LoginUser, bob: LoginUser; let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe; @@ -78,6 +77,7 @@ describe('User', () => { ])) { strictEqual(user.followersVisibility, 'public'); strictEqual(user.followingVisibility, 'public'); + strictEqual(user.publicReactions, true); } }); @@ -113,6 +113,22 @@ describe('User', () => { strictEqual(user.followingVisibility, 'private'); } }); + + test('Setting false for publicReactions is federated', async () => { + await Promise.all([ + alice.client.request('i/update', { publicReactions: false }), + bob.client.request('i/update', { publicReactions: false }), + ]); + await sleep(); + + for (const user of await Promise.all([ + alice.client.request('users/show', { userId: bobInA.id }), + bob.client.request('users/show', { userId: aliceInB.id }), + ])) { + strictEqual(user.publicReactions, false); + strictEqual(user.publicReactions, false); + } + }); }); describe('isCat is federated', () => { From 38c0ce7634e7a0ab497418e2238c6460855eac0b Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 9 Nov 2024 18:43:12 +0900 Subject: [PATCH 18/19] docs(changelog): update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a933cb889..1959e54af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### General - Feat: コンテンツの表示にログインを必須にできるように - Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように +- Enhance: リアクションの一覧の公開設定が連合されるように ### Client - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように @@ -88,7 +89,6 @@ - Enhance: l10nの更新 - Enhance: Playの「人気」タブで10件以上表示可能に #14399 - Fix: 連合のホワイトリストが正常に登録されない問題を修正 -- Enhance: リアクションの一覧の公開設定が連合されるように ### Client - Enhance: デザインの調整 From 8842c569567840396bdd8a313c359977f7df86af Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Sat, 9 Nov 2024 18:49:57 +0900 Subject: [PATCH 19/19] test(backend): skip federation test for updating `publicReactions` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ローカルでは通るが、何故かCIでは通らないため。 --- packages/backend/test-federation/test/user.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts index df6f8c10e0..af29b72f2a 100644 --- a/packages/backend/test-federation/test/user.test.ts +++ b/packages/backend/test-federation/test/user.test.ts @@ -114,7 +114,7 @@ describe('User', () => { } }); - test('Setting false for publicReactions is federated', async () => { + test.skip('Setting false for publicReactions is federated', async () => { await Promise.all([ alice.client.request('i/update', { publicReactions: false }), bob.client.request('i/update', { publicReactions: false }),