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/37] 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/37] =?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/37] 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/37] 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/37] 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/37] 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/37] =?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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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", From d25fa27c24222a4a7f1806bb3a563f5d56bebcd5 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 12:23:43 -0600 Subject: [PATCH 30/37] add content and tag to ap safelist Signed-off-by: eternal-flame-AD --- packages/backend/src/core/activitypub/type.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 79c561d933..7200d04f5a 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -195,6 +195,8 @@ export interface IActivity extends IObject { export interface SafeList { id: string; + content: string | null; + tag: IObject | IObject[]; published: string; visibility: string; mentionedUsers: any[]; @@ -204,6 +206,8 @@ export interface SafeList { function extractSafe(object: IObject): Partial { return { id: object.id, + content: object.content, + tag: object.tag, published: object.published, visibility: object.visibility, mentionedUsers: object.mentionedUsers, From e885beaab9994cf8e7ec63cb429aa8daebac181d Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 12:29:42 -0600 Subject: [PATCH 31/37] Revert "Pick pari sw.ts" This reverts commit 8e508b921ca21fd0e8722b73e037009cbb692c90. Revert "add media proxy to worker-src" This reverts commit b29f49fefc1b6353b2b9b870b216aeda9a7fe870. --- packages/backend/src/server/csp.ts | 1 - packages/sw/src/sw.ts | 383 +++++++++++++---------------- 2 files changed, 175 insertions(+), 209 deletions(-) diff --git a/packages/backend/src/server/csp.ts b/packages/backend/src/server/csp.ts index adbebd0059..3e1e7962b8 100644 --- a/packages/backend/src/server/csp.ts +++ b/packages/backend/src/server/csp.ts @@ -50,7 +50,6 @@ 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\'']], diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 042ce570da..bf980b83a4 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -12,242 +12,209 @@ import { createEmptyNotification, createNotification } from '@/scripts/create-no import { swLang } from '@/scripts/lang.js'; 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 cacheWithFallback(cache, paths) { - 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 cache = await caches.open(STATIC_CACHE_NAME); - await cacheWithFallback(cache, PATHS_TO_CACHE); - await globalThis.skipWaiting(); - })()); +globalThis.addEventListener('install', () => { + // ev.waitUntil(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 !== 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)); +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; + } - if (shouldCache) { - ev.respondWith( - caches.match(ev.request) - .then((response) => { - if (response) return response; - - 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; - } - - 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(); +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; + switch (data.type) { + // case 'driveFileCreated': + case 'notification': + case 'unreadAntennaNote': + // 1日以上経過している場合は無視 + 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; + 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 => { + switch (ev.data) { + case 'clear': + // Cache Storage全削除 + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)), + )); + return; // TODO + } - if (typeof ev.data === 'object') { - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + if (typeof ev.data === 'object') { + // E.g. '[object Array]' → 'array' + 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 2b1c4b724552233649b68049d0a503d34920f225 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Thu, 21 Nov 2024 12:38:16 -0600 Subject: [PATCH 32/37] 2024.11.0-yumechinokuni.6p2 Signed-off-by: eternal-flame-AD --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc3b559ffa..a6ba839280 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.11.0-yumechinokuni.6", + "version": "2024.11.0-yumechinokuni.6p2", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 6ad182c7e0..5dabcb4165 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.6", + "version": "2024.11.0-yumechinokuni.6p2", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From f7cdb9df70747d0f6d344e0b385c4ff596ae6649 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 22 Nov 2024 08:56:19 -0600 Subject: [PATCH 33/37] bump fedivet Signed-off-by: eternal-flame-AD --- yume-mods/nyuukyou/Cargo.lock | 40 +++++++++++++++++------------------ yume-mods/nyuukyou/Cargo.toml | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/yume-mods/nyuukyou/Cargo.lock b/yume-mods/nyuukyou/Cargo.lock index 7db6517a56..7615b7716d 100644 --- a/yume-mods/nyuukyou/Cargo.lock +++ b/yume-mods/nyuukyou/Cargo.lock @@ -139,7 +139,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tower 0.5.1", "tower-layer", @@ -162,7 +162,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -421,7 +421,7 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fedivet" version = "0.1.0" -source = "git+https://forge.yumechi.jp/yume/fedivet?tag=testing-audit%2Brelay%2Bfilter#b1b051dc2f1319a3948d7afcecfd3ac8f92a07de" +source = "git+https://forge.yumechi.jp/yume/fedivet?tag=v0.0.1#46456b0a61b449dad7bbe85e0342bdd5e3b6e031" dependencies = [ "async-trait", "axum", @@ -588,9 +588,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -683,9 +683,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -940,9 +940,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "js-sys" @@ -1192,9 +1192,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1307,7 +1307,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1408,9 +1408,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -1564,9 +1564,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -1581,9 +1581,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -1814,9 +1814,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" diff --git a/yume-mods/nyuukyou/Cargo.toml b/yume-mods/nyuukyou/Cargo.toml index 13ccafaa61..16338dd862 100644 --- a/yume-mods/nyuukyou/Cargo.toml +++ b/yume-mods/nyuukyou/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" axum = "0.7" clap = { version = "4.5.20", features = ["derive"] } env_logger = "0.11.5" -fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "testing-audit+relay+filter" } +fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "v0.0.1" } rand = "0.8.5" serde = { version = "1.0.210", features = ["derive"] } tokio = { version = "1" } From eec5ce1a995114ba1b7570feb667987311fc49d2 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 22 Nov 2024 09:27:48 -0600 Subject: [PATCH 34/37] Remove trademarked branding Signed-off-by: eternal-flame-AD --- .github/ISSUE_TEMPLATE/01_bug-report.yml | 97 ------------------- .github/ISSUE_TEMPLATE/02_feature-request.yml | 22 ----- .github/ISSUE_TEMPLATE/config.yml | 8 -- .github/PULL_REQUEST_TEMPLATE/01_bug.md | 23 ----- .github/PULL_REQUEST_TEMPLATE/02_enhance.md | 23 ----- .github/PULL_REQUEST_TEMPLATE/03_release.md | 20 ---- .github/pull_request_template.md | 4 +- .okteto/okteto-pipeline.yml | 6 -- README.md | 62 +++++------- SECURITY.md | 13 +-- .../backend/src/server/web/views/base.pug | 11 --- .../backend/src/server/web/views/error.pug | 11 --- .../frontend/src/pages/about.overview.vue | 2 +- packages/frontend/src/ui/visitor.vue | 2 - packages/misskey-js/CONTRIBUTING.md | 2 +- packages/misskey-js/package.json | 2 +- 16 files changed, 32 insertions(+), 276 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/01_bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/02_feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/PULL_REQUEST_TEMPLATE/01_bug.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/02_enhance.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/03_release.md delete mode 100644 .okteto/okteto-pipeline.yml diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml deleted file mode 100644 index 315e712c30..0000000000 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: 🐛 Bug Report -description: Create a report to help us improve -labels: ["⚠️bug?"] - -body: - - type: markdown - attributes: - value: | - Thanks for reporting! - First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported. - Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first. - - - type: textarea - attributes: - label: 💡 Summary - description: Tell us what the bug is - validations: - required: true - - - type: textarea - attributes: - label: 🥰 Expected Behavior - description: Tell us what should happen - validations: - required: true - - - type: textarea - attributes: - label: 🤬 Actual Behavior - description: | - Tell us what happens instead of the expected behavior. - Please include errors from the developer console and/or server log files if you have access to them. - validations: - required: true - - - type: textarea - attributes: - label: 📝 Steps to Reproduce - placeholder: | - 1. - 2. - 3. - validations: - required: false - - - type: textarea - attributes: - label: 💻 Frontend Environment - description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. - - Examples: - * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 - * Browser: Chrome 113.0.5672.126 - * Server URL: misskey.example.com - * Misskey: 2024.x.x - value: | - * Model and OS of the device(s): - * Browser: - * Server URL: - * Misskey: - render: markdown - validations: - required: false - - - type: textarea - attributes: - label: 🛰 Backend Environment (for server admin) - description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. - If you are using a managed service, put that after the version. - - Examples: - * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment - * Misskey: 2024.x.x - * Node: 20.x.x - * PostgreSQL: 15.x.x - * Redis: 7.x.x - * OS and Architecture: Ubuntu 24.04.2 LTS aarch64 - value: | - * Installation Method or Hosting Service: - * Misskey: - * Node: - * PostgreSQL: - * Redis: - * OS and Architecture: - render: markdown - validations: - required: false - - - type: checkboxes - attributes: - label: Do you want to address this bug yourself? - options: - - label: Yes, I will patch the bug myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.github/ISSUE_TEMPLATE/02_feature-request.yml deleted file mode 100644 index 8d7b0b2539..0000000000 --- a/.github/ISSUE_TEMPLATE/02_feature-request.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: ✨ Feature Request -description: Suggest an idea for this project -labels: ["✨Feature"] - -body: - - type: textarea - attributes: - label: Summary - description: Tell us what the suggestion is - validations: - required: true - - type: textarea - attributes: - label: Purpose - description: Describe the specific problem or need you think this feature will solve, and who it will help. - validations: - required: true - - type: checkboxes - attributes: - label: Do you want to implement this feature yourself? - options: - - label: Yes, I will implement this by myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 5acad83336..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -contact_links: - - name: 💬 Misskey official Discord - url: https://discord.gg/Wp8gVStHW3 - about: Chat freely about Misskey - # 仮 - - name: 💬 Start discussion - url: https://github.com/misskey-dev/misskey/discussions - about: The official forum to join conversation and ask question diff --git a/.github/PULL_REQUEST_TEMPLATE/01_bug.md b/.github/PULL_REQUEST_TEMPLATE/01_bug.md deleted file mode 100644 index 0739fee709..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/01_bug.md +++ /dev/null @@ -1,23 +0,0 @@ - - -## What - - - -## Why - - - -## Additional info (optional) - - - -## Checklist -- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) -- [ ] Test working in a local environment -- [ ] (If needed) Update CHANGELOG.md -- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md deleted file mode 100644 index 0739fee709..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md +++ /dev/null @@ -1,23 +0,0 @@ - - -## What - - - -## Why - - - -## Additional info (optional) - - - -## Checklist -- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) -- [ ] Test working in a local environment -- [ ] (If needed) Update CHANGELOG.md -- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/03_release.md b/.github/PULL_REQUEST_TEMPLATE/03_release.md deleted file mode 100644 index b5b832e1dc..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/03_release.md +++ /dev/null @@ -1,20 +0,0 @@ -## Summary -This is a release PR. - -For more information on the release instructions, please see: -https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release - -## For reviewers -- CHANGELOGに抜け漏れは無いか -- バージョンの上げ方は適切か -- 他にこのリリースに含めなければならない変更は無いか -- 全体的な変更内容を俯瞰し問題は無いか -- レビューされていないコミットがある場合は、それが問題ないか -- 最終的な動作確認を行い問題は無いか - -などを確認し、リリースする準備が整っていると思われる場合は approve してください。 - -## Checklist -- [ ] package.jsonのバージョンが正しく更新されている -- [ ] CHANGELOGが過不足無く更新されている -- [ ] CIが全て通っている diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e78b82c47c..2848050d2e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ ## What @@ -17,7 +17,7 @@ https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md ## Checklist -- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Read the [contribution guide](https://forge.yumechi.jp/yume/yumechi-no-kuni/src/branch/master/CONTRIBUTING.md) - [ ] Test working in a local environment - [ ] (If needed) Add story of storybook - [ ] (If needed) Update CHANGELOG.md diff --git a/.okteto/okteto-pipeline.yml b/.okteto/okteto-pipeline.yml deleted file mode 100644 index e2996fbbc9..0000000000 --- a/.okteto/okteto-pipeline.yml +++ /dev/null @@ -1,6 +0,0 @@ -build: - misskey: - args: - - NODE_ENV=development -deploy: - - helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development diff --git a/README.md b/README.md index 92e8fef639..9fb7a31652 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,31 @@ -
- - Misskey logo - +# ゆめちのくに -**🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀** +YumechiNoKuni is a fork of Misskey, with a focus on security, observability and reliability. -[Learn more](https://misskey-hub.net/) +[mi.yumechi.jp](https://mi.yumechi.jp) is running this version. ---- +[Learn more about Misskey](https://misskey-hub.net/) - - find an instance +## Main differences - - create an instance +### Unique features - - become a contributor +- Strict ActivityPub sanitization by whitelisting properties and normalizing all referential properties. +- Strict Content Security Policy. +- Require TLSv1.2+ over port 443 for all ActivityPub requests. +- Strongly-typed inbox filtering in Rust. +- Reduce needless retries by marking more errors as permanent. +- Detailed prometheus metrics for slow requests, DB queries, AP processing, failed auths, etc. +- Disable unauthenticated media processing and use custom AppArmored media proxy. +- Enable active users in nodeinfo back. +- Advertise Git information over nodeinfo for better observability and easy tracking of the actual code running. +- Logical replication for the database over mTLS. +- More atomic operations in API handlers. - - join the community +### Picked from github.com/paricafe/misskey - - become a patron +- pgroonga full-text search (with modifications). +- Better Service Worker caching. +- Better hashtag statistics. +- Better handling of deep recursive AP objects. -
- -## Thanks - -Sentry - -Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors. - -Chromatic - -Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions. - -Codecov - -Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage. - -Crowdin - -Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages. - -Docker - -Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production. diff --git a/SECURITY.md b/SECURITY.md index 04567baf07..e50dec6d98 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,15 +1,12 @@ # Reporting Security Issues -If you discover a security issue in Misskey, please report it by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**. +If you discover a security issue in this project, please use the `git blame` command to identify the source of the issue, +if it was introduced by this fork please contact me at secityyumechi.jp. -This will allow us to assess the risk, and make a fix available before we add a -bug report to the GitHub repository. +For upstream issues please report by **[this form](https://github.com/misskey-dev/misskey/security/advisories/new)**. -Thanks for helping make Misskey safe for everyone. +Thanks for helping make YumechiNoKuni safe for everyone. ## When create a patch -If you can also create a patch to fix the vulnerability, please create a PR on the private fork. - -> [!note] -> There is a GitHub bug that prevents merging if a PR not following the develop branch of upstream, so please keep follow the develop branch. +If you can also create a patch to fix the vulnerability, please send a diff file with the report. diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 88aabda04f..339c9510f4 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -9,17 +9,6 @@ block loadClientEntry doctype html // - - - _____ _ _ - | |_|___ ___| |_ ___ _ _ - | | | | |_ -|_ -| '_| -_| | | - |_|_|_|_|___|___|_,_|___|_ | - |___| - Thank you for using Misskey! - If you are reading this message... how about joining the development? - https://github.com/misskey-dev/misskey - - html head diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug index 44ebf53cf7..4d0863667d 100644 --- a/packages/backend/src/server/web/views/error.pug +++ b/packages/backend/src/server/web/views/error.pug @@ -1,17 +1,6 @@ doctype html // - - - _____ _ _ - | |_|___ ___| |_ ___ _ _ - | | | | |_ -|_ -| '_| -_| | | - |_|_|_|_|___|___|_,_|___|_ | - |___| - Thank you for using Misskey! - If you are reading this message... how about joining the development? - https://github.com/misskey-dev/misskey - - html head diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue index e5e57c05c4..219dd75412 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only - {{ i18n.ts.aboutMisskey }} + {{ i18n.ts.aboutMisskey }} (Upstream) diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 7d8677e3be..cc11d93f84 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -5,8 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only