From b7d056fb2266a91c4d671f200ef6c9f744f9416f Mon Sep 17 00:00:00 2001 From: hutchisr <42283663+hutchisr@users.noreply.github.com> Date: Wed, 12 Apr 2023 04:22:50 -0700 Subject: [PATCH 1/6] Use unique identifier for each follow request (#10600) Co-authored-by: anemone --- .../backend/src/core/UserFollowingService.ts | 6 +- .../src/server/ActivityPubServerService.ts | 88 +++++++++++++------ 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index dacaa7263a..a8eded6733 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; +import type { Config } from '@/config.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -44,6 +45,9 @@ export class UserFollowingService implements OnModuleInit { constructor( private moduleRef: ModuleRef, + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -411,7 +415,7 @@ export class UserFollowingService implements OnModuleInit { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, requestId ?? `${this.config.url}/follows/${followRequest.id}`)); this.queueService.deliver(follower, content, followee.inbox, false); } } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5799622074..e13e9265ab 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -6,7 +6,7 @@ import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; import accepts from 'accepts'; import vary from 'vary'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/index.js'; import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -54,6 +54,9 @@ export class ActivityPubServerService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + private utilityService: UtilityService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, @@ -205,22 +208,22 @@ export class ActivityPubServerService { 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; } - + //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - + if (profile.ffVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); @@ -231,31 +234,31 @@ export class ActivityPubServerService { return; } //#endregion - + const limit = 10; const partOf = `${this.config.url}/users/${userId}/following`; - + if (page) { const query = { followerId: user.id, } as FindOptionsWhere; - + // カーソルが指定されている場合 if (cursor) { query.id = LessThan(cursor); } - + // Get followings const followings = await this.followingsRepository.find({ where: query, take: limit + 1, order: { id: -1 }, }); - + // 「次のページ」があるかどうか const inStock = followings.length === limit + 1; if (inStock) followings.pop(); - + const renderedFollowees = await Promise.all(followings.map(following => this.apRendererService.renderFollowUser(following.followeeId))); const rendered = this.apRendererService.renderOrderedCollectionPage( `${partOf}?${url.query({ @@ -269,7 +272,7 @@ export class ActivityPubServerService { cursor: followings[followings.length - 1].id, })}` : undefined, ); - + this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } else { @@ -330,33 +333,33 @@ export class ActivityPubServerService { reply.code(400); return; } - + const untilId = request.query.until_id; if (untilId != null && typeof untilId !== 'string') { reply.code(400); return; } - + const page = request.query.page === 'true'; - + if (countIf(x => x != null, [sinceId, untilId]) > 1) { reply.code(400); return; } - + const user = await this.usersRepository.findOneBy({ id: userId, host: IsNull(), }); - + if (user == null) { reply.code(404); return; } - + const limit = 20; const partOf = `${this.config.url}/users/${userId}/outbox`; - + if (page) { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) .andWhere('note.userId = :userId', { userId: user.id }) @@ -365,11 +368,11 @@ export class ActivityPubServerService { .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE'); - + const notes = await query.take(limit).getMany(); - + if (sinceId) notes.reverse(); - + const activities = await Promise.all(notes.map(note => this.packActivity(note))); const rendered = this.apRendererService.renderOrderedCollectionPage( `${partOf}?${url.query({ @@ -387,7 +390,7 @@ export class ActivityPubServerService { until_id: notes[notes.length - 1].id, })}` : undefined, ); - + this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } else { @@ -457,7 +460,7 @@ export class ActivityPubServerService { // note fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); - + const note = await this.notesRepository.findOneBy({ id: request.params.note, visibility: In(['public', 'home']), @@ -639,6 +642,41 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); + // follow + fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { + // This may be used before the follow is completed, so we do not + // check if the following exists and only check if the follow request exists. + + const followRequest = await this.followRequestsRepository.findOneBy({ + id: request.params.followRequestId, + }); + + if (followRequest == null) { + reply.code(404); + return; + } + + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneBy({ + id: followRequest.followerId, + host: IsNull(), + }), + this.usersRepository.findOneBy({ + id: followRequest.followeeId, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + reply.code(404); + return; + } + + reply.header('Cache-Control', 'public, max-age=180'); + this.setResponseType(request, reply); + return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); + }); + done(); } } From 6ea057f8f89eec482e2b51bbd34ce3b8ebd739c6 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 12 Apr 2023 12:09:28 +0000 Subject: [PATCH 2/6] fix type in CustomEmojiService --- packages/backend/src/core/CustomEmojiService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 072dab7ce2..0f26d22666 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -46,9 +46,9 @@ export class CustomEmojiService { toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) - return new Map(JSON.parse(value).map((x) => [x.name, { + return new Map(JSON.parse(value).map((x: Emoji) => [x.name, { ...x, - updatedAt: new Date(x.updatedAt), + updatedAt: x.updatedAt && new Date(x.updatedAt), }])); }, }); From 3ff5a5ae293a86926043114bc8060846cb36489c Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 12 Apr 2023 12:32:27 +0000 Subject: [PATCH 3/6] fix type in CustomEmojiService 2 --- packages/backend/src/core/CustomEmojiService.ts | 3 ++- packages/backend/src/server/api/stream/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 0f26d22666..de9b748f4d 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -13,6 +13,7 @@ import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; import { query } from '@/misc/prelude/url.js'; +import { Serialized } from '@/server/api/stream/types'; @Injectable() export class CustomEmojiService { @@ -46,7 +47,7 @@ export class CustomEmojiService { toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) - return new Map(JSON.parse(value).map((x: Emoji) => [x.name, { + return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { ...x, updatedAt: x.updatedAt && new Date(x.updatedAt), }])); diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 101f6bf261..d9dba682cd 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -172,7 +172,7 @@ type EventUnionFromDictionary< > = U[keyof U]; // redis通すとDateのインスタンスはstringに変換されるので -type Serialized = { +export type Serialized = { [K in keyof T]: T[K] extends Date ? string From 4c0ef07f6f96c8a251c96cc8449bc51904c33a81 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 12 Apr 2023 12:34:34 +0000 Subject: [PATCH 4/6] fix --- packages/backend/src/core/CustomEmojiService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index de9b748f4d..eb18fb1b73 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -13,7 +13,7 @@ import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; import { query } from '@/misc/prelude/url.js'; -import { Serialized } from '@/server/api/stream/types'; +import type { Serialized } from '@/server/api/stream/types.js'; @Injectable() export class CustomEmojiService { From 04511ac141509b288d956ae378dc0c6a6559cb70 Mon Sep 17 00:00:00 2001 From: futchitwo <74236683+futchitwo@users.noreply.github.com> Date: Thu, 13 Apr 2023 08:33:36 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix(server):=20=E3=82=A2=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=83=8A=E3=81=A8=E3=83=AD=E3=83=BC=E3=83=ABTL=E3=81=AEuntil/s?= =?UTF-8?q?ince=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E3=81=8C?= =?UTF-8?q?=E5=8B=95=E3=81=8F=E3=82=88=E3=81=86=E3=81=AB=20(#10605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(server): アンテナとロールTLのuntil/sinceプロパティが動くように * fix --- .../backend/src/server/api/endpoints/antennas/notes.ts | 8 ++++---- packages/backend/src/server/api/endpoints/roles/notes.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index df83fe5f2a..88623ce26a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -76,18 +76,18 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchAntenna); } - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const noteIdsRes = await this.redisClient.xrevrange( `antennaTimeline:${antenna.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', - '-', + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', 'COUNT', limit); if (noteIdsRes.length === 0) { return []; } - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index d79528593f..b45d4af1fe 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -71,18 +71,18 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchRole); } - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const noteIdsRes = await this.redisClient.xrevrange( `roleTimeline:${role.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', - '-', + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', 'COUNT', limit); if (noteIdsRes.length === 0) { return []; } - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); if (noteIds.length === 0) { return []; From 78a2feb24ce464d1d049954f96892cd7f914a018 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 13 Apr 2023 08:34:25 +0900 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59149f59d1..bc77e01c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,11 @@ - ### Server +- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io - Misskey Webでのサーバーサイドエラー画面を改善 - Misskey Webでのサーバーサイドエラーのログが残るように - ノート作成時のアンテナ追加パフォーマンスを改善 -- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io +- アンテナとロールTLのuntil/sinceプロパティが動くように ## 13.11.2