From 3d3bfad5d0b30f37ebff5017e4adc2dee2136069 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 19 Nov 2024 03:15:48 -0600 Subject: [PATCH 01/29] fixup! more path sanitization --- .../backend/src/server/api/endpoints/drive/folders/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index cc45bd8c58..2374c754f7 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -96,7 +96,7 @@ export default class extends Endpoint { // eslint- // Check if the circular reference will occur const checkCircle = async (folderId: string, limit: number = 32): Promise => { if (limit <= 0) { - return false; + return true; } const folder2 = await this.driveFoldersRepository.findOneByOrFail({ id: folderId, From 763c708253161d67358a8e36c27af3b02d133298 Mon Sep 17 00:00:00 2001 From: "zawa-ch." Date: Tue, 19 Nov 2024 21:12:40 +0900 Subject: [PATCH 02/29] =?UTF-8?q?Fix(backend):=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E5=89=8A=E9=99=A4=E3=81=AE=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=81=8C=E5=8B=95=E4=BD=9C=E3=81=97=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1499?= =?UTF-8?q?6)=20(#14997)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * アカウント削除のモデレーションログが動作していないのを修正 * update CHANGELOG --- CHANGELOG.md | 1 + .../backend/src/server/api/endpoints/admin/accounts/delete.ts | 2 +- .../backend/src/server/api/endpoints/admin/delete-account.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 058e41c486..befe237b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ - Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) - Fix: User Webhookテスト機能のMock Payloadを修正 +- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) ### Misskey.js - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 01dea703a3..ece1984cff 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -46,7 +46,7 @@ export default class extends Endpoint { // eslint- throw new Error('cannot delete a root account'); } - await this.deleteAccoountService.deleteAccount(user); + await this.deleteAccoountService.deleteAccount(user, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index b6f0f22d60..9065a71f6a 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -33,13 +33,13 @@ export default class extends Endpoint { // eslint- private deleteAccountService: DeleteAccountService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneByOrFail({ id: ps.userId }); if (user.isDeleted) { return; } - await this.deleteAccountService.deleteAccount(user); + await this.deleteAccountService.deleteAccount(user, me); }); } } From a236bbb8d436cd69d2201039b381e9598e545c9b Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 19 Nov 2024 06:17:23 -0600 Subject: [PATCH 03/29] CSP: allow blob images for cropping avatars Signed-off-by: eternal-flame-AD --- packages/backend/src/server/csp.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/server/csp.ts b/packages/backend/src/server/csp.ts index aeee4eab3a..3e1e7962b8 100644 --- a/packages/backend/src/server/csp.ts +++ b/packages/backend/src/server/csp.ts @@ -30,6 +30,7 @@ export function generateCSP(hashedMap: Map, options: { [ '\'self\'', 'data:', + 'blob:', // 'https://avatars.githubusercontent.com', // uncomment this for contributor avatars to work options.mediaProxy ].filter(Boolean)], From 416d71002a8730d04e71771d0d687da1c22e3fce Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 19 Nov 2024 10:35:56 -0600 Subject: [PATCH 04/29] improve emoji packing Signed-off-by: eternal-flame-AD --- .../src/core/entities/EmojiEntityService.ts | 67 ++++++++++++++----- .../src/server/api/endpoints/emojis.ts | 17 ++--- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0..8929d4e640 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -10,6 +10,7 @@ import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; +import { In } from 'typeorm'; @Injectable() export class EmojiEntityService { @@ -20,11 +21,9 @@ export class EmojiEntityService { } @bindThis - public async packSimple( - src: MiEmoji['id'] | MiEmoji, - ): Promise> { - const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); - + public packSimpleNoQuery( + emoji: MiEmoji, + ): Packed<'EmojiSimple'> { return { aliases: emoji.aliases, name: emoji.name, @@ -38,18 +37,34 @@ export class EmojiEntityService { } @bindThis - public packSimpleMany( - emojis: any[], - ) { - return Promise.all(emojis.map(x => this.packSimple(x))); + public async packSimple( + src: MiEmoji['id'] | MiEmoji, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + return this.packSimpleNoQuery(emoji); } @bindThis - public async packDetailed( - src: MiEmoji['id'] | MiEmoji, - ): Promise> { - const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + public async packSimpleMany( + emojis: MiEmoji['id'][] | MiEmoji[], + ): Promise[]> { + if (emojis.length === 0) { + return []; + } + + if (typeof emojis[0] === 'string') { + const res = await this.emojisRepository.findBy({ id: In(emojis as MiEmoji['id'][]) }); + return res.map(this.packSimpleNoQuery); + } + return (emojis as MiEmoji[]).map(this.packSimpleNoQuery); + } + + @bindThis + public packDetailedNoQuery( + emoji: MiEmoji, + ): Packed<'EmojiDetailed'> { return { id: emoji.id, aliases: emoji.aliases, @@ -66,10 +81,28 @@ export class EmojiEntityService { } @bindThis - public packDetailedMany( - emojis: any[], - ) { - return Promise.all(emojis.map(x => this.packDetailed(x))); + public async packDetailed( + src: MiEmoji['id'] | MiEmoji, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + return this.packDetailedNoQuery(emoji); + } + + @bindThis + public async packDetailedMany( + emojis: MiEmoji['id'][] | MiEmoji[], + ) : Promise[]> { + if (emojis.length === 0) { + return []; + } + + if (typeof emojis[0] === 'string') { + const res = await this.emojisRepository.findBy({ id: In(emojis as MiEmoji['id'][]) }); + return res.map(this.packDetailedNoQuery); + } + + return (emojis as MiEmoji[]).map(this.packDetailedNoQuery); } } diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..7888e65794 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -50,18 +50,15 @@ export default class extends Endpoint { // eslint- private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - }); + const emojis = await this.emojisRepository + .createQueryBuilder() + .where({ host: IsNull() }) + .orderBy('LOWER(category)', 'ASC') + .addOrderBy('LOWER(name)', 'ASC') + .getMany(); return { - emojis: await this.emojiEntityService.packSimpleMany(emojis), + emojis: emojis.map(this.emojiEntityService.packSimpleNoQuery), }; }); } From 4ba0357d4914b026b812cc3c27f9e4380b5375c6 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 19 Nov 2024 21:07:02 -0600 Subject: [PATCH 05/29] fix(backend): Atomically mark remote account deletions Signed-off-by: eternal-flame-AD --- .../1732071810971-IndexUserDeleted.js | 16 +++++ .../backend/src/core/DeleteAccountService.ts | 4 ++ packages/backend/src/core/RoleService.ts | 1 + .../src/core/activitypub/ApInboxService.ts | 9 +-- .../activitypub/models/ApPersonService.ts | 6 +- .../DeleteAccountProcessorService.ts | 62 ++++++++++++++++--- .../src/server/api/endpoints/users/show.ts | 1 + 7 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 packages/backend/migration/1732071810971-IndexUserDeleted.js diff --git a/packages/backend/migration/1732071810971-IndexUserDeleted.js b/packages/backend/migration/1732071810971-IndexUserDeleted.js new file mode 100644 index 0000000000..b4c3d714ad --- /dev/null +++ b/packages/backend/migration/1732071810971-IndexUserDeleted.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project and yumechi + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class IndexUserDeleted1732071810971 { + name = 'IndexUserDeleted1732071810971' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_199b79e682bdc5ba946f491686" ON "user" ("isDeleted")`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_199b79e682bdc5ba946f491686"`); + } +} diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 7f1b8f3efb..a5b0f60fbf 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -47,6 +47,10 @@ export class DeleteAccountService { }); } + if (!(await this.usersRepository.update({ id: user.id, isDeleted: false }, { isDeleted: true })).affected) { + return; + } + // 物理削除する前にDelete activityを送信する if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 5af6b05942..ba4507d206 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -488,6 +488,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return ids.length > 0 ? await this.usersRepository.findBy({ id: In(ids), + isDeleted: false, }) : []; } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index f3aa46292e..fccf86cb91 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -509,19 +509,12 @@ export class ApInboxService { return `skip: delete actor ${actor.uri} !== ${uri}`; } - const user = await this.usersRepository.findOneBy({ id: actor.id }); - if (user == null) { - return 'skip: actor not found'; - } else if (user.isDeleted) { + if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) { return 'skip: already deleted'; } const job = await this.queueService.createDeleteAccountJob(actor); - await this.usersRepository.update(actor.id, { - isDeleted: true, - }); - this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id }); return `ok: queued ${job.name} ${job.id}`; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 8c4e40c561..e01b098194 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -557,7 +557,9 @@ export class ApPersonService implements OnModuleInit { if (moving) updates.movedAt = new Date(); // Update user - await this.usersRepository.update(exist.id, updates); + if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) { + return 'skip'; + } if (person.publicKey) { await this.userPublickeysRepository.update({ userId: exist.id }, { @@ -662,7 +664,7 @@ export class ApPersonService implements OnModuleInit { @bindThis public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise { - const user = await this.usersRepository.findOneByOrFail({ id: userId }); + const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false }); if (!this.userEntityService.isRemoteUser(user)) return; if (!user.featured) return; diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 14a53e0c42..05be2e02f0 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -4,9 +4,9 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; +import { DataSource, MoreThan, QueryFailedError, TypeORMError } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { MiUser, type DriveFilesRepository, type NotesRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -26,6 +26,9 @@ export class DeleteAccountProcessorService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.db) + private db: DataSource, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -52,6 +55,14 @@ export class DeleteAccountProcessorService { return; } + if (!user.isDeleted) { + this.logger.warn('User is not pre-marked as deleted, this is likely a bug'); + if (process.env.NODE_ENV !== 'production') { + throw new Error('User is not pre-marked as deleted'); // make some noise to make sure tests fail + } + await this.usersRepository.update({ id: user.id }, { isDeleted: true }); + } + { // Delete notes let cursor: MiNote['id'] | null = null; @@ -121,13 +132,46 @@ export class DeleteAccountProcessorService { } } - // soft指定されている場合は物理削除しない - if (job.data.soft) { - // nop - } else { - await this.usersRepository.delete(job.data.user.id); + // Deadlockが発生した場合にリトライする + for (let remaining = 3; remaining > 0; remaining--) { + try { + // soft指定されている場合は物理削除しない + await this.db.transaction(async txn => { + // soft指定してもデータをすべで削除する + await txn.delete(MiUser, user.id); + if (job.data.soft) { + await txn.insert(MiUser, { + ...user, + isRoot: false, + updatedAt: new Date(), + emojis: [], + hideOnlineStatus: true, + followersCount: 0, + followingCount: 0, + avatarUrl: null, + avatarId: null, + notesCount: 0, + inbox: null, + sharedInbox: null, + featured: null, + uri: null, + followersUri: null, + token: null, + isDeleted: true, + }); + } + }); + return 'Account deleted'; + } catch (e) { + // 40P01 = deadlock_detected + // https://www.postgresql.org/docs/current/errcodes-appendix.html + if (remaining > 0 && e instanceof QueryFailedError && e.driverError.code === '40P01') { + this.logger.warn(`Deadlock occurred, retrying after 1s... [${remaining - 1} remaining]`); + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + throw e; + } } - - return 'Account deleted'; } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 062326e28d..6daed35372 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -106,6 +106,7 @@ export default class extends Endpoint { // eslint- id: In(ps.userIds), } : { id: In(ps.userIds), + isDeleted: false, isSuspended: false, }); From 95d3fb08f40adfcd5535fa4d199158640b51d305 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 20 Nov 2024 01:59:48 -0600 Subject: [PATCH 06/29] Prefix all calls to Image and Video Processing service Signed-off-by: eternal-flame-AD --- packages/backend/src/core/CoreModule.ts | 16 +- packages/backend/src/core/DriveService.ts | 18 +- .../src/core/ImageProcessingService.ts | 4 +- .../src/core/VideoProcessingService.ts | 8 +- .../core/entities/DriveFileEntityService.ts | 8 +- .../backend/src/server/FileServerService.ts | 481 ++++-------------- 6 files changed, 126 insertions(+), 409 deletions(-) diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 734d135648..9fdaf5eb86 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -36,7 +36,7 @@ import { GlobalEventService } from './GlobalEventService.js'; import { HashtagService } from './HashtagService.js'; import { HttpRequestService } from './HttpRequestService.js'; import { IdService } from './IdService.js'; -import { ImageProcessingService } from './ImageProcessingService.js'; +import { __YUME_PRIVATE_ImageProcessingService } from './ImageProcessingService.js'; import { InstanceActorService } from './InstanceActorService.js'; import { InternalStorageService } from './InternalStorageService.js'; import { MetaService } from './MetaService.js'; @@ -67,7 +67,7 @@ import { UserMutingService } from './UserMutingService.js'; import { UserRenoteMutingService } from './UserRenoteMutingService.js'; import { UserSuspendService } from './UserSuspendService.js'; import { UserAuthService } from './UserAuthService.js'; -import { VideoProcessingService } from './VideoProcessingService.js'; +import { __YUME_PRIVATE_VideoProcessingService } from './VideoProcessingService.js'; import { UserWebhookService } from './UserWebhookService.js'; import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; @@ -179,7 +179,7 @@ const $GlobalEventService: Provider = { provide: 'GlobalEventService', useExisti const $HashtagService: Provider = { provide: 'HashtagService', useExisting: HashtagService }; const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService }; const $IdService: Provider = { provide: 'IdService', useExisting: IdService }; -const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService }; +const $ImageProcessingService: Provider = { provide: '__YUME_PRIVATE_ImageProcessingService', useExisting: __YUME_PRIVATE_ImageProcessingService }; const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService }; const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService }; const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService }; @@ -212,7 +212,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; -const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; +const $VideoProcessingService: Provider = { provide: '__YUME_PRIVATE_VideoProcessingService', useExisting: __YUME_PRIVATE_VideoProcessingService }; const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService }; const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService }; const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; @@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HashtagService, HttpRequestService, IdService, - ImageProcessingService, + __YUME_PRIVATE_ImageProcessingService, InstanceActorService, InternalStorageService, MetaService, @@ -363,7 +363,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserSearchService, UserSuspendService, UserAuthService, - VideoProcessingService, + __YUME_PRIVATE_VideoProcessingService, UserWebhookService, SystemWebhookService, WebhookTestService, @@ -625,7 +625,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HashtagService, HttpRequestService, IdService, - ImageProcessingService, + __YUME_PRIVATE_ImageProcessingService, InstanceActorService, InternalStorageService, MetaService, @@ -658,7 +658,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserSearchService, UserSuspendService, UserAuthService, - VideoProcessingService, + __YUME_PRIVATE_VideoProcessingService, UserWebhookService, SystemWebhookService, WebhookTestService, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 495d67a93b..517a682753 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -22,8 +22,8 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { VideoProcessingService } from '@/core/VideoProcessingService.js'; -import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import { __YUME_PRIVATE_VideoProcessingService } from '@/core/VideoProcessingService.js'; +import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { QueueService } from '@/core/QueueService.js'; import type { MiDriveFolder } from '@/models/DriveFolder.js'; @@ -120,8 +120,8 @@ export class DriveService { private downloadService: DownloadService, private internalStorageService: InternalStorageService, private s3Service: S3Service, - private imageProcessingService: ImageProcessingService, - private videoProcessingService: VideoProcessingService, + private privateImageProcessingService: __YUME_PRIVATE_ImageProcessingService, + private privateVideoProcessingService: __YUME_PRIVATE_VideoProcessingService, private globalEventService: GlobalEventService, private queueService: QueueService, private roleService: RoleService, @@ -277,7 +277,7 @@ export class DriveService { } try { - const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path); + const thumbnail = await this.privateVideoProcessingService.generateVideoThumbnail(path); return { webpublic: null, thumbnail, @@ -331,9 +331,9 @@ export class DriveService { try { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048); + webpublic = await this.privateImageProcessingService.convertSharpToWebp(img, 2048, 2048); } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); + webpublic = await this.privateImageProcessingService.convertSharpToPng(img, 2048, 2048); } else { this.registerLogger.debug('web image not created (not an required image)'); } @@ -352,9 +352,9 @@ export class DriveService { try { if (isAnimated) { - thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 }); + thumbnail = await this.privateImageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 }); } else { - thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422); + thumbnail = await this.privateImageProcessingService.convertSharpToWebp(img, 498, 422); } } catch (err) { this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error); diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 6f978b34c8..6aa25decc8 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -46,7 +46,9 @@ import { bindThis } from '@/decorators.js'; import { Readable } from 'node:stream'; @Injectable() -export class ImageProcessingService { +// Prevent accidental import by upstream merge +// eslint-disable-next-line +export class __YUME_PRIVATE_ImageProcessingService { constructor( ) { } diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 747fe4fc7e..461e427b0d 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -7,19 +7,21 @@ import { Inject, Injectable } from '@nestjs/common'; import FFmpeg from 'fluent-ffmpeg'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { ImageProcessingService } from '@/core/ImageProcessingService.js'; +import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; @Injectable() -export class VideoProcessingService { +// Prevent accidental import by upstream merge +// eslint-disable-next-line +export class __YUME_PRIVATE_VideoProcessingService { constructor( @Inject(DI.config) private config: Config, - private imageProcessingService: ImageProcessingService, + private imageProcessingService: __YUME_PRIVATE_ImageProcessingService, ) { } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index c485555f90..a1dbef36da 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -18,7 +18,6 @@ import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '../UtilityService.js'; -import { VideoProcessingService } from '../VideoProcessingService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFolderEntityService } from './DriveFolderEntityService.js'; @@ -43,7 +42,6 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, - private videoProcessingService: VideoProcessingService, private idService: IdService, ) { } @@ -86,11 +84,7 @@ export class DriveFileEntityService { @bindThis public getThumbnailUrl(file: MiDriveFile): string | null { - if (file.type.startsWith('video')) { - if (file.thumbnailUrl) return file.thumbnailUrl; - - return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url); - } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { // 動画ではなくリモートかつメディアプロキシ return this.getProxiedUrl(file.uri, 'static'); } diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 91d826382d..1bdcdbe2a0 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -8,27 +8,19 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import rename from 'rename'; -import sharp from 'sharp'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import type { Config } from '@/config.js'; import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; -import { createTemp } from '@/misc/create-temp.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { StatusError } from '@/misc/status-error.js'; import type Logger from '@/logger.js'; -import { DownloadService } from '@/core/DownloadService.js'; -import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js'; -import { VideoProcessingService } from '@/core/VideoProcessingService.js'; -import { InternalStorageService } from '@/core/InternalStorageService.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { isMimeImage } from '@/misc/is-mime-image.js'; -import { correctFilename } from '@/misc/correct-filename.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; +import { InternalStorageService } from '@/core/InternalStorageService.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -46,11 +38,8 @@ export class FileServerService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private fileInfoService: FileInfoService, - private downloadService: DownloadService, - private imageProcessingService: ImageProcessingService, - private videoProcessingService: VideoProcessingService, private internalStorageService: InternalStorageService, + private fileInfoService: FileInfoService, private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('server', 'gray'); @@ -134,165 +123,78 @@ export class FileServerService { return; } - try { - if (file.state === 'remote') { - let image: IImageStreamable | null = null; + if (file.state === 'remote') { + const url = new URL(`${this.config.mediaProxy}/`); - if (file.fileRole === 'thumbnail') { - if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) { - reply.header('Cache-Control', 'max-age=31536000, immutable'); + url.searchParams.set('url', file.url); - const url = new URL(`${this.config.mediaProxy}/static.webp`); - url.searchParams.set('url', file.url); - url.searchParams.set('static', '1'); + return await reply.redirect(url.toString(), 301); + } - file.cleanup(); - return await reply.redirect(url.toString(), 301); - } else if (file.mime.startsWith('video/')) { - const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); - if (externalThumbnail) { - file.cleanup(); - return await reply.redirect(externalThumbnail, 301); - } + if (file.fileRole !== 'original') { + const filename = rename(file.filename, { + suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web', + extname: file.ext ? `.${file.ext}` : '.unknown', + }).toString(); - image = await this.videoProcessingService.generateVideoThumbnail(file.path); - } + reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + reply.header('Content-Disposition', contentDisposition('inline', filename)); + + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; } - - if (file.fileRole === 'webpublic') { - if (['image/svg+xml'].includes(file.mime)) { - reply.header('Cache-Control', 'max-age=31536000, immutable'); - - const url = new URL(`${this.config.mediaProxy}/svg.webp`); - url.searchParams.set('url', file.url); - - file.cleanup(); - return await reply.redirect(url.toString(), 301); - } - } - - if (!image) { - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } - } - - if ('pipe' in image.data && typeof image.data.pipe === 'function') { - // image.dataがstreamなら、stream終了後にcleanup - image.data.on('end', file.cleanup); - image.data.on('close', file.cleanup); - } else { - // image.dataがstreamでないなら直ちにcleanup - file.cleanup(); - } - - reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - reply.header('Content-Disposition', - contentDisposition( - 'inline', - correctFilename(file.filename, image.ext), - ), - ); - return image.data; + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; } - if (file.fileRole !== 'original') { - const filename = rename(file.filename, { - suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web', - extname: file.ext ? `.${file.ext}` : '.unknown', - }).toString(); + return fs.createReadStream(file.path); + } else { + reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); + reply.header('Content-Length', file.file.size); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + reply.header('Content-Disposition', contentDisposition('inline', file.filename)); - reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - reply.header('Content-Disposition', contentDisposition('inline', filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; } - - return fs.createReadStream(file.path); - } else { - reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - reply.header('Content-Disposition', contentDisposition('inline', file.filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; - } - - return fs.createReadStream(file.path); + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; } - } catch (e) { - if ('cleanup' in file) file.cleanup(); - throw e; + + return fs.createReadStream(file.path); } } @bindThis private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) { - let url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; + const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; if (typeof url !== 'string') { reply.code(400); @@ -302,234 +204,56 @@ export class FileServerService { // アバタークロップなど、どうしてもオリジンである必要がある場合 const mustOrigin = 'origin' in request.query; - if (this.config.externalMediaProxyEnabled) { - // 外部のメディアプロキシが有効なら、そちらにリダイレクト - - reply.header('Cache-Control', 'public, max-age=259200'); // 3 days - - const externalURL = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`); - - for (const [key, value] of Object.entries(request.query)) { - externalURL.searchParams.append(key, value); - } - - if (mustOrigin) { - url = `${this.config.mediaProxy}?url=${encodeURIComponent(url)}`; - } else { - return await reply.redirect( - externalURL.toString(), - 301, - ); - } + if (!this.config.mediaProxy) { + reply.code(501); } - if (!request.headers['user-agent']) { - throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); - } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { - throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); + const proxiedURL = new URL(`${this.config.mediaProxy}/?url=${encodeURIComponent(url)}`); + + for (const [key, value] of Object.entries(request.query)) { + if (key.toLowerCase() === 'url') continue; + proxiedURL.searchParams.append(key, value); } - if (!request.headers['user-agent']) { - throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); - } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { - throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); - } - - // Create temp file - const file = await this.getStreamAndTypeFromUrl(url); - if (file === '404') { - reply.code(404); - reply.header('Cache-Control', 'max-age=86400'); - return reply.sendFile('/dummy.png', assets); - } - - if (file === '204') { - reply.code(204); - reply.header('Cache-Control', 'max-age=86400'); - return; - } - - try { - const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp'); - const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp'); - - if ( - 'emoji' in request.query || - 'avatar' in request.query || - 'static' in request.query || - 'preview' in request.query || - 'badge' in request.query - ) { - if (!isConvertibleImage) { - // 画像でないなら404でお茶を濁す - throw new StatusError('Unexpected mime', 404); - } - } - - let image: IImageStreamable | null = null; - if ('emoji' in request.query || 'avatar' in request.query) { - if (!isAnimationConvertibleImage && !('static' in request.query)) { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } else { - const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) - .resize({ - height: 'emoji' in request.query ? 128 : 320, - withoutEnlargement: true, - }) - .webp(webpDefault); - - image = { - data, - ext: 'webp', - type: 'image/webp', - }; - } - } else if ('static' in request.query) { - image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422); - } else if ('preview' in request.query) { - image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200); - } else if ('badge' in request.query) { - const mask = (await sharpBmp(file.path, file.mime)) - .resize(96, 96, { - fit: 'contain', - position: 'centre', - withoutEnlargement: false, - }) - .greyscale() - .normalise() - .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast - .flatten({ background: '#000' }) - .toColorspace('b-w'); - - const stats = await mask.clone().stats(); - - if (stats.entropy < 0.1) { - // エントロピーがあまりない場合は404にする - throw new StatusError('Skip to provide badge', 404); - } - - const data = sharp({ - create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, - }) - .pipelineColorspace('b-w') - .boolean(await mask.png().toBuffer(), 'eor'); - - image = { - data: await data.png().toBuffer(), - ext: 'png', - type: 'image/png', - }; - } else if (file.mime === 'image/svg+xml') { - image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048); - } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) { - throw new StatusError('Rejected type', 403, 'Rejected type'); - } - - if (!image) { - if (request.headers.range && file.file && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } - } - - if ('cleanup' in file) { - if ('pipe' in image.data && typeof image.data.pipe === 'function') { - // image.dataがstreamなら、stream終了後にcleanup - image.data.on('end', file.cleanup); - image.data.on('close', file.cleanup); - } else { - // image.dataがstreamでないなら直ちにcleanup - file.cleanup(); - } - } - - reply.header('Content-Type', image.type); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - reply.header('Content-Disposition', - contentDisposition( - 'inline', - correctFilename(file.filename, image.ext), - ), + if (!mustOrigin) { + return await reply.redirect( + proxiedURL.toString(), + 301, ); - return image.data; - } catch (e) { - if ('cleanup' in file) file.cleanup(); - throw e; - } - } - - @bindThis - private async getStreamAndTypeFromUrl(url: string): Promise< - { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } - | '404' - | '204' - > { - if (url.startsWith(`${this.config.url}/files/`)) { - const key = url.replace(`${this.config.url}/files/`, '').split('/').shift(); - if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key'); - - return await this.getFileFromKey(key); } - return await this.downloadAndDetectTypeFromUrl(url); - } + reply.header('Cache-Control', 'public, max-age=259200'); // 3 days - @bindThis - private async downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } - > { - const [path, cleanup] = await createTemp(); - try { - const { filename } = await this.downloadService.downloadUrl(url, path); - - const { mime, ext } = await this.fileInfoService.detectType(path); - - return { - state: 'remote', - mime, ext, - path, cleanup, - filename, - }; - } catch (e) { - cleanup(); - throw e; + if (!request.headers['user-agent']) { + throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); + } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { + throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); } + + if (!request.headers['user-agent']) { + throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); + } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { + throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); + } + + // directly proxy request through + const res = await fetch(proxiedURL, { + headers: { + 'X-Forwarded-For': request.headers['x-forwarded-for']?.at(0) ?? request.ip, + 'User-Agent': request.headers['user-agent'], + }, + }); + + reply.code(res.status); + for (const [key, value] of res.headers.entries()) { + reply.header(key, value); + } + reply.send(res.body); } @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; filename: string; url: string; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' @@ -548,15 +272,10 @@ export class FileServerService { if (!file.storedInternal) { if (!(file.isLink && file.uri)) return '204'; - const result = await this.downloadAndDetectTypeFromUrl(file.uri); - file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので - return { - ...result, - url: file.uri, - fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', - file, - filename: file.name, - }; + return { state: 'remote', + fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', + filename: file.name + , url: file.uri }; } const path = this.internalStorageService.resolvePath(key); From 4603ab67bb96da39f6c4186ab655e8011fdcb740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B4=87=E5=B3=B0=20=E6=9C=94=E8=8F=AF?= <160555157+sakuhanight@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:08:26 +0900 Subject: [PATCH 07/29] =?UTF-8?q?feat:=20=E7=B5=B5=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=81=AE=E3=83=9D=E3=83=83=E3=83=97=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=AB=E7=B7=A8=E9=9B=86?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#15004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mod: 絵文字のポップアップメニューに編集を追加 * fix: code styleの修正 * fix: code styleの修正 * fix --- .../src/components/global/MkCustomEmoji.vue | 29 +++++++++++++++++-- packages/frontend/src/pages/emojis.emoji.vue | 28 ++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 66f82a7898..ec1d859080 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only
${messages.header}
v${_VERSION_}
`; + return `${messages.title}
${messages.header}
v${_VERSION_}
`; } globalThis.addEventListener('fetch', ev => { - let isHTMLRequest = false; - if (ev.request.headers.get('sec-fetch-dest') === 'document') { - isHTMLRequest = true; - } else if (ev.request.headers.get('accept')?.includes('/html')) { - isHTMLRequest = true; - } else if (ev.request.url.endsWith('/')) { - isHTMLRequest = true; - } + const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); - if (!isHTMLRequest) return; - ev.respondWith( - fetch(ev.request) - .catch(async () => { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - }), - ); + if (shouldCache) { + ev.respondWith( + caches.match(ev.request) + .then(async response => { + if (response) return response; + + try { + const fetchResponse = await fetch(ev.request); + if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { + return fetchResponse; + } + + const responseToCache = fetchResponse.clone(); + const cache = await caches.open(STATIC_CACHE_NAME); + + try { + await cache.put(ev.request, responseToCache); + } catch (err) { + const keys = await cache.keys(); + if (keys.length > 0) { + const deleteCount = Math.ceil(keys.length * 0.2); + for (let i = 0; i < deleteCount; i++) { + await cache.delete(keys[i]); + } + await cache.put(ev.request, responseToCache.clone()); + } + } + + return fetchResponse; + } catch { + return response; + } + }) + ); + return; + } + + let isHTMLRequest = false; + if (ev.request.headers.get('sec-fetch-dest') === 'document') { + isHTMLRequest = true; + } else if (ev.request.headers.get('accept')?.includes('/html')) { + isHTMLRequest = true; + } else if (ev.request.url.endsWith('/')) { + isHTMLRequest = true; + } + + if (!isHTMLRequest) return; + ev.respondWith( + fetch(ev.request) + .catch(async () => { + const html = await offlineContentHTML(); + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); + }), + ); }); globalThis.addEventListener('push', ev => { - // クライアント取得 - ev.waitUntil(globalThis.clients.matchAll({ - includeUncontrolled: true, - type: 'window', - }).then(async () => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); + ev.waitUntil(globalThis.clients.matchAll({ + includeUncontrolled: true, + type: 'window', + }).then(async () => { + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); - switch (data.type) { - // case 'driveFileCreated': - case 'notification': - case 'unreadAntennaNote': - // 1日以上経過している場合は無視 - if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; + switch (data.type) { + case 'notification': + case 'unreadAntennaNote': + if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; - return createNotification(data); - case 'readAllNotifications': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - break; - } + return createNotification(data); + case 'readAllNotifications': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); + break; + } - await createEmptyNotification(); - return; - })); + await createEmptyNotification(); + return; + })); }); globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { - ev.waitUntil((async (): Promise => { - if (_DEV_) { - console.log('notificationclick', ev.action, ev.notification.data); - } + ev.waitUntil((async (): Promise => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } - const { action, notification } = ev; - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; - const { userId: loginId } = data; - let client: WindowClient | null = null; + const { action, notification } = ev; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; + const { userId: loginId } = data; + let client: WindowClient | null = null; - switch (data.type) { - case 'notification': - switch (action) { - case 'follow': - if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); - break; - case 'showUser': - if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - break; - case 'reply': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - case 'renote': - if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); - break; - case 'accept': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); - break; - } - break; - case 'reject': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); - break; - } - break; - case 'showFollowRequests': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - default: - switch (data.body.type) { - case 'receiveFollowRequest': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'reaction': - client = await swos.openNote(data.body.note.id, loginId); - break; - default: - if ('note' in data.body) { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('user' in data.body) { - client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - } - break; - } - } - break; - case 'unreadAntennaNote': - client = await swos.openAntenna(data.body.antenna.id, loginId); - break; - default: - switch (action) { - case 'markAllAsRead': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get[]>('accounts').then(accounts => { - return Promise.all((accounts ?? []).map(async account => { - await swos.sendMarkAllAsRead(account.id); - })); - }); - break; - case 'settings': - client = await swos.openClient('push', '/settings/notifications', loginId); - break; - } - } + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); + break; + case 'showUser': + if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + break; + case 'reply': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); + break; + case 'renote': + if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); + break; + case 'accept': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + break; + } + break; + case 'reject': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + break; + } + break; + case 'showFollowRequests': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'edited': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); + break; + default: + switch (data.body.type) { + case 'receiveFollowRequest': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'reaction': + client = await swos.openNote(data.body.note.id, loginId); + break; + default: + if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; + } + } + break; + case 'unreadAntennaNote': + client = await swos.openAntenna(data.body.antenna.id, loginId); + break; + default: + switch (action) { + case 'markAllAsRead': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); + await get[]>('accounts').then(accounts => { + return Promise.all((accounts ?? []).map(async account => { + await swos.sendMarkAllAsRead(account.id); + })); + }); + break; + case 'settings': + client = await swos.openClient('push', '/settings/notifications', loginId); + break; + } + } - if (client) { - client.focus(); - } - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(loginId); - } + if (client) { + client.focus(); + } + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(loginId); + } - notification.close(); - })()); + notification.close(); + })()); }); globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; - ev.waitUntil((async (): Promise => { - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(data.userId); - } - return; - })()); + ev.waitUntil((async (): Promise => { + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(data.userId); + } + return; + })()); }); globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { - ev.waitUntil((async (): Promise => { - switch (ev.data) { - case 'clear': - // Cache Storage全削除 - await caches.keys() - .then(cacheNames => Promise.all( - cacheNames.map(name => caches.delete(name)), - )); - return; // TODO - } + ev.waitUntil((async (): Promise => { + if (ev.data === 'clear') { + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)), + )); + return; + } - if (typeof ev.data === 'object') { - // E.g. '[object Array]' → 'array' - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + if (typeof ev.data === 'object') { + const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - swLang.setLang(ev.data.lang); - } - } - } - })()); + if (otype === 'object') { + if (ev.data.msg === 'initialize') { + swLang.setLang(ev.data.lang); + } + } + } + })()); }); From 5587de26c7326e48a61d849b72d6d0a012d0ccd1 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 06:05:51 -0600 Subject: [PATCH 23/29] fix sw.js initialization order Signed-off-by: eternal-flame-AD --- packages/sw/src/sw.ts | 457 +++++++++++++++++++++--------------------- 1 file changed, 230 insertions(+), 227 deletions(-) diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 124e24a98a..89754c976e 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -15,274 +15,277 @@ import * as swos from '@/scripts/operations.js'; const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`; const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/']; -async function requestStorage() { - try { - if (navigator.storage && navigator.storage.persisted) { - await navigator.storage.persisted(); - } - if (navigator.storage && navigator.storage.estimate) { - const quota = await navigator.storage.estimate(); - if (quota.quota && quota.quota < 2 * 1024 * 1024 * 1024) { - await navigator.storage.estimate; - } - } - } catch { - console.error('Failed to request storage'); - } +async function requestStorage(): Promise { + try { + if (navigator.storage && navigator.storage.estimate) { + await navigator.storage.estimate(); + if (navigator.storage && navigator.storage.persisted) { + if (!await navigator.storage.persisted()) { + return null; + } + } + return navigator.storage.getDirectory(); + } + } catch { + console.error('Failed to request storage'); + } + return null; } async function cacheWithFallback(cache, paths) { - for (const path of paths) { - try { - await cache.add(new Request(path, { credentials: 'same-origin' })); - } catch (error) {} - } + for (const path of paths) { + try { + await cache.add(new Request(path, { credentials: 'same-origin' })); + } catch (error) { } + } } globalThis.addEventListener('install', (ev) => { - ev.waitUntil((async () => { - await requestStorage(); - const cache = await caches.open(STATIC_CACHE_NAME); - await cacheWithFallback(cache, PATHS_TO_CACHE); - await globalThis.skipWaiting(); - })()); + ev.waitUntil((async () => { + const storage = await requestStorage(); + registerFetchHandler(storage); + const cache = await caches.open(STATIC_CACHE_NAME); + await cacheWithFallback(cache, PATHS_TO_CACHE); + await globalThis.skipWaiting(); + })()); }); globalThis.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== STATIC_CACHE_NAME && v !== swLang.cacheName) - .map(name => caches.delete(name)), - )) - .then(() => globalThis.clients.claim()), - ); + ev.waitUntil( + caches.keys() + .then(cacheNames => Promise.all( + cacheNames + .filter((v) => v !== STATIC_CACHE_NAME && v !== swLang.cacheName) + .map(name => caches.delete(name)), + )) + .then(() => globalThis.clients.claim()), + ); }); async function offlineContentHTML() { - const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial>; - const messages = { - title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server', - header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server', - reload: i18n.ts?.reload ?? 'Reload', - }; + const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial>; + const messages = { + title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server', + header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server', + reload: i18n.ts?.reload ?? 'Reload', + }; - return `${messages.title}
${messages.header}
v${_VERSION_}
`; + return `${messages.title}
${messages.header}
v${_VERSION_}
`; } -globalThis.addEventListener('fetch', ev => { - const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); +async function registerFetchHandler(root: FileSystemDirectoryHandle | null) { + console.debug('rootfs:', root); - if (shouldCache) { - ev.respondWith( - caches.match(ev.request) - .then(async response => { - if (response) return response; + globalThis.addEventListener('fetch', ev => { + const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); - try { - const fetchResponse = await fetch(ev.request); - if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { - return fetchResponse; - } + if (shouldCache) { + ev.respondWith( + caches.match(ev.request) + .then(async response => { + if (response) return response; - const responseToCache = fetchResponse.clone(); - const cache = await caches.open(STATIC_CACHE_NAME); + const fetchResponse = await fetch(ev.request); + if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { + return fetchResponse; + } - try { - await cache.put(ev.request, responseToCache); - } catch (err) { - const keys = await cache.keys(); - if (keys.length > 0) { - const deleteCount = Math.ceil(keys.length * 0.2); - for (let i = 0; i < deleteCount; i++) { - await cache.delete(keys[i]); - } - await cache.put(ev.request, responseToCache.clone()); - } - } + const responseToCache = fetchResponse.clone(); + const cache = await caches.open(STATIC_CACHE_NAME); - return fetchResponse; - } catch { - return response; - } - }) - ); - return; - } + try { + await cache.put(ev.request, responseToCache); + } catch (err) { + const keys = await cache.keys(); + if (keys.length > 0) { + const deleteCount = Math.ceil(keys.length * 0.2); + for (let i = 0; i < deleteCount; i++) { + await cache.delete(keys[i]); + } + await cache.put(ev.request, responseToCache.clone()); + } + } - let isHTMLRequest = false; - if (ev.request.headers.get('sec-fetch-dest') === 'document') { - isHTMLRequest = true; - } else if (ev.request.headers.get('accept')?.includes('/html')) { - isHTMLRequest = true; - } else if (ev.request.url.endsWith('/')) { - isHTMLRequest = true; - } + return fetchResponse; + }) + ); + return; + } + + let isHTMLRequest = false; + if (ev.request.headers.get('sec-fetch-dest') === 'document') { + isHTMLRequest = true; + } else if (ev.request.headers.get('accept')?.includes('/html')) { + isHTMLRequest = true; + } else if (ev.request.url.endsWith('/')) { + isHTMLRequest = true; + } + + if (!isHTMLRequest) return; + ev.respondWith( + fetch(ev.request) + .catch(async () => { + const html = await offlineContentHTML(); + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); + }), + ); + }); +} - if (!isHTMLRequest) return; - ev.respondWith( - fetch(ev.request) - .catch(async () => { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - }), - ); -}); globalThis.addEventListener('push', ev => { - ev.waitUntil(globalThis.clients.matchAll({ - includeUncontrolled: true, - type: 'window', - }).then(async () => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); + ev.waitUntil(globalThis.clients.matchAll({ + includeUncontrolled: true, + type: 'window', + }).then(async () => { + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); - switch (data.type) { - case 'notification': - case 'unreadAntennaNote': - if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; + switch (data.type) { + case 'notification': + case 'unreadAntennaNote': + if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; - return createNotification(data); - case 'readAllNotifications': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - break; - } + return createNotification(data); + case 'readAllNotifications': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); + break; + } - await createEmptyNotification(); - return; - })); + await createEmptyNotification(); + return; + })); }); globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { - ev.waitUntil((async (): Promise => { - if (_DEV_) { - console.log('notificationclick', ev.action, ev.notification.data); - } + ev.waitUntil((async (): Promise => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } - const { action, notification } = ev; - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; - const { userId: loginId } = data; - let client: WindowClient | null = null; + const { action, notification } = ev; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; + const { userId: loginId } = data; + let client: WindowClient | null = null; - switch (data.type) { - case 'notification': - switch (action) { - case 'follow': - if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); - break; - case 'showUser': - if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - break; - case 'reply': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - case 'renote': - if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); - break; - case 'accept': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); - break; - } - break; - case 'reject': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); - break; - } - break; - case 'showFollowRequests': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'edited': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - default: - switch (data.body.type) { - case 'receiveFollowRequest': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'reaction': - client = await swos.openNote(data.body.note.id, loginId); - break; - default: - if ('note' in data.body) { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('user' in data.body) { - client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - } - break; - } - } - break; - case 'unreadAntennaNote': - client = await swos.openAntenna(data.body.antenna.id, loginId); - break; - default: - switch (action) { - case 'markAllAsRead': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get[]>('accounts').then(accounts => { - return Promise.all((accounts ?? []).map(async account => { - await swos.sendMarkAllAsRead(account.id); - })); - }); - break; - case 'settings': - client = await swos.openClient('push', '/settings/notifications', loginId); - break; - } - } + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); + break; + case 'showUser': + if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + break; + case 'reply': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); + break; + case 'renote': + if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); + break; + case 'accept': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + break; + } + break; + case 'reject': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + break; + } + break; + case 'showFollowRequests': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'edited': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); + break; + default: + switch (data.body.type) { + case 'receiveFollowRequest': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'reaction': + client = await swos.openNote(data.body.note.id, loginId); + break; + default: + if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; + } + } + break; + case 'unreadAntennaNote': + client = await swos.openAntenna(data.body.antenna.id, loginId); + break; + default: + switch (action) { + case 'markAllAsRead': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); + await get[]>('accounts').then(accounts => { + return Promise.all((accounts ?? []).map(async account => { + await swos.sendMarkAllAsRead(account.id); + })); + }); + break; + case 'settings': + client = await swos.openClient('push', '/settings/notifications', loginId); + break; + } + } - if (client) { - client.focus(); - } - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(loginId); - } + if (client) { + client.focus(); + } + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(loginId); + } - notification.close(); - })()); + notification.close(); + })()); }); globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; - ev.waitUntil((async (): Promise => { - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(data.userId); - } - return; - })()); + ev.waitUntil((async (): Promise => { + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(data.userId); + } + return; + })()); }); globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { - ev.waitUntil((async (): Promise => { - if (ev.data === 'clear') { - await caches.keys() - .then(cacheNames => Promise.all( - cacheNames.map(name => caches.delete(name)), - )); - return; - } + ev.waitUntil((async (): Promise => { + if (ev.data === 'clear') { + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)), + )); + return; + } - if (typeof ev.data === 'object') { - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + if (typeof ev.data === 'object') { + const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - swLang.setLang(ev.data.lang); - } - } - } - })()); + if (otype === 'object') { + if (ev.data.msg === 'initialize') { + swLang.setLang(ev.data.lang); + } + } + } + })()); }); From 8d48909e4f0e8dbbf9af85b42e3dd26884eb3c9c Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 06:06:30 -0600 Subject: [PATCH 24/29] remove unused httpAgent Signed-off-by: eternal-flame-AD --- packages/backend/src/core/DownloadService.ts | 1 - packages/backend/src/server/web/UrlPreviewService.ts | 1 - packages/sw/src/sw.ts | 8 +++++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 93f4a38246..6266e51496 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -61,7 +61,6 @@ export class DownloadService { request: operationTimeout, // whole operation timeout }, agent: { - http: this.httpRequestService.httpAgent, https: this.httpRequestService.httpsAgent, }, http2: false, // default diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 5d493c2c46..94b4a9fd90 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -118,7 +118,6 @@ export class UrlPreviewService { private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise { const agent = this.config.proxy ? { - http: this.httpRequestService.httpAgent, https: this.httpRequestService.httpsAgent, } : undefined; diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 89754c976e..dba5bd55ad 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -43,7 +43,7 @@ async function cacheWithFallback(cache, paths) { globalThis.addEventListener('install', (ev) => { ev.waitUntil((async () => { const storage = await requestStorage(); - registerFetchHandler(storage); + await registerFetchHandler(storage); const cache = await caches.open(STATIC_CACHE_NAME); await cacheWithFallback(cache, PATHS_TO_CACHE); await globalThis.skipWaiting(); @@ -75,6 +75,12 @@ async function offlineContentHTML() { async function registerFetchHandler(root: FileSystemDirectoryHandle | null) { console.debug('rootfs:', root); + const state = await root?.getFileHandle('state.json', { create: true }) + await state?.createWritable().then(async writable => { + await writable.write(JSON.stringify({ started: Date.now() })); + await writable.close(); + }); + globalThis.addEventListener('fetch', ev => { const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); From 748685e53e2800da283bdaefee5b142a267c56d6 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 10:11:42 -0600 Subject: [PATCH 25/29] fix handling of private renotes Signed-off-by: eternal-flame-AD --- packages/backend/src/core/NoteCreateService.ts | 5 +++-- packages/backend/src/core/activitypub/type.ts | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3647fa7231..6f5773058b 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -7,6 +7,7 @@ import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; +import * as Bull from 'bullmq'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; @@ -291,7 +292,7 @@ export class NoteCreateService implements OnApplicationShutdown { case 'followers': // 他人のfollowers noteはreject if (data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); + throw new Bull.UnrecoverableError('Renote target is not public or home'); } // Renote対象がfollowersならfollowersにする @@ -299,7 +300,7 @@ export class NoteCreateService implements OnApplicationShutdown { break; case 'specified': // specified / direct noteはreject - throw new Error('Renote target is not public or home'); + throw new Bull.UnrecoverableError('Renote target is not public or home'); } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 6c77ef92d0..57dc2fe5f8 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -79,7 +79,6 @@ export function markOutgoing(object: T, _ba export function yumeNormalizeURL(url: string): string { const u = new URL(url); - u.hash = ''; u.host = toASCII(u.host); if (u.protocol && u.protocol !== 'https:') { throw new bull.UnrecoverableError('protocol is not https'); From 9052a025989eadce8f9a720f5cc4b9dbde9147c8 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 10:20:20 -0600 Subject: [PATCH 26/29] fix a mishap during merging upstream Signed-off-by: eternal-flame-AD --- packages/backend/src/core/activitypub/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 57dc2fe5f8..79c561d933 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -611,7 +611,7 @@ export function yumeDowncastRemove(object: IObject): IRemove | null { export function yumeDowncastLike(object: IObject): ILike | null { if (getApType(object) !== 'Like') return null; const obj = object as ILike; - if (!obj.actor || !obj.object || !obj.target) return null; + if (!obj.actor || !obj.object) return null; return { ...extractMisskeyVendorKeys(object), ...extractSafe(object), From 8e508b921ca21fd0e8722b73e037009cbb692c90 Mon Sep 17 00:00:00 2001 From: fly_mc Date: Fri, 22 Nov 2024 00:40:28 +0800 Subject: [PATCH 27/29] Pick pari sw.ts --- packages/sw/src/sw.ts | 442 +++++++++++++++++++----------------------- 1 file changed, 199 insertions(+), 243 deletions(-) diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index dba5bd55ad..042ce570da 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -15,283 +15,239 @@ import * as swos from '@/scripts/operations.js'; const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`; const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/']; -async function requestStorage(): Promise { - try { - if (navigator.storage && navigator.storage.estimate) { - await navigator.storage.estimate(); - if (navigator.storage && navigator.storage.persisted) { - if (!await navigator.storage.persisted()) { - return null; - } - } - return navigator.storage.getDirectory(); - } - } catch { - console.error('Failed to request storage'); - } - return null; -} - async function cacheWithFallback(cache, paths) { - for (const path of paths) { - try { - await cache.add(new Request(path, { credentials: 'same-origin' })); - } catch (error) { } - } + for (const path of paths) { + try { + await cache.add(new Request(path, { credentials: 'same-origin' })); + } catch (error) { + // eslint-disable-next-line no-empty + } + } } globalThis.addEventListener('install', (ev) => { - ev.waitUntil((async () => { - const storage = await requestStorage(); - await registerFetchHandler(storage); - const cache = await caches.open(STATIC_CACHE_NAME); - await cacheWithFallback(cache, PATHS_TO_CACHE); - await globalThis.skipWaiting(); - })()); + ev.waitUntil((async () => { + const cache = await caches.open(STATIC_CACHE_NAME); + await cacheWithFallback(cache, PATHS_TO_CACHE); + await globalThis.skipWaiting(); + })()); }); -globalThis.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== STATIC_CACHE_NAME && v !== swLang.cacheName) - .map(name => caches.delete(name)), - )) - .then(() => globalThis.clients.claim()), - ); +globalThis.addEventListener('activate', (ev) => { + ev.waitUntil( + caches.keys() + .then((cacheNames) => Promise.all( + cacheNames + .filter((v) => v !== STATIC_CACHE_NAME && v !== swLang.cacheName) + .map((name) => caches.delete(name)), + )) + .then(() => globalThis.clients.claim()), + ); }); async function offlineContentHTML() { - const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial>; - const messages = { - title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server', - header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server', - reload: i18n.ts?.reload ?? 'Reload', - }; + const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial>; + const messages = { + title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server', + header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server', + reload: i18n.ts?.reload ?? 'Reload', + }; - return `${messages.title}
${messages.header}
v${_VERSION_}
`; + return `${messages.title}
${messages.header}
v${_VERSION_}
`; } -async function registerFetchHandler(root: FileSystemDirectoryHandle | null) { - console.debug('rootfs:', root); - const state = await root?.getFileHandle('state.json', { create: true }) - await state?.createWritable().then(async writable => { - await writable.write(JSON.stringify({ started: Date.now() })); - await writable.close(); - }); +globalThis.addEventListener('fetch', (ev) => { + const shouldCache = PATHS_TO_CACHE.some((path) => ev.request.url.includes(path)); + if (shouldCache) { + ev.respondWith( + caches.match(ev.request) + .then((response) => { + if (response) return response; - globalThis.addEventListener('fetch', ev => { - const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); + return fetch(ev.request).then((response) => { + if (!response || response.status !== 200 || response.type !== 'basic') return response; + const responseToCache = response.clone(); + caches.open(STATIC_CACHE_NAME) + .then((cache) => { + cache.put(ev.request, responseToCache); + }); + return response; + }); + }), + ); + return; + } - if (shouldCache) { - ev.respondWith( - caches.match(ev.request) - .then(async response => { - if (response) return response; + let isHTMLRequest = false; + if (ev.request.headers.get('sec-fetch-dest') === 'document') { + isHTMLRequest = true; + } else if (ev.request.headers.get('accept')?.includes('/html')) { + isHTMLRequest = true; + } else if (ev.request.url.endsWith('/')) { + isHTMLRequest = true; + } - const fetchResponse = await fetch(ev.request); - if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { - return fetchResponse; - } + if (!isHTMLRequest) return; + ev.respondWith( + fetch(ev.request) + .catch(async () => { + const html = await offlineContentHTML(); + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); + }), + ); +}); - const responseToCache = fetchResponse.clone(); - const cache = await caches.open(STATIC_CACHE_NAME); +globalThis.addEventListener('push', (ev) => { + ev.waitUntil(globalThis.clients.matchAll({ + includeUncontrolled: true, + type: 'window', + }).then(async () => { + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); - try { - await cache.put(ev.request, responseToCache); - } catch (err) { - const keys = await cache.keys(); - if (keys.length > 0) { - const deleteCount = Math.ceil(keys.length * 0.2); - for (let i = 0; i < deleteCount; i++) { - await cache.delete(keys[i]); - } - await cache.put(ev.request, responseToCache.clone()); - } - } + switch (data.type) { + case 'notification': + case 'unreadAntennaNote': + if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; - return fetchResponse; - }) - ); - return; - } + return createNotification(data); + case 'readAllNotifications': + await globalThis.registration.getNotifications() + .then((notifications) => notifications.forEach((n) => n.tag !== 'read_notification' && n.close())); + break; + } - let isHTMLRequest = false; - if (ev.request.headers.get('sec-fetch-dest') === 'document') { - isHTMLRequest = true; - } else if (ev.request.headers.get('accept')?.includes('/html')) { - isHTMLRequest = true; - } else if (ev.request.url.endsWith('/')) { - isHTMLRequest = true; - } - - if (!isHTMLRequest) return; - ev.respondWith( - fetch(ev.request) - .catch(async () => { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - }), - ); - }); -} - - -globalThis.addEventListener('push', ev => { - ev.waitUntil(globalThis.clients.matchAll({ - includeUncontrolled: true, - type: 'window', - }).then(async () => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); - - switch (data.type) { - case 'notification': - case 'unreadAntennaNote': - if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; - - return createNotification(data); - case 'readAllNotifications': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - break; - } - - await createEmptyNotification(); - return; - })); + await createEmptyNotification(); + return; + })); }); globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { - ev.waitUntil((async (): Promise => { - if (_DEV_) { - console.log('notificationclick', ev.action, ev.notification.data); - } + ev.waitUntil((async (): Promise => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } - const { action, notification } = ev; - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; - const { userId: loginId } = data; - let client: WindowClient | null = null; + const { action, notification } = ev; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; + const { userId: loginId } = data; + let client: WindowClient | null = null; - switch (data.type) { - case 'notification': - switch (action) { - case 'follow': - if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); - break; - case 'showUser': - if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - break; - case 'reply': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - case 'renote': - if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); - break; - case 'accept': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); - break; - } - break; - case 'reject': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); - break; - } - break; - case 'showFollowRequests': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'edited': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - default: - switch (data.body.type) { - case 'receiveFollowRequest': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'reaction': - client = await swos.openNote(data.body.note.id, loginId); - break; - default: - if ('note' in data.body) { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('user' in data.body) { - client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - } - break; - } - } - break; - case 'unreadAntennaNote': - client = await swos.openAntenna(data.body.antenna.id, loginId); - break; - default: - switch (action) { - case 'markAllAsRead': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get[]>('accounts').then(accounts => { - return Promise.all((accounts ?? []).map(async account => { - await swos.sendMarkAllAsRead(account.id); - })); - }); - break; - case 'settings': - client = await swos.openClient('push', '/settings/notifications', loginId); - break; - } - } + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); + break; + case 'showUser': + if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + break; + case 'reply': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); + break; + case 'renote': + if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); + break; + case 'accept': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + break; + } + break; + case 'reject': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + break; + } + break; + case 'showFollowRequests': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + default: + switch (data.body.type) { + case 'receiveFollowRequest': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'reaction': + client = await swos.openNote(data.body.note.id, loginId); + break; + default: + if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; + } + } + break; + case 'unreadAntennaNote': + client = await swos.openAntenna(data.body.antenna.id, loginId); + break; + default: + switch (action) { + case 'markAllAsRead': + await globalThis.registration.getNotifications() + .then((notifications) => notifications.forEach((n) => n.tag !== 'read_notification' && n.close())); + await get[]>('accounts').then((accounts) => { + return Promise.all((accounts ?? []).map(async (account) => { + await swos.sendMarkAllAsRead(account.id); + })); + }); + break; + case 'settings': + client = await swos.openClient('push', '/settings/notifications', loginId); + break; + } + } - if (client) { - client.focus(); - } - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(loginId); - } + if (client) { + client.focus(); + } + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(loginId); + } - notification.close(); - })()); + notification.close(); + })()); }); globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; - ev.waitUntil((async (): Promise => { - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(data.userId); - } - return; - })()); + ev.waitUntil((async (): Promise => { + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(data.userId); + } + return; + })()); }); globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { - ev.waitUntil((async (): Promise => { - if (ev.data === 'clear') { - await caches.keys() - .then(cacheNames => Promise.all( - cacheNames.map(name => caches.delete(name)), - )); - return; - } + ev.waitUntil((async (): Promise => { + if (ev.data === 'clear') { + await caches.keys() + .then((cacheNames) => Promise.all( + cacheNames.map((name) => caches.delete(name)), + )); + return; + } - if (typeof ev.data === 'object') { - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + if (typeof ev.data === 'object') { + const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - swLang.setLang(ev.data.lang); - } - } - } - })()); + if (otype === 'object') { + if (ev.data.msg === 'initialize') { + swLang.setLang(ev.data.lang); + } + } + } + })()); }); From b29f49fefc1b6353b2b9b870b216aeda9a7fe870 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 11:21:45 -0600 Subject: [PATCH 28/29] add media proxy to worker-src Signed-off-by: eternal-flame-AD --- packages/backend/src/server/csp.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/server/csp.ts b/packages/backend/src/server/csp.ts index 3e1e7962b8..adbebd0059 100644 --- a/packages/backend/src/server/csp.ts +++ b/packages/backend/src/server/csp.ts @@ -50,6 +50,7 @@ export function generateCSP(hashedMap: Map, options: { '\'wasm-unsafe-eval\'', ...scripts ]], + ['worker-src', ['\'self\'', options.mediaProxy].filter(Boolean)], ['object-src', ['\'none\'']], ['base-uri', ['\'self\'']], ['form-action', ['\'self\'']], From 63a98f3b413fbafc39989ac1ae58e10c76e3c600 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 10:30:17 -0600 Subject: [PATCH 29/29] 2024.11.0-yumechinokuni.6 Signed-off-by: eternal-flame-AD --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- packages/misskey-js/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd084495b..e0e05e51d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2024.11.0-yumechinokuni.6 + +- Upstream: 2024.11.0-alpha.4 タッグをマージする +- Performance: EmojiのリクエストをProxyでキャッシュするように +- Performance: Service Workerのキャッシュを最適化 +- Security: AP Payloadの検証を強化 +- Security: Image/Video Processorはドライブ機能だけを使うように + ## 2024.11.0-yumechinokuni.5 - Upstream: 2024.11.0-alpha.2 タッグをマージする diff --git a/package.json b/package.json index 6e61cd1a55..fc3b559ffa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.11.0-yumechinokuni.5", + "version": "2024.11.0-yumechinokuni.6", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 0749978fa9..6ad182c7e0 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.11.0-yumechinokuni.5", + "version": "2024.11.0-yumechinokuni.6", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js",