From f85aa7b6415eaf61d213deb370f598f603f3fe9b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:29:05 +0900 Subject: [PATCH 01/12] =?UTF-8?q?fix(backend):=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=8C=E3=82=B5=E3=82=A4=E3=82=BA=E3=81=AE?= =?UTF-8?q?=E5=88=B6=E9=99=90=E3=82=92=E8=B6=85=E3=81=88=E3=81=A6=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=9F=E9=9A=9B=E3=81=AB=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=95=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- packages/backend/src/server/api/ApiCallService.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da204c9198..ad070cc981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - ### Server -- +- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 ## 2024.8.0 diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 47f64f6609..ea852d590d 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -199,9 +199,17 @@ export class ApiCallService implements OnApplicationShutdown { return; } - const [path] = await createTemp(); + const [path, cleanup] = await createTemp(); await stream.pipeline(multipartData.file, fs.createWriteStream(path)); + // ファイルサイズが制限を超えていた場合 + if (multipartData.file.truncated) { + cleanup(); + reply.code(413); + reply.send(); + return; + } + const fields = {} as Record; for (const [k, v] of Object.entries(multipartData.fields)) { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; From 2f009f7d49e2468da6950c7ff8448565b095df38 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:31:16 +0900 Subject: [PATCH 02/12] add note --- packages/backend/src/server/api/ApiCallService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index ea852d590d..f95c272757 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -203,6 +203,7 @@ export class ApiCallService implements OnApplicationShutdown { await stream.pipeline(multipartData.file, fs.createWriteStream(path)); // ファイルサイズが制限を超えていた場合 + // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある if (multipartData.file.truncated) { cleanup(); reply.code(413); From 8032a4e12ad0425b3a2b4d7a857f12de656a718d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:40:11 +0900 Subject: [PATCH 03/12] =?UTF-8?q?enhance(frontend):=20=E3=82=B5=E3=82=A4?= =?UTF-8?q?=E3=82=BA=E5=88=B6=E9=99=90=E3=82=92=E8=B6=85=E9=81=8E=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=97=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=A8=E3=81=97=E3=81=9F=E9=9A=9B=E3=81=AB=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=92=E5=87=BA=E3=81=99=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- packages/backend/src/config.ts | 4 ++-- packages/backend/src/core/DownloadService.ts | 2 +- .../backend/src/core/entities/MetaEntityService.ts | 1 + packages/backend/src/models/json-schema/meta.ts | 4 ++++ packages/backend/src/server/api/ApiServerService.ts | 2 +- packages/frontend/src/scripts/upload.ts | 10 ++++++++++ packages/misskey-js/src/autogen/types.ts | 1 + 8 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad070cc981..6359033c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - ### Client -- +- サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように ### Server - ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 3e5a1e81cd..cff0194780 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -133,7 +133,7 @@ export type Config = { proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; - maxFileSize: number | undefined; + maxFileSize: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -250,7 +250,7 @@ export function loadConfig(): Config { proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, - maxFileSize: config.maxFileSize, + maxFileSize: config.maxFileSize ?? 262144000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 21ae798f9f..93f4a38246 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -42,7 +42,7 @@ export class DownloadService { const timeout = 30 * 1000; const operationTimeout = 60 * 1000; - const maxSize = this.config.maxFileSize ?? 262144000; + const maxSize = this.config.maxFileSize; const urlObj = new URL(url); let filename = urlObj.pathname.split('/').pop() ?? 'untitled'; diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 44ec0d6a7b..f4b1e302d0 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -129,6 +129,7 @@ export class MetaEntityService { mediaProxy: this.config.mediaProxy, enableUrlPreview: instance.urlPreviewEnabled, noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', + maxFileSize: this.config.maxFileSize, }; return packed; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 3bcf9cac92..99feeaa7d7 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -253,6 +253,10 @@ export const packedMetaLiteSchema = { optional: false, nullable: false, default: 'local', }, + maxFileSize: { + type: 'number', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 4a5935f930..13cbdfc3be 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -49,7 +49,7 @@ export class ApiServerService { fastify.register(multipart, { limits: { - fileSize: this.config.maxFileSize ?? 262144000, + fileSize: this.config.maxFileSize, files: 1, }, }); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 3e947183c9..abb0e1e677 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -13,6 +13,7 @@ import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; type Uploading = { id: string; @@ -39,6 +40,15 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; + if (file.size > instance.maxFileSize) { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + return Promise.reject(); + } + return new Promise((resolve, reject) => { const id = uuid(); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 6d2f787767..0c50825203 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4947,6 +4947,7 @@ export type components = { * @enum {string} */ noteSearchableScope: 'local' | 'global'; + maxFileSize: number; }; MetaDetailedOnly: { features?: { From 44f62160cb4b876f415b48b0574592f87bea9b3d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:59:17 +0900 Subject: [PATCH 04/12] enhance(frontend): error message i18n --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/frontend/src/scripts/get-note-menu.ts | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index 75e1703b4a..9fd3441ab1 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5068,6 +5068,10 @@ export interface Locale extends ILocale { * 作成したアンテナ */ "createdAntennas": string; + /** + * これ以上このクリップにノートを追加できません。 + */ + "clipNoteLimitExceeded": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 98e3cbfa41..587b67d987 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1263,6 +1263,7 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示 sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?" createdLists: "作成したリスト" createdAntennas: "作成したアンテナ" +clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" _delivery: status: "配信状態" diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 2563b0baf3..b5d7350a41 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -66,6 +66,11 @@ export async function getNoteClipMenu(props: { }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } + } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { + os.alert({ + type: 'error', + text: i18n.ts.clipNoteLimitExceeded, + }); } else { os.alert({ type: 'error', From 255c8bd1b9a35b36f33b2e524a7146de106d387e Mon Sep 17 00:00:00 2001 From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:55:38 +0900 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20=E6=8A=95=E7=A8=BF=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=AE=E5=AD=97=E6=95=B0=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E8=A8=88=E7=AE=97=E3=82=92=E5=AE=9F=E9=9A=9B=E3=81=AE?= =?UTF-8?q?=E6=8A=95=E7=A8=BF=E5=86=85=E5=AE=B9=E3=81=AB=E5=90=88=E3=82=8F?= =?UTF-8?q?=E3=81=9B=E3=82=8B=20(#14466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkPostForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f0826fcf4e..df251d9192 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -245,7 +245,7 @@ const submitText = computed((): string => { }); const textLength = computed((): number => { - return (text.value + imeText.value).trim().length; + return (text.value + imeText.value).length; }); const maxTextLength = computed((): number => { From 36dff6688323485fa0d47a74b6db1b7b8d393325 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:36:43 +0900 Subject: [PATCH 06/12] refactor --- packages/backend/src/server/web/boot.js | 6 +++--- packages/frontend/src/_boot_.ts | 2 ++ packages/frontend/src/_dev_boot_.ts | 5 ----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 5e52360456..5283596316 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -166,7 +166,7 @@ if (!errorsElement) { document.body.innerHTML = ` - + @@ -178,7 +178,7 @@

The following actions may solve the problem. / 以下を行うと解決する可能性があります。

Update your os and browser / ブラウザおよびOSを最新バージョンに更新する

Disable an adblocker / アドブロッカーを無効にする

-

Clear the browser cache / ブラウザのキャッシュをクリアする

+

Clear the browser cache / ブラウザのキャッシュをクリアする

(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する

Other options / その他のオプション @@ -320,6 +320,6 @@ #errorInfo { width: 50%; } - }`) + }`); } })(); diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 875353f8a4..13a97e433c 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -6,6 +6,8 @@ // https://vitejs.dev/config/build-options.html#build-modulepreload import 'vite/modulepreload-polyfill'; +import '@tabler/icons-webfont/dist/tabler-icons.scss'; + import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index 7c6e537fbc..1601f247d7 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -3,11 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。 -// よって、devモードとして起動されるときはビルド時に組み込む形としておく。 -// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない) -import '@tabler/icons-webfont/dist/tabler-icons.scss'; - await main(); import('@/_boot_.js'); From b6fdd7195725d09ab30245ac174d0ea04ef550ae Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:40:11 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=E6=B6=88=E3=81=97=E5=BF=98=E3=82=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/web/views/base.pug | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 151b7bca6c..da6d1eafd3 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -36,8 +36,6 @@ html link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) - //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists From 3e85052754b1c86e3c3b16d2e29b5aef6db04ccf Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:57:44 +0900 Subject: [PATCH 08/12] fix(backend): correct `app`-type notification schema (#14471) --- packages/backend/src/models/Notification.ts | 2 +- packages/backend/src/models/json-schema/notification.ts | 4 ++-- packages/misskey-js/src/autogen/types.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index df88b99636..87d8c16cb3 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -85,7 +85,7 @@ export type MiNotification = { /** * アプリ通知のbody */ - customBody: string | null; + customBody: string; /** * アプリ通知のheader diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index b4c4442758..6f5fb8247b 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -311,11 +311,11 @@ export const packedNotificationSchema = { }, header: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, icon: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, }, }, { diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 0c50825203..3f866a9ab3 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4253,7 +4253,7 @@ export type components = { /** @enum {string} */ type: 'achievementEarned'; achievement: string; - } | { + } | ({ /** Format: id */ id: string; /** Format: date-time */ @@ -4261,9 +4261,9 @@ export type components = { /** @enum {string} */ type: 'app'; body: string; - header: string; - icon: string; - } | { + header: string | null; + icon: string | null; + }) | { /** Format: id */ id: string; /** Format: date-time */ From 06855f769f1fe8c84fc3bbef615dac0a9fd2cf7b Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:58:11 +0900 Subject: [PATCH 09/12] refactor(sw): use fully typed locales (#14470) * refactor(sw): use fully typed locales * fix(backend): enumerate achievement notification type --- .../src/models/json-schema/notification.ts | 2 + packages/frontend/src/scripts/i18n.ts | 49 ----------------- packages/frontend/test/i18n.test.ts | 52 +++++++++++++++++++ packages/misskey-js/src/autogen/types.ts | 7 +-- .../sw/src/scripts/create-notification.ts | 50 +++++++++--------- packages/sw/src/scripts/i18n.ts | 37 ------------- packages/sw/src/scripts/lang.ts | 5 +- packages/sw/src/sw.ts | 7 +-- 8 files changed, 89 insertions(+), 120 deletions(-) create mode 100644 packages/frontend/test/i18n.test.ts delete mode 100644 packages/sw/src/scripts/i18n.ts diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 6f5fb8247b..b05ec8b762 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; import { notificationTypes } from '@/types.js'; const baseSchema = { @@ -294,6 +295,7 @@ export const packedNotificationSchema = { achievement: { type: 'string', optional: false, nullable: false, + enum: ACHIEVEMENT_TYPES, }, }, }, { diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts index c2f44a33cc..b258a2a678 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend/src/scripts/i18n.ts @@ -137,7 +137,6 @@ export class I18n { return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.tsxCache) { return this.tsxCache; } @@ -244,51 +243,3 @@ export class I18n { return str; } } - -if (import.meta.vitest) { - const { describe, expect, it } = import.meta.vitest; - - describe('i18n', () => { - it('t', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.t('foo')).toBe('foo'); - expect(i18n.t('bar.baz')).toBe('baz'); - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - it('ts', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.ts.foo).toBe('foo'); - expect(i18n.ts.bar.baz).toBe('baz'); - }); - it('tsx', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - }); -} diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts new file mode 100644 index 0000000000..e1cab1f15f --- /dev/null +++ b/packages/frontend/test/i18n.test.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, it } from 'vitest'; +import { I18n } from '@/scripts/i18n.js'; +import { ParameterizedString } from '../../../locales/index.js'; + +describe('i18n', () => { + it('t', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.t('foo')).toBe('foo'); + expect(i18n.t('bar.baz')).toBe('baz'); + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + it('ts', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.ts.foo).toBe('foo'); + expect(i18n.ts.bar.baz).toBe('baz'); + }); + it('tsx', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); +}); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 3f866a9ab3..37f1bf2d38 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4245,15 +4245,16 @@ export type components = { /** @enum {string} */ type: 'roleAssigned'; role: components['schemas']['Role']; - } | { + } | ({ /** Format: id */ id: string; /** Format: date-time */ createdAt: string; /** @enum {string} */ type: 'achievementEarned'; - achievement: string; - } | ({ + /** @enum {string} */ + achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; + }) | ({ /** Format: id */ id: string; /** Format: date-time */ diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 7d28d8a694..02d9b07767 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -41,11 +41,10 @@ export async function createNotification { const i18n = await (swLang.i18n ?? swLang.fetchLocale()); - const { t } = i18n; switch (data.type) { /* case 'driveFileCreated': // TODO (Server Side) - return [t('_notification.fileUploaded'), { + return [i18n.ts._notification.fileUploaded, { body: body.name, icon: body.url, data @@ -58,7 +57,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif const account = await getAccountFromId(data.userId); if (!account) return null; const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token); - return [t('_notification.youWereFollowed'), { + return [i18n.ts._notification.youWereFollowed, { body: getUserName(data.body.user), icon: data.body.user.avatarUrl, badge: iconUrl('user-plus'), @@ -66,14 +65,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif actions: userDetail.isFollowing ? [] : [ { action: 'follow', - title: t('_notification._actions.followBack'), + title: i18n.ts._notification._actions.followBack, }, ], }]; } case 'mention': - return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotMention({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', icon: data.body.user.avatarUrl, badge: iconUrl('at'), @@ -81,13 +80,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ], }]; case 'reply': - return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotReply({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', icon: data.body.user.avatarUrl, badge: iconUrl('arrow-back-up'), @@ -95,13 +94,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ], }]; case 'renote': - return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youRenoted({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', icon: data.body.user.avatarUrl, badge: iconUrl('repeat'), @@ -115,7 +114,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; case 'quote': - return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotQuote({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', icon: data.body.user.avatarUrl, badge: iconUrl('quote'), @@ -123,19 +122,19 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ { action: 'renote', - title: t('_notification._actions.renote'), + title: i18n.ts._notification._actions.renote, }, ] : []), ], }]; case 'note': - return [t('_notification.newNote') + ': ' + getUserName(data.body.user), { + return [i18n.ts._notification.newNote + ': ' + getUserName(data.body.user), { body: data.body.note.text ?? '', icon: data.body.user.avatarUrl, data, @@ -178,7 +177,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif } case 'receiveFollowRequest': - return [t('_notification.youReceivedFollowRequest'), { + return [i18n.ts._notification.youReceivedFollowRequest, { body: getUserName(data.body.user), icon: data.body.user.avatarUrl, badge: iconUrl('user-plus'), @@ -186,17 +185,17 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif actions: [ { action: 'accept', - title: t('accept'), + title: i18n.ts.accept, }, { action: 'reject', - title: t('reject'), + title: i18n.ts.reject, }, ], }]; case 'followRequestAccepted': - return [t('_notification.yourFollowRequestAccepted'), { + return [i18n.ts._notification.yourFollowRequestAccepted, { body: getUserName(data.body.user), icon: data.body.user.avatarUrl, badge: iconUrl('circle-check'), @@ -204,15 +203,15 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; case 'achievementEarned': - return [t('_notification.achievementEarned'), { - body: t(`_achievements._types._${data.body.achievement}.title`), + return [i18n.ts._notification.achievementEarned, { + body: i18n.ts._achievements._types[`_${data.body.achievement}`].title, badge: iconUrl('medal'), data, tag: `achievement:${data.body.achievement}`, }]; case 'pollEnded': - return [t('_notification.pollEnded'), { + return [i18n.ts._notification.pollEnded, { body: data.body.note.text ?? '', badge: iconUrl('chart-arrows'), data, @@ -226,8 +225,8 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; case 'test': - return [t('_notification.testNotification'), { - body: t('_notification.notificationWillBeDisplayedLikeThis'), + return [i18n.ts._notification.testNotification, { + body: i18n.ts._notification.notificationWillBeDisplayedLikeThis, badge: iconUrl('bell'), data, }]; @@ -236,7 +235,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif return null; } case 'unreadAntennaNote': - return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), { + return [i18n.tsx._notification.unreadAntennaNote({ name: data.body.antenna.name }), { body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`, icon: data.body.note.user.avatarUrl, badge: iconUrl('antenna'), @@ -252,7 +251,6 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif export async function createEmptyNotification(): Promise { return new Promise(async res => { const i18n = await (swLang.i18n ?? swLang.fetchLocale()); - const { t } = i18n; await globalThis.registration.showNotification( (new URL(origin)).host, @@ -264,11 +262,11 @@ export async function createEmptyNotification(): Promise { actions: [ { action: 'markAllAsRead', - title: t('markAllAsRead'), + title: i18n.ts.markAllAsRead, }, { action: 'settings', - title: t('notificationSettings'), + title: i18n.ts.notificationSettings, }, ], data: {}, diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts deleted file mode 100644 index 77b955dbe8..0000000000 --- a/packages/sw/src/scripts/i18n.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type Locale = { [key: string]: string | Locale }; - -export class I18n { - public ts: T; - - constructor(locale: T) { - this.ts = locale; - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record): string { - try { - let str = key.split('.').reduce((o, i) => o[i], this.ts); - if (typeof str !== 'string') throw new Error(); - - if (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v); - } - } - return str; - } catch (err) { - console.warn(`missing localization '${key}'`); - return key; - } - } -} diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts index 6fccedd746..0db4cc6381 100644 --- a/packages/sw/src/scripts/lang.ts +++ b/packages/sw/src/scripts/lang.ts @@ -7,7 +7,8 @@ * Language manager for SW */ import { get, set } from 'idb-keyval'; -import { I18n, type Locale } from '@/scripts/i18n.js'; +import { I18n } from '../../../frontend/src/scripts/i18n.js'; +import type { Locale } from '../../../../locales/index.js'; class SwLang { public cacheName = `mk-cache-${_VERSION_}`; @@ -23,7 +24,7 @@ class SwLang { return this.fetchLocale(); } - public i18n: Promise | null = null; + public i18n: Promise> | null = null; public fetchLocale(): Promise> { return (this.i18n = this._fetch()); diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index cc79d88713..7a0010992e 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -6,7 +6,8 @@ import { get } from 'idb-keyval'; import * as Misskey from 'misskey-js'; import type { PushNotificationDataMap } from '@/types.js'; -import type { I18n, Locale } from '@/scripts/i18n.js'; +import type { I18n } from '../../frontend/src/scripts/i18n.js'; +import type { Locale } from '../../../locales/index.js'; import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js'; import { swLang } from '@/scripts/lang.js'; import * as swos from '@/scripts/operations.js'; @@ -30,8 +31,8 @@ globalThis.addEventListener('activate', ev => { 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', + 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', }; From 7fe303505961c764561b93e3c34b9e8e83d70698 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:58:59 +0900 Subject: [PATCH 10/12] fix(backend): use `prefixItems` in `admin/queue/*-delayed` endpoint schema (#14468) * fix(backend): represent tuples with `prefixItems` * refactor(frontend): fix type errors * fix(backend): add `prefixItems` in `SchemaType` * fix(backend): add `unevaluatedItems: false` to disallow extra items * refactor(frontend): consolidate `'deliver' | 'queue'` type def into `queue.vue` * fix(backend): add `unevaluatedItems` in `SchemaType` --- packages/backend/src/misc/json-schema.ts | 9 ++++++ .../endpoints/admin/queue/deliver-delayed.ts | 19 ++++++------ .../endpoints/admin/queue/inbox-delayed.ts | 19 ++++++------ .../src/pages/admin/overview.queue.vue | 20 +++++++------ .../frontend/src/pages/admin/queue.chart.vue | 30 +++++++++---------- packages/frontend/src/pages/admin/queue.vue | 6 ++-- packages/misskey-js/src/autogen/types.ts | 4 +-- 7 files changed, 59 insertions(+), 48 deletions(-) diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index a721b8663c..040e36228c 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -144,7 +144,9 @@ export interface Schema extends OfSchema { readonly type?: TypeStringef; readonly nullable?: boolean; readonly optional?: boolean; + readonly prefixItems?: ReadonlyArray; readonly items?: Schema; + readonly unevaluatedItems?: Schema | boolean; readonly properties?: Obj; readonly required?: ReadonlyArray, string>>; readonly description?: string; @@ -198,6 +200,7 @@ type UnionSchemaType = X //type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; type UnionObjType = a[number]> = X extends any ? ObjType : never; type ArrayUnion = T extends any ? Array : never; +type ArrayToTuple> = { [K in keyof X]: SchemaType }; type ObjectSchemaTypeDef

= p['ref'] extends keyof typeof refs ? Packed : @@ -232,6 +235,12 @@ export type SchemaTypeDef

= p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : never ) : + p['prefixItems'] extends ReadonlyArray ? ( + p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType[]] : + p['items'] extends false ? ArrayToTuple : + p['unevaluatedItems'] extends false ? ArrayToTuple : + [...ArrayToTuple, ...unknown[]] + ) : p['items'] extends NonNullable ? SchemaType[] : any[] ) : diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 7a3410ffa7..f3e440b4cb 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -21,16 +21,15 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - }, + prefixItems: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + unevaluatedItems: false, }, example: [[ 'example.com', diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index 305ae1af1d..e7589cba81 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -21,16 +21,15 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - }, + prefixItems: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + unevaluatedItems: false, }, example: [[ 'example.com', diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index c7478f252a..fb190f5325 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only