From 6c5593d45660de8c63331671a6a1999ddc5961bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Aug 2024 08:08:49 +0000 Subject: [PATCH 001/159] [skip ci] Update CHANGELOG.md (prepend template) --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8873ba0..da204c919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## Unreleased + +### General +- + +### Client +- + +### Server +- + + ## 2024.8.0 ### General From e78110a5cd7891f89682a9c58a4a3648d6940205 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:13:32 +0900 Subject: [PATCH 002/159] refactor --- packages/frontend/src/themes/_dark.json5 | 1 - packages/frontend/src/themes/_light.json5 | 1 - packages/frontend/src/ui/_common_/navbar-for-mobile.vue | 6 ++++-- packages/frontend/src/ui/_common_/navbar.vue | 9 +++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 index c82a95686..a310fb716 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -89,7 +89,6 @@ X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', X15: ':alpha<0<@panel', X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 index 63bc03091..827a935a0 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -89,7 +89,6 @@ X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', X15: ':alpha<0<@panel', X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 87e9e45e6..e80d5fd39 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -82,6 +82,8 @@ function more() { diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index ce1d4e48d..1938b8d48 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -96,6 +96,7 @@ const decorationsForPreview = computed(() => { flipH: flipH.value, offsetX: offsetX.value, offsetY: offsetY.value, + blink: true, }; const decorations = [...$i.avatarDecorations]; if (props.usingIndex != null) { From 21a3095eb0d0ce8f385b4450f371c2160ea18b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:39:01 +0900 Subject: [PATCH 006/159] fix button translation (#14444) that Japanese string exactly matches that i18n key (cherry picked from commit a408d32bb72ada9a4ad6bd1afe6e3fadb9b403db) Co-authored-by: dakkar --- packages/frontend/src/pages/admin/abuses.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 9a9fa472a..0b9847fed 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ "通知設定" }} + {{ i18n.ts.notificationSetting }}
From 043ab1f69b78af5caea47644a2878cae3cdbe141 Mon Sep 17 00:00:00 2001 From: atsuchan <83960488+atsu1125@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:49:42 +0900 Subject: [PATCH 007/159] fix(backend): Fix chart generation non-matching blockedHosts (#14441) --- packages/backend/src/core/chart/charts/federation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index c2329a2f7..f40a26495 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -65,21 +65,21 @@ export default class FederationChart extends Chart { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -88,7 +88,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -96,7 +96,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() From 1008fa32a059f4c66c17a2ab77c3d5595d998233 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:03:11 +0900 Subject: [PATCH 008/159] better boot error screen --- packages/backend/src/server/web/boot.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 4275dc952..5e5236045 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -176,10 +176,10 @@ Reload / リロード

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

-

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

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

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

-

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

+

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

+

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

Other options / その他のオプション @@ -212,7 +212,7 @@ ERROR CODE: ${code} - ${JSON.stringify(details)}`; + ${details.toString()} ${JSON.stringify(details)}`; errorsElement.appendChild(detailsElement); addStyle(` * { 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 009/159] =?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 da204c919..ad070cc98 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 47f64f660..ea852d590 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 010/159] 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 ea852d590..f95c27275 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 011/159] =?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 ad070cc98..6359033c1 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 3e5a1e81c..cff019478 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 21ae798f9..93f4a3824 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 44ec0d6a7..f4b1e302d 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 3bcf9cac9..99feeaa7d 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 4a5935f93..13cbdfc3b 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 3e947183c..abb0e1e67 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 6d2f78776..0c5082520 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 012/159] 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 75e1703b4..9fd3441ab 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 98e3cbfa4..587b67d98 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 2563b0baf..b5d7350a4 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 013/159] =?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 f0826fcf4..df251d919 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 014/159] 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 5e5236045..528359631 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 875353f8a..13a97e433 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 7c6e537fb..1601f247d 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 015/159] =?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 151b7bca6..da6d1eafd 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 016/159] 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 df88b9963..87d8c16cb 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 b4c444275..6f5fb8247 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 0c5082520..3f866a9ab 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 017/159] 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 6f5fb8247..b05ec8b76 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 c2f44a33c..b258a2a67 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 000000000..e1cab1f15 --- /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 3f866a9ab..37f1bf2d3 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 7d28d8a69..02d9b0776 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 77b955dbe..000000000 --- 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 6fccedd74..0db4cc638 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 cc79d8871..7a0010992 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 018/159] 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 a721b8663..040e36228 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 7a3410ffa..f3e440b4c 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 305ae1af1..e7589cba8 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 c7478f252..fb190f532 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 + + From cdb0566c5b823f0ce4ecc493bd459cb726431be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:12:14 +0900 Subject: [PATCH 024/159] =?UTF-8?q?refactor(frontend):=20scss=20deprecated?= =?UTF-8?q?=20=E8=AD=A6=E5=91=8A=E3=81=AB=E5=AF=BE=E5=BF=9C=20(#14513)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/components/MkModalWindow.vue | 6 +++--- packages/frontend/src/components/MkSuperMenu.vue | 6 +++--- packages/frontend/src/components/MkWindow.vue | 8 ++++---- .../frontend/src/pages/admin/overview.users.vue | 8 ++++---- packages/frontend/src/pages/page.vue | 3 +-- packages/frontend/src/style.scss | 8 ++++---- packages/frontend/src/ui/_common_/statusbars.vue | 16 ++++++++-------- packages/frontend/src/ui/deck/column.vue | 6 +++--- 8 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index c3c781203..f26959888 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -94,12 +94,12 @@ defineExpose({ --root-margin: 24px; + --headerHeight: 46px; + --headerHeightNarrow: 42px; + @media (max-width: 500px) { --root-margin: 16px; } - - --headerHeight: 46px; - --headerHeightNarrow: 42px; } .header { diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 1a880170b..3746ffd8f 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -100,14 +100,14 @@ defineProps<{ &.grid { > .group { + margin-left: 0; + margin-right: 0; + & + .group { padding-top: 0; border-top: none; } - margin-left: 0; - margin-right: 0; - > .title { font-size: 1em; opacity: 0.7; diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 303e49de0..26ba59849 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -508,10 +508,6 @@ defineExpose({ .header { --height: 39px; - &.mini { - --height: 32px; - } - display: flex; position: relative; z-index: 1; @@ -524,6 +520,10 @@ defineExpose({ //border-bottom: solid 1px var(--divider); font-size: 90%; font-weight: bold; + + &.mini { + --height: 32px; + } } .headerButton { diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 408be88d4..a7dd4c0a4 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, { .root { &:global { > .users { - .chart-move { - transition: transform 1s ease; - } - display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; + .chart-move { + transition: transform 1s ease; + } + > .user:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index cb1ce9b91..7ae61236e 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -433,13 +433,12 @@ definePageMetadata(() => ({ .pageBannerTitleUser { --height: 32px; flex-shrink: 0; + line-height: var(--height); .avatar { height: var(--height); width: var(--height); } - - line-height: var(--height); } .pageBannerTitleSubActions { diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 44ef740a2..caaf9fca6 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -17,10 +17,6 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); - @media (max-width: 500px) { - --margin: var(--marginHalf); - } - //--ad: rgb(255 169 0 / 10%); --eventFollow: #36aed2; --eventRenote: #36d298; @@ -29,6 +25,10 @@ --eventReaction: #e99a0b; --eventAchievement: #cb9a11; --eventOther: #88a6b7; + + @media (max-width: 500px) { + --margin: var(--marginHalf); + } } ::selection { diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 872c69810..690366307 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') --nameMargin: 10px; font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + &.verySmall { --nameMargin: 7px; --height: 16px; @@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') font-size: 0.9em; } - display: flex; - vertical-align: bottom; - width: 100%; - line-height: var(--height); - height: var(--height); - overflow: clip; - contain: strict; - &.black { background: #000; color: #fff; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index e96402d13..893301122 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -324,11 +324,11 @@ function onDrop(ev) { > .body { background: transparent !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: transparent; } - scrollbar-color: var(--scrollbarHandle) transparent; } } @@ -338,11 +338,11 @@ function onDrop(ev) { > .body { background: var(--bg) !important; overflow-y: scroll !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: inherit; } - scrollbar-color: var(--scrollbarHandle) transparent; } } } @@ -423,10 +423,10 @@ function onDrop(ev) { box-sizing: border-box; container-type: size; background-color: var(--bg); + scrollbar-color: var(--scrollbarHandle) var(--panel); &::-webkit-scrollbar-track { background: var(--panel); } - scrollbar-color: var(--scrollbarHandle) var(--panel); } From 8d19bdbb65c79e2425bf5c73fd8b6310670a8c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:22:45 +0900 Subject: [PATCH 025/159] =?UTF-8?q?fix(misskey-js):=20content-type?= =?UTF-8?q?=E3=81=AFapplication/json=E3=81=A7=E3=81=AA=E3=81=84=E3=82=82?= =?UTF-8?q?=E3=81=AE=E3=81=AE=E3=81=BF=E3=82=92=E8=A8=98=E9=8C=B2=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../misskey-js/generator/src/generator.ts | 21 +- packages/misskey-js/src/api.ts | 9 +- packages/misskey-js/src/autogen/endpoint.ts | 385 +----------------- 3 files changed, 22 insertions(+), 393 deletions(-) diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts index 4ae00a452..88f2ae9ee 100644 --- a/packages/misskey-js/generator/src/generator.ts +++ b/packages/misskey-js/generator/src/generator.ts @@ -96,15 +96,11 @@ async function generateEndpoints( endpoint.request = req; const reqType = new EndpointReqMediaType(path, req); - endpointReqMediaTypesSet.add(reqType.getMediaType()); - endpointReqMediaTypes.push(reqType); - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); + if (reqType.getMediaType() !== 'application/json') { + endpointReqMediaTypesSet.add(reqType.getMediaType()); + endpointReqMediaTypes.push(reqType); + } } - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) { @@ -158,16 +154,19 @@ async function generateEndpoints( endpointOutputLine.push(''); function generateEndpointReqMediaTypesType() { - return `Record `'${t}'`).join(' | ')}>`; + return `{ [K in keyof Endpoints]?: ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}; }`; } - endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`); + endpointOutputLine.push(`/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */`); + endpointOutputLine.push('export const endpointReqTypes = {'); endpointOutputLine.push( ...endpointReqMediaTypes.map(it => '\t' + it.toLine()), ); - endpointOutputLine.push('};'); + endpointOutputLine.push(`} as const satisfies ${generateEndpointReqMediaTypesType()};`); endpointOutputLine.push(''); await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index ea1df57f3..659a29a22 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -56,6 +56,10 @@ export class APIClient { return obj !== null && typeof obj === 'object' && !Array.isArray(obj); } + private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes { + return ep in endpointReqTypes; + } + public request( endpoint: E, params: P = {} as P, @@ -63,9 +67,10 @@ export class APIClient { ): Promise> { return new Promise((resolve, reject) => { let mediaType = 'application/json'; - if (endpoint in endpointReqTypes) { + if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) { mediaType = endpointReqTypes[endpoint]; } + let payload: FormData | string = '{}'; if (mediaType === 'application/json') { @@ -100,7 +105,7 @@ export class APIClient { method: 'POST', body: payload, headers: { - 'Content-Type': endpointReqTypes[endpoint], + 'Content-Type': mediaType, }, credentials: 'omit', cache: 'no-cache', diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index be41951e4..8fbdbbb62 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -955,384 +955,9 @@ export type Endpoints = { 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse }; } -export const endpointReqTypes: Record = { - 'admin/meta': 'application/json', - 'admin/abuse-user-reports': 'application/json', - 'admin/abuse-report/notification-recipient/list': 'application/json', - 'admin/abuse-report/notification-recipient/show': 'application/json', - 'admin/abuse-report/notification-recipient/create': 'application/json', - 'admin/abuse-report/notification-recipient/update': 'application/json', - 'admin/abuse-report/notification-recipient/delete': 'application/json', - 'admin/accounts/create': 'application/json', - 'admin/accounts/delete': 'application/json', - 'admin/accounts/find-by-email': 'application/json', - 'admin/ad/create': 'application/json', - 'admin/ad/delete': 'application/json', - 'admin/ad/list': 'application/json', - 'admin/ad/update': 'application/json', - 'admin/announcements/create': 'application/json', - 'admin/announcements/delete': 'application/json', - 'admin/announcements/list': 'application/json', - 'admin/announcements/update': 'application/json', - 'admin/avatar-decorations/create': 'application/json', - 'admin/avatar-decorations/delete': 'application/json', - 'admin/avatar-decorations/list': 'application/json', - 'admin/avatar-decorations/update': 'application/json', - 'admin/delete-all-files-of-a-user': 'application/json', - 'admin/unset-user-avatar': 'application/json', - 'admin/unset-user-banner': 'application/json', - 'admin/drive/clean-remote-files': 'application/json', - 'admin/drive/cleanup': 'application/json', - 'admin/drive/files': 'application/json', - 'admin/drive/show-file': 'application/json', - 'admin/emoji/add-aliases-bulk': 'application/json', - 'admin/emoji/add': 'application/json', - 'admin/emoji/copy': 'application/json', - 'admin/emoji/delete-bulk': 'application/json', - 'admin/emoji/delete': 'application/json', - 'admin/emoji/import-zip': 'application/json', - 'admin/emoji/list-remote': 'application/json', - 'admin/emoji/list': 'application/json', - 'admin/emoji/remove-aliases-bulk': 'application/json', - 'admin/emoji/set-aliases-bulk': 'application/json', - 'admin/emoji/set-category-bulk': 'application/json', - 'admin/emoji/set-license-bulk': 'application/json', - 'admin/emoji/update': 'application/json', - 'admin/federation/delete-all-files': 'application/json', - 'admin/federation/refresh-remote-instance-metadata': 'application/json', - 'admin/federation/remove-all-following': 'application/json', - 'admin/federation/update-instance': 'application/json', - 'admin/get-index-stats': 'application/json', - 'admin/get-table-stats': 'application/json', - 'admin/get-user-ips': 'application/json', - 'admin/invite/create': 'application/json', - 'admin/invite/list': 'application/json', - 'admin/promo/create': 'application/json', - 'admin/queue/clear': 'application/json', - 'admin/queue/deliver-delayed': 'application/json', - 'admin/queue/inbox-delayed': 'application/json', - 'admin/queue/promote': 'application/json', - 'admin/queue/stats': 'application/json', - 'admin/relays/add': 'application/json', - 'admin/relays/list': 'application/json', - 'admin/relays/remove': 'application/json', - 'admin/reset-password': 'application/json', - 'admin/resolve-abuse-user-report': 'application/json', - 'admin/send-email': 'application/json', - 'admin/server-info': 'application/json', - 'admin/show-moderation-logs': 'application/json', - 'admin/show-user': 'application/json', - 'admin/show-users': 'application/json', - 'admin/suspend-user': 'application/json', - 'admin/unsuspend-user': 'application/json', - 'admin/update-meta': 'application/json', - 'admin/delete-account': 'application/json', - 'admin/update-user-note': 'application/json', - 'admin/roles/create': 'application/json', - 'admin/roles/delete': 'application/json', - 'admin/roles/list': 'application/json', - 'admin/roles/show': 'application/json', - 'admin/roles/update': 'application/json', - 'admin/roles/assign': 'application/json', - 'admin/roles/unassign': 'application/json', - 'admin/roles/update-default-policies': 'application/json', - 'admin/roles/users': 'application/json', - 'admin/system-webhook/create': 'application/json', - 'admin/system-webhook/delete': 'application/json', - 'admin/system-webhook/list': 'application/json', - 'admin/system-webhook/show': 'application/json', - 'admin/system-webhook/update': 'application/json', - 'announcements': 'application/json', - 'announcements/show': 'application/json', - 'antennas/create': 'application/json', - 'antennas/delete': 'application/json', - 'antennas/list': 'application/json', - 'antennas/notes': 'application/json', - 'antennas/show': 'application/json', - 'antennas/update': 'application/json', - 'ap/get': 'application/json', - 'ap/show': 'application/json', - 'app/create': 'application/json', - 'app/show': 'application/json', - 'auth/accept': 'application/json', - 'auth/session/generate': 'application/json', - 'auth/session/show': 'application/json', - 'auth/session/userkey': 'application/json', - 'blocking/create': 'application/json', - 'blocking/delete': 'application/json', - 'blocking/list': 'application/json', - 'channels/create': 'application/json', - 'channels/featured': 'application/json', - 'channels/follow': 'application/json', - 'channels/followed': 'application/json', - 'channels/owned': 'application/json', - 'channels/show': 'application/json', - 'channels/timeline': 'application/json', - 'channels/unfollow': 'application/json', - 'channels/update': 'application/json', - 'channels/favorite': 'application/json', - 'channels/unfavorite': 'application/json', - 'channels/my-favorites': 'application/json', - 'channels/search': 'application/json', - 'charts/active-users': 'application/json', - 'charts/ap-request': 'application/json', - 'charts/drive': 'application/json', - 'charts/federation': 'application/json', - 'charts/instance': 'application/json', - 'charts/notes': 'application/json', - 'charts/user/drive': 'application/json', - 'charts/user/following': 'application/json', - 'charts/user/notes': 'application/json', - 'charts/user/pv': 'application/json', - 'charts/user/reactions': 'application/json', - 'charts/users': 'application/json', - 'clips/add-note': 'application/json', - 'clips/remove-note': 'application/json', - 'clips/create': 'application/json', - 'clips/delete': 'application/json', - 'clips/list': 'application/json', - 'clips/notes': 'application/json', - 'clips/show': 'application/json', - 'clips/update': 'application/json', - 'clips/favorite': 'application/json', - 'clips/unfavorite': 'application/json', - 'clips/my-favorites': 'application/json', - 'drive': 'application/json', - 'drive/files': 'application/json', - 'drive/files/attached-notes': 'application/json', - 'drive/files/check-existence': 'application/json', +/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */ +export const endpointReqTypes = { 'drive/files/create': 'multipart/form-data', - 'drive/files/delete': 'application/json', - 'drive/files/find-by-hash': 'application/json', - 'drive/files/find': 'application/json', - 'drive/files/show': 'application/json', - 'drive/files/update': 'application/json', - 'drive/files/upload-from-url': 'application/json', - 'drive/folders': 'application/json', - 'drive/folders/create': 'application/json', - 'drive/folders/delete': 'application/json', - 'drive/folders/find': 'application/json', - 'drive/folders/show': 'application/json', - 'drive/folders/update': 'application/json', - 'drive/stream': 'application/json', - 'email-address/available': 'application/json', - 'endpoint': 'application/json', - 'endpoints': 'application/json', - 'export-custom-emojis': 'application/json', - 'federation/followers': 'application/json', - 'federation/following': 'application/json', - 'federation/instances': 'application/json', - 'federation/show-instance': 'application/json', - 'federation/update-remote-user': 'application/json', - 'federation/users': 'application/json', - 'federation/stats': 'application/json', - 'following/create': 'application/json', - 'following/delete': 'application/json', - 'following/update': 'application/json', - 'following/update-all': 'application/json', - 'following/invalidate': 'application/json', - 'following/requests/accept': 'application/json', - 'following/requests/cancel': 'application/json', - 'following/requests/list': 'application/json', - 'following/requests/reject': 'application/json', - 'gallery/featured': 'application/json', - 'gallery/popular': 'application/json', - 'gallery/posts': 'application/json', - 'gallery/posts/create': 'application/json', - 'gallery/posts/delete': 'application/json', - 'gallery/posts/like': 'application/json', - 'gallery/posts/show': 'application/json', - 'gallery/posts/unlike': 'application/json', - 'gallery/posts/update': 'application/json', - 'get-online-users-count': 'application/json', - 'get-avatar-decorations': 'application/json', - 'hashtags/list': 'application/json', - 'hashtags/search': 'application/json', - 'hashtags/show': 'application/json', - 'hashtags/trend': 'application/json', - 'hashtags/users': 'application/json', - 'i': 'application/json', - 'i/2fa/done': 'application/json', - 'i/2fa/key-done': 'application/json', - 'i/2fa/password-less': 'application/json', - 'i/2fa/register-key': 'application/json', - 'i/2fa/register': 'application/json', - 'i/2fa/update-key': 'application/json', - 'i/2fa/remove-key': 'application/json', - 'i/2fa/unregister': 'application/json', - 'i/apps': 'application/json', - 'i/authorized-apps': 'application/json', - 'i/claim-achievement': 'application/json', - 'i/change-password': 'application/json', - 'i/delete-account': 'application/json', - 'i/export-blocking': 'application/json', - 'i/export-following': 'application/json', - 'i/export-mute': 'application/json', - 'i/export-notes': 'application/json', - 'i/export-clips': 'application/json', - 'i/export-favorites': 'application/json', - 'i/export-user-lists': 'application/json', - 'i/export-antennas': 'application/json', - 'i/favorites': 'application/json', - 'i/gallery/likes': 'application/json', - 'i/gallery/posts': 'application/json', - 'i/import-blocking': 'application/json', - 'i/import-following': 'application/json', - 'i/import-muting': 'application/json', - 'i/import-user-lists': 'application/json', - 'i/import-antennas': 'application/json', - 'i/notifications': 'application/json', - 'i/notifications-grouped': 'application/json', - 'i/page-likes': 'application/json', - 'i/pages': 'application/json', - 'i/pin': 'application/json', - 'i/read-all-unread-notes': 'application/json', - 'i/read-announcement': 'application/json', - 'i/regenerate-token': 'application/json', - 'i/registry/get-all': 'application/json', - 'i/registry/get-detail': 'application/json', - 'i/registry/get': 'application/json', - 'i/registry/keys-with-type': 'application/json', - 'i/registry/keys': 'application/json', - 'i/registry/remove': 'application/json', - 'i/registry/scopes-with-domain': 'application/json', - 'i/registry/set': 'application/json', - 'i/revoke-token': 'application/json', - 'i/signin-history': 'application/json', - 'i/unpin': 'application/json', - 'i/update-email': 'application/json', - 'i/update': 'application/json', - 'i/move': 'application/json', - 'i/webhooks/create': 'application/json', - 'i/webhooks/list': 'application/json', - 'i/webhooks/show': 'application/json', - 'i/webhooks/update': 'application/json', - 'i/webhooks/delete': 'application/json', - 'invite/create': 'application/json', - 'invite/delete': 'application/json', - 'invite/list': 'application/json', - 'invite/limit': 'application/json', - 'meta': 'application/json', - 'emojis': 'application/json', - 'emoji': 'application/json', - 'miauth/gen-token': 'application/json', - 'mute/create': 'application/json', - 'mute/delete': 'application/json', - 'mute/list': 'application/json', - 'renote-mute/create': 'application/json', - 'renote-mute/delete': 'application/json', - 'renote-mute/list': 'application/json', - 'my/apps': 'application/json', - 'notes': 'application/json', - 'notes/children': 'application/json', - 'notes/clips': 'application/json', - 'notes/conversation': 'application/json', - 'notes/create': 'application/json', - 'notes/delete': 'application/json', - 'notes/favorites/create': 'application/json', - 'notes/favorites/delete': 'application/json', - 'notes/featured': 'application/json', - 'notes/global-timeline': 'application/json', - 'notes/hybrid-timeline': 'application/json', - 'notes/local-timeline': 'application/json', - 'notes/mentions': 'application/json', - 'notes/polls/recommendation': 'application/json', - 'notes/polls/vote': 'application/json', - 'notes/reactions': 'application/json', - 'notes/reactions/create': 'application/json', - 'notes/reactions/delete': 'application/json', - 'notes/renotes': 'application/json', - 'notes/replies': 'application/json', - 'notes/search-by-tag': 'application/json', - 'notes/search': 'application/json', - 'notes/show': 'application/json', - 'notes/state': 'application/json', - 'notes/thread-muting/create': 'application/json', - 'notes/thread-muting/delete': 'application/json', - 'notes/timeline': 'application/json', - 'notes/translate': 'application/json', - 'notes/unrenote': 'application/json', - 'notes/user-list-timeline': 'application/json', - 'notifications/create': 'application/json', - 'notifications/flush': 'application/json', - 'notifications/mark-all-as-read': 'application/json', - 'notifications/test-notification': 'application/json', - 'page-push': 'application/json', - 'pages/create': 'application/json', - 'pages/delete': 'application/json', - 'pages/featured': 'application/json', - 'pages/like': 'application/json', - 'pages/show': 'application/json', - 'pages/unlike': 'application/json', - 'pages/update': 'application/json', - 'flash/create': 'application/json', - 'flash/delete': 'application/json', - 'flash/featured': 'application/json', - 'flash/like': 'application/json', - 'flash/show': 'application/json', - 'flash/unlike': 'application/json', - 'flash/update': 'application/json', - 'flash/my': 'application/json', - 'flash/my-likes': 'application/json', - 'ping': 'application/json', - 'pinned-users': 'application/json', - 'promo/read': 'application/json', - 'roles/list': 'application/json', - 'roles/show': 'application/json', - 'roles/users': 'application/json', - 'roles/notes': 'application/json', - 'request-reset-password': 'application/json', - 'reset-db': 'application/json', - 'reset-password': 'application/json', - 'server-info': 'application/json', - 'stats': 'application/json', - 'sw/show-registration': 'application/json', - 'sw/update-registration': 'application/json', - 'sw/register': 'application/json', - 'sw/unregister': 'application/json', - 'test': 'application/json', - 'username/available': 'application/json', - 'users': 'application/json', - 'users/clips': 'application/json', - 'users/followers': 'application/json', - 'users/following': 'application/json', - 'users/gallery/posts': 'application/json', - 'users/get-frequently-replied-users': 'application/json', - 'users/featured-notes': 'application/json', - 'users/lists/create': 'application/json', - 'users/lists/delete': 'application/json', - 'users/lists/list': 'application/json', - 'users/lists/pull': 'application/json', - 'users/lists/push': 'application/json', - 'users/lists/show': 'application/json', - 'users/lists/favorite': 'application/json', - 'users/lists/unfavorite': 'application/json', - 'users/lists/update': 'application/json', - 'users/lists/create-from-public': 'application/json', - 'users/lists/update-membership': 'application/json', - 'users/lists/get-memberships': 'application/json', - 'users/notes': 'application/json', - 'users/pages': 'application/json', - 'users/flashs': 'application/json', - 'users/reactions': 'application/json', - 'users/recommendation': 'application/json', - 'users/relation': 'application/json', - 'users/report-abuse': 'application/json', - 'users/search-by-username-and-host': 'application/json', - 'users/search': 'application/json', - 'users/show': 'application/json', - 'users/achievements': 'application/json', - 'users/update-memo': 'application/json', - 'fetch-rss': 'application/json', - 'fetch-external-resources': 'application/json', - 'retention': 'application/json', - 'bubble-game/register': 'application/json', - 'bubble-game/ranking': 'application/json', - 'reversi/cancel-match': 'application/json', - 'reversi/games': 'application/json', - 'reversi/match': 'application/json', - 'reversi/invitations': 'application/json', - 'reversi/show-game': 'application/json', - 'reversi/surrender': 'application/json', - 'reversi/verify': 'application/json', -}; +} as const satisfies { [K in keyof Endpoints]?: 'multipart/form-data'; }; From 567acea2a3a040dbde69748deb2112e3ff2b92b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:23:40 +0900 Subject: [PATCH 026/159] =?UTF-8?q?fix(frontend):=20instance=20info?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A7=E4=B8=8D=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E3=81=AAapi=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=8C=E9=A3=9B=E3=81=B6=E3=81=AE=E3=82=92=E6=8A=91=E6=AD=A2?= =?UTF-8?q?=20(#14515)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): instance infoページで不必要なapiリクエストが飛ぶのを抑止 * fix --- packages/frontend/src/components/MkChart.vue | 50 ++++++++++--------- packages/frontend/src/pages/instance-info.vue | 25 +++++++--- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 4b2456224..57d325b11 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only

- + + diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue new file mode 100644 index 000000000..07315e6a8 --- /dev/null +++ b/packages/frontend-embed/src/components/EmAcct.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue new file mode 100644 index 000000000..58c35c8ef --- /dev/null +++ b/packages/frontend-embed/src/components/EmAvatar.vue @@ -0,0 +1,250 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue new file mode 100644 index 000000000..e4149cf36 --- /dev/null +++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue @@ -0,0 +1,101 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue new file mode 100644 index 000000000..224979707 --- /dev/null +++ b/packages/frontend-embed/src/components/EmEmoji.vue @@ -0,0 +1,26 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue new file mode 100644 index 000000000..d376b29a7 --- /dev/null +++ b/packages/frontend-embed/src/components/EmError.vue @@ -0,0 +1,43 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue new file mode 100644 index 000000000..d19cd08d0 --- /dev/null +++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue @@ -0,0 +1,240 @@ + + + + + + + + + diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue new file mode 100644 index 000000000..eeeaee528 --- /dev/null +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue new file mode 100644 index 000000000..319ad7239 --- /dev/null +++ b/packages/frontend-embed/src/components/EmLink.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue new file mode 100644 index 000000000..49d8ace37 --- /dev/null +++ b/packages/frontend-embed/src/components/EmLoading.vue @@ -0,0 +1,112 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue new file mode 100644 index 000000000..435da238a --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaBanner.vue @@ -0,0 +1,55 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue new file mode 100644 index 000000000..fe1aa5a87 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -0,0 +1,154 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue new file mode 100644 index 000000000..0b2d835ab --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaList.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue new file mode 100644 index 000000000..ce751f9ac --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaVideo.vue @@ -0,0 +1,64 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue new file mode 100644 index 000000000..5eadf828c --- /dev/null +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -0,0 +1,46 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts new file mode 100644 index 000000000..7543d3cd5 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -0,0 +1,461 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { VNode, h, SetupContext, provide } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import EmUrl from '@/components/EmUrl.vue'; +import EmTime from '@/components/EmTime.vue'; +import EmLink from '@/components/EmLink.vue'; +import EmMention from '@/components/EmMention.vue'; +import EmEmoji from '@/components/EmEmoji.vue'; +import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; +import EmA from '@/components/EmA.vue'; +import { host } from '@/config.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} + +const QUOTE_STYLE = ` +display: block; +margin: 8px; +padding: 6px 0 6px 12px; +color: var(--fg); +border-left: solid 3px var(--fg); +opacity: 0.7; +`.split('\n').join(' '); + +type MfmProps = { + text: string; + plain?: boolean; + nowrap?: boolean; + author?: Misskey.entities.UserLite; + isNote?: boolean; + emojiUrls?: Record; + rootScale?: number; + nyaize?: boolean | 'respect'; + parsedNodes?: mfm.MfmNode[] | null; + enableEmojiMenu?: boolean; + enableEmojiMenuReaction?: boolean; + linkNavigationBehavior?: string; +}; + +type MfmEvents = { + clickEv(id: string): void; +}; + +// eslint-disable-next-line import/no-default-export +export default function (props: MfmProps, { emit }: { emit: SetupContext['emit'] }) { + provide('linkNavigationBehavior', props.linkNavigationBehavior); + + const isNote = props.isNote ?? true; + const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.text == null || props.text === '') return; + + const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text); + + const validTime = (t: string | boolean | null | undefined) => { + if (t == null) return null; + if (typeof t === 'boolean') return null; + return t.match(/^\-?[0-9.]+s$/) ? t : null; + }; + + const validColor = (c: unknown): string | null => { + if (typeof c !== 'string') return null; + return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; + }; + + const useAnim = true; + + /** + * Gen Vue Elements from MFM AST + * @param ast MFM AST + * @param scale How times large the text is + * @param disableNyaize Whether nyaize is disabled or not + */ + const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => { + switch (token.type) { + case 'text': { + let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + + if (!props.plain) { + const res: (VNode | string)[] = []; + for (const t of text.split('\n')) { + res.push(h('br')); + res.push(t); + } + res.shift(); + return res; + } else { + return [text.replace(/\n/g, ' ')]; + } + } + + case 'bold': { + return [h('b', genEl(token.children, scale))]; + } + + case 'strike': { + return [h('del', genEl(token.children, scale))]; + } + + case 'italic': { + return h('i', { + style: 'font-style: oblique;', + }, genEl(token.children, scale)); + } + + case 'fn': { + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + let style: string | undefined; + switch (token.props.name) { + case 'tada': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'jelly': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'twitch': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'shake': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'spin': { + const direction = + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : + 'normal'; + const anime = + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : + 'mfm-spin'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : ''; + break; + } + case 'jump': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : ''; + break; + } + case 'bounce': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : ''; + break; + } + case 'flip': { + const transform = + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : + 'scaleX(-1)'; + style = `transform: ${transform};`; + break; + } + case 'x2': { + return h('span', { + class: 'mfm-x2', + }, genEl(token.children, scale * 2)); + } + case 'x3': { + return h('span', { + class: 'mfm-x3', + }, genEl(token.children, scale * 3)); + } + case 'x4': { + return h('span', { + class: 'mfm-x4', + }, genEl(token.children, scale * 4)); + } + case 'font': { + const family = + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : + null; + if (family) style = `font-family: ${family};`; + break; + } + case 'blur': { + return h('span', { + class: '_mfm_blur_', + }, genEl(token.children, scale)); + } + case 'rainbow': { + if (!useAnim) { + return h('span', { + class: '_mfm_rainbow_fallback_', + }, genEl(token.children, scale)); + } + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; + break; + } + case 'sparkle': { + return genEl(token.children, scale); + } + case 'rotate': { + const degrees = safeParseFloat(token.props.args.deg) ?? 90; + style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + break; + } + case 'position': { + const x = safeParseFloat(token.props.args.x) ?? 0; + const y = safeParseFloat(token.props.args.y) ?? 0; + style = `transform: translateX(${x}em) translateY(${y}em);`; + break; + } + case 'scale': { + const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5); + const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5); + style = `transform: scale(${x}, ${y});`; + scale = scale * Math.max(x, y); + break; + } + case 'fg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'bg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `background-color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'border': { + let color = validColor(token.props.args.color); + color = color ? `#${color}` : 'var(--accent)'; + let b_style = token.props.args.style; + if ( + typeof b_style !== 'string' || + !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] + .includes(b_style) + ) b_style = 'solid'; + const width = safeParseFloat(token.props.args.width) ?? 1; + const radius = safeParseFloat(token.props.args.radius) ?? 0; + style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`; + break; + } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + let text = child.type === 'text' ? child.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + let text = rt.type === 'text' ? rt.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(EmTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } + case 'clickable': { + return h('span', { onClick(ev: MouseEvent): void { + ev.stopPropagation(); + ev.preventDefault(); + const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : ''; + emit('clickEv', clickEv); + } }, genEl(token.children, scale)); + } + } + if (style === undefined) { + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); + } else { + return h('span', { + style: 'display: inline-block; ' + style, + }, genEl(token.children, scale)); + } + } + + case 'small': { + return [h('small', { + style: 'opacity: 0.7;', + }, genEl(token.children, scale))]; + } + + case 'center': { + return [h('div', { + style: 'text-align:center;', + }, genEl(token.children, scale))]; + } + + case 'url': { + return [h(EmUrl, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + })]; + } + + case 'link': { + return [h(EmLink, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }, genEl(token.children, scale, true))]; + } + + case 'mention': { + return [h(EmMention, { + key: Math.random(), + host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, + username: token.props.username, + })]; + } + + case 'hashtag': { + return [h(EmA, { + key: Math.random(), + to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, + style: 'color:var(--hashtag);', + }, `#${token.props.hashtag}`)]; + } + + case 'blockCode': { + return [h('code', { + key: Math.random(), + lang: token.props.lang ?? undefined, + }, token.props.code)]; + } + + case 'inlineCode': { + return [h('code', { + key: Math.random(), + }, token.props.code)]; + } + + case 'quote': { + if (!props.nowrap) { + return [h('div', { + style: QUOTE_STYLE, + }, genEl(token.children, scale, true))]; + } else { + return [h('span', { + style: QUOTE_STYLE, + }, genEl(token.children, scale, true))]; + } + } + + case 'emojiCode': { + if (props.author?.host == null) { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + normal: props.plain, + host: null, + useOriginalSize: scale >= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + fallbackToImage: false, + })]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) { + return [h('span', `:${token.props.name}:`)]; + } else { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + url: props.emojiUrls && props.emojiUrls[token.props.name], + normal: props.plain, + host: props.author.host, + useOriginalSize: scale >= 2.5, + })]; + } + } + } + + case 'unicodeEmoji': { + return [h(EmEmoji, { + key: Math.random(), + emoji: token.props.emoji, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + })]; + } + + case 'mathInline': { + return [h('code', token.props.formula)]; + } + + case 'mathBlock': { + return [h('code', token.props.formula)]; + } + + case 'search': { + return [h('div', { + key: Math.random(), + }, token.props.query)]; + } + + case 'plain': { + return [h('span', genEl(token.children, scale, true))]; + } + + default: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.error('unrecognized ast type:', (token as any).type); + + return []; + } + } + }).flat(Infinity) as (VNode | string)[]; + + return h('span', { + // https://codeday.me/jp/qa/20190424/690106.html + style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', + }, genEl(rootAst, props.rootScale ?? 1)); +} diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue new file mode 100644 index 000000000..7c4d59106 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -0,0 +1,609 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue new file mode 100644 index 000000000..74a26856c --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -0,0 +1,486 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue new file mode 100644 index 000000000..e4add9501 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteHeader.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue new file mode 100644 index 000000000..828b6cd2e --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSimple.vue @@ -0,0 +1,105 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue new file mode 100644 index 000000000..c98b95680 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSub.vue @@ -0,0 +1,149 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue new file mode 100644 index 000000000..3970d0509 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -0,0 +1,48 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue new file mode 100644 index 000000000..5d5317a91 --- /dev/null +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -0,0 +1,504 @@ + + + + + + + + diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue new file mode 100644 index 000000000..a2b120344 --- /dev/null +++ b/packages/frontend-embed/src/components/EmPoll.vue @@ -0,0 +1,82 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue new file mode 100644 index 000000000..5c38ecb0e --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionIcon.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue new file mode 100644 index 000000000..2e43eb8d1 --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue new file mode 100644 index 000000000..014dd1c93 --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue new file mode 100644 index 000000000..382e39e49 --- /dev/null +++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue @@ -0,0 +1,113 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue new file mode 100644 index 000000000..a8627e02c --- /dev/null +++ b/packages/frontend-embed/src/components/EmTime.vue @@ -0,0 +1,107 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue new file mode 100644 index 000000000..6c30b1102 --- /dev/null +++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue @@ -0,0 +1,39 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue new file mode 100644 index 000000000..a96bfdb49 --- /dev/null +++ b/packages/frontend-embed/src/components/EmUrl.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue new file mode 100644 index 000000000..c0c7c443c --- /dev/null +++ b/packages/frontend-embed/src/components/EmUserName.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue new file mode 100644 index 000000000..b621110ec --- /dev/null +++ b/packages/frontend-embed/src/components/I18n.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/frontend-embed/src/config.ts b/packages/frontend-embed/src/config.ts new file mode 100644 index 000000000..f9850ba46 --- /dev/null +++ b/packages/frontend-embed/src/config.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href); +const siteName = document.querySelector('meta[property="og:site_name"]')?.content; + +export const host = address.host; +export const hostname = address.hostname; +export const url = address.origin; +export const apiUrl = location.origin + '/api'; +export const lang = localStorage.getItem('lang') ?? 'en-US'; +export const langs = _LANGS_; +const preParseLocale = localStorage.getItem('locale'); +export const locale = preParseLocale ? JSON.parse(preParseLocale) : null; +export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName; +export const debug = localStorage.getItem('debug') === 'true'; diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts new file mode 100644 index 000000000..d5b40885c --- /dev/null +++ b/packages/frontend-embed/src/custom-emojis.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { misskeyApi, misskeyApiGet } from '@/misskey-api.js'; + +function get(key: string) { + const value = localStorage.getItem(key); + if (value === null) return null; + return JSON.parse(value); +} + +function set(key: string, value: any) { + localStorage.setItem(key, JSON.stringify(value)); +} + +const storageCache = await get('emojis'); +export const customEmojis = shallowRef(Array.isArray(storageCache) ? storageCache : []); + +export const customEmojisMap = new Map(); +watch(customEmojis, emojis => { + customEmojisMap.clear(); + for (const emoji of emojis) { + customEmojisMap.set(emoji.name, emoji); + } +}, { immediate: true }); + +export async function fetchCustomEmojis(force = false) { + const now = Date.now(); + + let res; + if (force) { + res = await misskeyApi('emojis', {}); + } else { + const lastFetchedAt = await get('lastEmojisFetchedAt'); + if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return; + res = await misskeyApiGet('emojis', {}); + } + + customEmojis.value = res.emojis; + set('emojis', res.emojis); + set('lastEmojisFetchedAt', now); +} + +let cachedTags; +export function getCustomEmojiTags() { + if (cachedTags) return cachedTags; + + const tags = new Set(); + for (const emoji of customEmojis.value) { + for (const tag of emoji.aliases) { + tags.add(tag); + } + } + const res = Array.from(tags); + cachedTags = res; + return res; +} diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts new file mode 100644 index 000000000..799bbed59 --- /dev/null +++ b/packages/frontend-embed/src/di.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { InjectionKey } from 'vue'; +import * as Misskey from 'misskey-js'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import type { ParsedEmbedParams } from '@@/js/embed-page.js'; + +export const DI = { + serverMetadata: Symbol() as InjectionKey, + embedParams: Symbol() as InjectionKey, + mediaProxy: Symbol() as InjectionKey, +}; diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts new file mode 100644 index 000000000..17e787f9f --- /dev/null +++ b/packages/frontend-embed/src/i18n.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; +import type { Locale } from '../../../locales/index.js'; +import { locale } from '@/config.js'; + +export const i18n = markRaw(new I18n(locale, _DEV_)); + +export function updateI18n(newLocale: Locale) { + i18n.locale = newLocale; +} diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html new file mode 100644 index 000000000..47b0b0e84 --- /dev/null +++ b/packages/frontend-embed/src/index.html @@ -0,0 +1,36 @@ + + + + + + + + + [DEV] Loading... + + + + + + + +
+ + + diff --git a/packages/frontend-embed/src/misskey-api.ts b/packages/frontend-embed/src/misskey-api.ts new file mode 100644 index 000000000..13630590b --- /dev/null +++ b/packages/frontend-embed/src/misskey-api.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { ref } from 'vue'; +import { apiUrl } from '@/config.js'; + +export const pendingApiRequestsCount = ref(0); + +// Implements Misskey.api.ApiClient.request +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( + endpoint: E, + data: P = {} as any, + signal?: AbortSignal, +): Promise<_ResT> { + if (endpoint.includes('://')) throw new Error('invalid endpoint'); + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + signal, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} + +// Implements Misskey.api.ApiClient.request +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( + endpoint: E, + data: P = {} as any, +): Promise<_ResT> { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data as any); + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue new file mode 100644 index 000000000..6564eecd7 --- /dev/null +++ b/packages/frontend-embed/src/pages/clip.vue @@ -0,0 +1,140 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue new file mode 100644 index 000000000..bbb03b4e6 --- /dev/null +++ b/packages/frontend-embed/src/pages/not-found.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue new file mode 100644 index 000000000..86aebe072 --- /dev/null +++ b/packages/frontend-embed/src/pages/note.vue @@ -0,0 +1,48 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue new file mode 100644 index 000000000..d69555287 --- /dev/null +++ b/packages/frontend-embed/src/pages/tag.vue @@ -0,0 +1,125 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue new file mode 100644 index 000000000..d590f6e65 --- /dev/null +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -0,0 +1,138 @@ + + + + + + + diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts new file mode 100644 index 000000000..fd8eb8a5d --- /dev/null +++ b/packages/frontend-embed/src/post-message.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const postMessageEventTypes = [ + 'misskey:embed:ready', + 'misskey:embed:changeHeight', +] as const; + +export type PostMessageEventType = typeof postMessageEventTypes[number]; + +export interface PostMessageEventPayload extends Record { + 'misskey:embed:ready': undefined; + 'misskey:embed:changeHeight': { + height: number; + }; +} + +export type MiPostMessageEvent = { + type: T; + iframeId?: string; + payload?: PostMessageEventPayload[T]; +} + +let defaultIframeId: string | null = null; + +export function setIframeId(id: string): void { + if (defaultIframeId != null) return; + + if (_DEV_) console.log('setIframeId', id); + defaultIframeId = id; +} + +/** + * 親フレームにイベントを送信 + */ +export function postMessageToParentWindow(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void { + let _iframeId = iframeId; + if (_iframeId == null) { + _iframeId = defaultIframeId; + } + if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload); + window.parent.postMessage({ + type, + iframeId: _iframeId, + payload, + }, '*'); +} diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts new file mode 100644 index 000000000..2bd57a099 --- /dev/null +++ b/packages/frontend-embed/src/server-metadata.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { misskeyApi } from '@/misskey-api.js'; + +const providedMetaEl = document.getElementById('misskey_meta'); + +const _serverMetadata = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; + +// NOTE: devモードのときしか _serverMetadata が null になることは無い +export const serverMetadata = _serverMetadata ?? await misskeyApi('meta', { + detail: true, +}); diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss new file mode 100644 index 000000000..02008ddbd --- /dev/null +++ b/packages/frontend-embed/src/style.scss @@ -0,0 +1,453 @@ +@charset "utf-8"; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +:root { + --radius: 12px; + --marginFull: 14px; + --marginHalf: 10px; + + --margin: var(--marginFull); +} + +html { + background-color: transparent; + color-scheme: light dark; + color: var(--fg); + accent-color: var(--accent); + overflow: clip; + overflow-wrap: break-word; + font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; + font-size: 14px; + line-height: 1.35; + text-size-adjust: 100%; + tab-size: 2; + -webkit-text-size-adjust: 100%; + + &, * { + scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: inherit; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbarHandle); + + &:hover { + background: var(--scrollbarHandleHover); + } + + &:active { + background: var(--accent); + } + } + } +} + +html, body { + height: 100%; + touch-action: manipulation; + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +#misskey_app { + height: 100%; +} + +a { + text-decoration: none; + cursor: pointer; + color: inherit; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + + &:focus-visible { + outline-offset: 2px; + } + + &:hover { + text-decoration: underline; + } + + &[target="_blank"] { + -webkit-touch-callout: default; + } +} + +rt { + white-space: initial; +} + +:focus-visible { + outline: var(--focus) solid 2px; + outline-offset: -2px; + + &:hover { + text-decoration: none; + } +} + +.ti { + width: 1.28em; + vertical-align: -12%; + line-height: 1em; + + &::before { + font-size: 128%; + } +} + +.ti-fw { + display: inline-block; + text-align: center; +} + +._nowrap { + white-space: pre !important; + word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html + overflow: hidden; + text-overflow: ellipsis; +} + +._button { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + appearance: none; + display: inline-block; + padding: 0; + margin: 0; // for Safari + background: none; + border: none; + cursor: pointer; + color: inherit; + touch-action: manipulation; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + font-size: 1em; + font-family: inherit; + line-height: inherit; + max-width: 100%; + + &:disabled { + opacity: 0.5; + cursor: default; + } +} + +._buttonGray { + @extend ._button; + background: var(--buttonBg); + + &:not(:disabled):hover { + background: var(--buttonHoverBg); + } +} + +._buttonPrimary { + @extend ._button; + color: var(--fgOnAccent); + background: var(--accent); + + &:not(:disabled):hover { + background: hsl(from var(--accent) h s calc(l + 5)); + } + + &:not(:disabled):active { + background: hsl(from var(--accent) h s calc(l - 5)); + } +} + +._buttonGradate { + @extend ._buttonPrimary; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + + &:not(:disabled):hover { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } + + &:not(:disabled):active { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } +} + +._buttonRounded { + font-size: 0.95em; + padding: 0.5em 1em; + min-width: 100px; + border-radius: 99rem; + + &._buttonPrimary, + &._buttonGradate { + font-weight: 700; + } +} + +._help { + color: var(--accent); + cursor: help; +} + +._textButton { + @extend ._button; + color: var(--accent); + + &:focus-visible { + outline-offset: 2px; + } + + &:not(:disabled):hover { + text-decoration: underline; + } +} + +._panel { + background: var(--panel); + border-radius: var(--radius); + overflow: clip; +} + +._margin { + margin: var(--margin) 0; +} + +._gaps_m { + display: flex; + flex-direction: column; + gap: 1.5em; +} + +._gaps_s { + display: flex; + flex-direction: column; + gap: 0.75em; +} + +._gaps { + display: flex; + flex-direction: column; + gap: var(--margin); +} + +._buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +._buttonsCenter { + @extend ._buttons; + + justify-content: center; +} + +._borderButton { + @extend ._button; + display: block; + width: 100%; + padding: 10px; + box-sizing: border-box; + text-align: center; + border: solid 0.5px var(--divider); + border-radius: var(--radius); + + &:active { + border-color: var(--accent); + } +} + +._popup { + background: var(--popup); + border-radius: var(--radius); + contain: content; +} + +._acrylic { + background: var(--acrylicPanel); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +._fullinfo { + padding: 64px 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._link { + color: var(--link); +} + +._caption { + font-size: 0.8em; + opacity: 0.7; +} + +._monospace { + font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; +} + +// MFM ----------------------------- + +._mfm_blur_ { + filter: blur(6px); + transition: filter 0.3s; + + &:hover { + filter: blur(0px); + } +} + +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + +._mfm_rainbow_fallback_ { + background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +@keyframes mfm-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes mfm-spinX { + 0% { transform: perspective(128px) rotateX(0deg); } + 100% { transform: perspective(128px) rotateX(360deg); } +} + +@keyframes mfm-spinY { + 0% { transform: perspective(128px) rotateY(0deg); } + 100% { transform: perspective(128px) rotateY(360deg); } +} + +@keyframes mfm-jump { + 0% { transform: translateY(0); } + 25% { transform: translateY(-16px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} + +@keyframes mfm-bounce { + 0% { transform: translateY(0) scale(1, 1); } + 25% { transform: translateY(-16px) scale(1, 1); } + 50% { transform: translateY(0) scale(1, 1); } + 75% { transform: translateY(0) scale(1.5, 0.75); } + 100% { transform: translateY(0) scale(1, 1); } +} + +// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-twitch { + 0% { transform: translate(7px, -2px) } + 5% { transform: translate(-3px, 1px) } + 10% { transform: translate(-7px, -1px) } + 15% { transform: translate(0px, -1px) } + 20% { transform: translate(-8px, 6px) } + 25% { transform: translate(-4px, -3px) } + 30% { transform: translate(-4px, -6px) } + 35% { transform: translate(-8px, -8px) } + 40% { transform: translate(4px, 6px) } + 45% { transform: translate(-3px, 1px) } + 50% { transform: translate(2px, -10px) } + 55% { transform: translate(-7px, 0px) } + 60% { transform: translate(-2px, 4px) } + 65% { transform: translate(3px, -8px) } + 70% { transform: translate(6px, 7px) } + 75% { transform: translate(-7px, -2px) } + 80% { transform: translate(-7px, -8px) } + 85% { transform: translate(9px, 3px) } + 90% { transform: translate(-3px, -2px) } + 95% { transform: translate(-10px, 2px) } + 100% { transform: translate(-2px, -6px) } +} + +// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-shake { + 0% { transform: translate(-3px, -1px) rotate(-8deg) } + 5% { transform: translate(0px, -1px) rotate(-10deg) } + 10% { transform: translate(1px, -3px) rotate(0deg) } + 15% { transform: translate(1px, 1px) rotate(11deg) } + 20% { transform: translate(-2px, 1px) rotate(1deg) } + 25% { transform: translate(-1px, -2px) rotate(-2deg) } + 30% { transform: translate(-1px, 2px) rotate(-3deg) } + 35% { transform: translate(2px, 1px) rotate(6deg) } + 40% { transform: translate(-2px, -3px) rotate(-9deg) } + 45% { transform: translate(0px, -1px) rotate(-12deg) } + 50% { transform: translate(1px, 2px) rotate(10deg) } + 55% { transform: translate(0px, -3px) rotate(8deg) } + 60% { transform: translate(1px, -1px) rotate(8deg) } + 65% { transform: translate(0px, -1px) rotate(-7deg) } + 70% { transform: translate(-1px, -3px) rotate(6deg) } + 75% { transform: translate(0px, -2px) rotate(4deg) } + 80% { transform: translate(-2px, -1px) rotate(3deg) } + 85% { transform: translate(1px, -3px) rotate(-10deg) } + 90% { transform: translate(1px, 0px) rotate(3deg) } + 95% { transform: translate(-2px, 0px) rotate(-3deg) } + 100% { transform: translate(2px, 1px) rotate(2deg) } +} + +@keyframes mfm-rubberBand { + from { transform: scale3d(1, 1, 1); } + 30% { transform: scale3d(1.25, 0.75, 1); } + 40% { transform: scale3d(0.75, 1.25, 1); } + 50% { transform: scale3d(1.15, 0.85, 1); } + 65% { transform: scale3d(0.95, 1.05, 1); } + 75% { transform: scale3d(1.05, 0.95, 1); } + to { transform: scale3d(1, 1, 1); } +} + +@keyframes mfm-rainbow { + 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } + 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } +} diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts new file mode 100644 index 000000000..050d8cf63 --- /dev/null +++ b/packages/frontend-embed/src/theme.ts @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; +import type { BundledTheme } from 'shiki/themes'; + +export type Theme = { + id: string; + name: string; + author: string; + desc?: string; + base?: 'dark' | 'light'; + props: Record; + codeHighlighter?: { + base: BundledTheme; + overrides?: Record; + } | { + base: '_none_'; + overrides: Record; + }; +}; + +let timeout: number | null = null; + +export function applyTheme(theme: Theme, persist = true) { + if (timeout) window.clearTimeout(timeout); + + document.documentElement.classList.add('_themeChanging_'); + + timeout = window.setTimeout(() => { + document.documentElement.classList.remove('_themeChanging_'); + }, 1000); + + const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; + + // Deep copy + const _theme = JSON.parse(JSON.stringify(theme)); + + if (_theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); + if (base) _theme.props = Object.assign({}, base.props, _theme.props); + } + + const props = compile(_theme); + + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', props['htmlThemeColor']); + break; + } + } + + for (const [k, v] of Object.entries(props)) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + } + + document.documentElement.style.setProperty('color-scheme', colorScheme); +} + +function compile(theme: Theme): Record { + function getColor(val: string): tinycolor.Instance { + if (val[0] === '@') { // ref (prop) + return getColor(theme.props[val.substring(1)]); + } else if (val[0] === '$') { // ref (const) + return getColor(theme.props[val]); + } else if (val[0] === ':') { // func + const parts = val.split('<'); + const func = parts.shift().substring(1); + const arg = parseFloat(parts.shift()); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + case 'hue': return color.spin(arg); + case 'saturate': return color.saturate(arg); + } + } + + // other case + return tinycolor(val); + } + + const props = {}; + + for (const [k, v] of Object.entries(theme.props)) { + if (k.startsWith('$')) continue; // ignore const + + props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); + } + + return props; +} + +function genValue(c: tinycolor.Instance): string { + return c.toRgbString(); +} diff --git a/packages/frontend-embed/src/to-be-shared/collapsed.ts b/packages/frontend-embed/src/to-be-shared/collapsed.ts new file mode 100644 index 000000000..4ec88a3c6 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/collapsed.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; + +export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { + const collapsed = note.cw == null && ( + note.text != null && ( + (note.text.includes('$[x2')) || + (note.text.includes('$[x3')) || + (note.text.includes('$[x4')) || + (note.text.includes('$[scale')) || + (note.text.split('\n').length > 9) || + (note.text.length > 500) || + (urls.length >= 4) + ) || note.files.length >= 5 + ); + + return collapsed; +} diff --git a/packages/frontend-embed/src/to-be-shared/intl-const.ts b/packages/frontend-embed/src/to-be-shared/intl-const.ts new file mode 100644 index 000000000..aaa4f0a86 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/intl-const.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { lang } from '@/config.js'; + +export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); + +let _dateTimeFormat: Intl.DateTimeFormat; +try { + _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _dateTimeFormat = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} +export const dateTimeFormat = _dateTimeFormat; + +export const timeZone = dateTimeFormat.resolvedOptions().timeZone; + +export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; + +let _numberFormat: Intl.NumberFormat; +try { + _numberFormat = new Intl.NumberFormat(versatileLang); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _numberFormat = new Intl.NumberFormat('en-US'); +} +export const numberFormat = _numberFormat; diff --git a/packages/frontend-embed/src/to-be-shared/is-link.ts b/packages/frontend-embed/src/to-be-shared/is-link.ts new file mode 100644 index 000000000..946f86400 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts new file mode 100644 index 000000000..6b3fcd938 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +function defaultUseWorkerNumber(prev: number, totalWorkers: number) { + return prev + 1; +} + +export class WorkerMultiDispatch { + private symbol = Symbol('WorkerMultiDispatch'); + private workers: Worker[] = []; + private terminated = false; + private prevWorkerNumber = 0; + private getUseWorkerNumber = defaultUseWorkerNumber; + private finalizationRegistry: FinalizationRegistry; + + constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { + this.getUseWorkerNumber = getUseWorkerNumber; + for (let i = 0; i < concurrency; i++) { + this.workers.push(workerConstructor()); + } + + this.finalizationRegistry = new FinalizationRegistry(() => { + this.terminate(); + }); + this.finalizationRegistry.register(this, this.symbol); + + if (_DEV_) console.log('WorkerMultiDispatch: Created', this); + } + + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { + let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); + workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; + if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); + this.prevWorkerNumber = workerNumber; + + // 不毛だがunionをoverloadに突っ込めない + // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error + // https://github.com/microsoft/TypeScript/issues/14107 + if (Array.isArray(options)) { + this.workers[workerNumber].postMessage(message, options); + } else { + this.workers[workerNumber].postMessage(message, options); + } + return workerNumber; + } + + public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.addEventListener('message', callback, options); + }); + } + + public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.removeEventListener('message', callback, options); + }); + } + + public terminate() { + this.terminated = true; + if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); + this.workers.forEach(worker => { + worker.terminate(); + }); + this.workers = []; + this.finalizationRegistry.unregister(this); + } + + public isTerminated() { + return this.terminated; + } + + public getWorkers() { + return this.workers; + } + + public getSymbol() { + return this.symbol; + } +} diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue new file mode 100644 index 000000000..3b8449dac --- /dev/null +++ b/packages/frontend-embed/src/ui.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts new file mode 100644 index 000000000..9a2fd0bee --- /dev/null +++ b/packages/frontend-embed/src/utils.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { url } from '@/config.js'; + +export const acct = (user: Misskey.Acct) => { + return Misskey.acct.toString(user); +}; + +export const userName = (user: Misskey.entities.User) => { + return user.name || user.username; +}; + +export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => { + return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; +}; + +export const notePage = note => { + return `/notes/${note.id}`; +}; diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts new file mode 100644 index 000000000..22de6cd3a --- /dev/null +++ b/packages/frontend-embed/src/workers/draw-blurhash.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { render } from 'buraha'; + +const canvas = new OffscreenCanvas(64, 64); + +onmessage = (event) => { + // console.log(event.data); + if (!('id' in event.data && typeof event.data.id === 'string')) { + return; + } + if (!('hash' in event.data && typeof event.data.hash === 'string')) { + return; + } + + render(event.data.hash, canvas); + const bitmap = canvas.transferToImageBitmap(); + postMessage({ id: event.data.id, bitmap }); +}; diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts new file mode 100644 index 000000000..b203ebe66 --- /dev/null +++ b/packages/frontend-embed/src/workers/test-webgl2.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1); +// 環境によってはOffscreenCanvasが存在しないため +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const gl = canvas?.getContext('webgl2'); +if (gl) { + postMessage({ result: true }); +} else { + postMessage({ result: false }); +} diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json new file mode 100644 index 000000000..8ee893046 --- /dev/null +++ b/packages/frontend-embed/src/workers/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["esnext", "webworker"], + } +} diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json new file mode 100644 index 000000000..370134362 --- /dev/null +++ b/packages/frontend-embed/tsconfig.json @@ -0,0 +1,53 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": false, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] + }, + "typeRoots": [ + "./@types", + "./node_modules/@types", + "./node_modules/@vue-macros", + "./node_modules" + ], + "types": [ + "vite/client", + ], + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "jsx": "preserve" + }, + "compileOnSave": false, + "include": [ + "./**/*.ts", + "./**/*.vue" + ], + "exclude": [ + ".storybook/**/*" + ] +} diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts new file mode 100644 index 000000000..bf2f47888 --- /dev/null +++ b/packages/frontend-embed/vite.config.local-dev.ts @@ -0,0 +1,96 @@ +import dns from 'dns'; +import { readFile } from 'node:fs/promises'; +import type { IncomingMessage } from 'node:http'; +import { defineConfig } from 'vite'; +import type { UserConfig } from 'vite'; +import * as yaml from 'js-yaml'; +import locales from '../../locales/index.js'; +import { getConfig } from './vite.config.js'; + +dns.setDefaultResultOrder('ipv4first'); + +const defaultConfig = getConfig(); + +const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')); + +const httpUrl = `http://localhost:${port}/`; +const websocketUrl = `ws://localhost:${port}/`; + +// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す +function varyHandler(req: IncomingMessage) { + if (req.headers.accept?.includes('application/activity+json')) { + return null; + } + return '/index.html'; +} + +const devConfig: UserConfig = { + // 基本の設定は vite.config.js から引き継ぐ + ...defaultConfig, + root: 'src', + publicDir: '../assets', + base: '/embed', + server: { + host: 'localhost', + port: 5174, + proxy: { + '/api': { + changeOrigin: true, + target: httpUrl, + }, + '/assets': httpUrl, + '/static-assets': httpUrl, + '/client-assets': httpUrl, + '/files': httpUrl, + '/twemoji': httpUrl, + '/fluent-emoji': httpUrl, + '/sw.js': httpUrl, + '/streaming': { + target: websocketUrl, + ws: true, + }, + '/favicon.ico': httpUrl, + '/robots.txt': httpUrl, + '/embed.js': httpUrl, + '/identicon': { + target: httpUrl, + rewrite(path) { + return path.replace('@localhost:5173', ''); + }, + }, + '/url': httpUrl, + '/proxy': httpUrl, + '/_info_card_': httpUrl, + '/bios': httpUrl, + '/cli': httpUrl, + '/inbox': httpUrl, + '/emoji/': httpUrl, + '/notes': { + target: httpUrl, + bypass: varyHandler, + }, + '/users': { + target: httpUrl, + bypass: varyHandler, + }, + '/.well-known': { + target: httpUrl, + }, + }, + }, + build: { + ...defaultConfig.build, + rollupOptions: { + ...defaultConfig.build?.rollupOptions, + input: 'index.html', + }, + }, + + define: { + ...defaultConfig.define, + _LANGS_FULL_: JSON.stringify(Object.entries(locales)), + }, +}; + +export default defineConfig(({ command, mode }) => devConfig); + diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts new file mode 100644 index 000000000..64e67401c --- /dev/null +++ b/packages/frontend-embed/vite.config.ts @@ -0,0 +1,156 @@ +import path from 'path'; +import pluginVue from '@vitejs/plugin-vue'; +import { type UserConfig, defineConfig } from 'vite'; + +import locales from '../../locales/index.js'; +import meta from '../../package.json'; +import packageInfo from './package.json' with { type: 'json' }; +import pluginJson5 from './vite.json5.js'; + +const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; + +/** + * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。 + * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK + */ +const externalPackages = [ + // shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む + { + name: 'shiki', + match: /^shiki\/(?(langs|themes))$/, + path(id: string, pattern: RegExp): string { + const match = pattern.exec(id)?.groups; + return match + ? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}` + : id; + }, + }, +]; + +const hash = (str: string, seed = 0): number => { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; + +const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +function toBase62(n: number): string { + if (n === 0) { + return '0'; + } + let result = ''; + while (n > 0) { + result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result; + n = Math.floor(n / BASE62_DIGITS.length); + } + + return result; +} + +export function getConfig(): UserConfig { + return { + base: '/embed_vite/', + + server: { + port: 5174, + }, + + plugins: [ + pluginVue(), + pluginJson5(), + ], + + resolve: { + extensions, + alias: { + '@/': __dirname + '/src/', + '@@/': __dirname + '/../frontend-shared/', + '/client-assets/': __dirname + '/assets/', + '/static-assets/': __dirname + '/../backend/assets/' + }, + }, + + css: { + modules: { + generateScopedName(name, filename, _css): string { + const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, ''); + if (process.env.NODE_ENV === 'production') { + return 'x' + toBase62(hash(id)).substring(0, 4); + } else { + return id; + } + }, + }, + }, + + define: { + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + __VUE_OPTIONS_API__: false, + __VUE_PROD_DEVTOOLS__: false, + }, + + build: { + target: [ + 'chrome116', + 'firefox116', + 'safari16', + ], + manifest: 'manifest.json', + rollupOptions: { + input: { + app: './src/boot.ts', + }, + external: externalPackages.map(p => p.match), + output: { + manualChunks: { + vue: ['vue'], + }, + chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', + assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', + paths(id) { + for (const p of externalPackages) { + if (p.match.test(id)) { + return p.path(id, p.match); + } + } + + return id; + }, + }, + }, + cssCodeSplit: true, + outDir: __dirname + '/../../built/_frontend_embed_vite_', + assetsDir: '.', + emptyOutDir: false, + sourcemap: process.env.NODE_ENV === 'development', + reportCompressedSize: false, + + // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies + commonjsOptions: { + include: [/misskey-js/, /node_modules/], + }, + }, + + worker: { + format: 'es', + }, + }; +} + +const config = defineConfig(({ command, mode }) => getConfig()); + +export default config; diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts new file mode 100644 index 000000000..87b67c214 --- /dev/null +++ b/packages/frontend-embed/vite.json5.ts @@ -0,0 +1,48 @@ +// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json + +import JSON5 from 'json5'; +import { Plugin } from 'rollup'; +import { createFilter, dataToEsm } from '@rollup/pluginutils'; +import { RollupJsonOptions } from '@rollup/plugin-json'; + +// json5 extends SyntaxError with additional fields (without subclassing) +// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112 +interface Json5SyntaxError extends SyntaxError { + lineNumber: number; + columnNumber: number; +} + +export default function json5(options: RollupJsonOptions = {}): Plugin { + const filter = createFilter(options.include, options.exclude); + const indent = 'indent' in options ? options.indent : '\t'; + + return { + name: 'json5', + + // eslint-disable-next-line no-shadow + transform(json, id) { + if (id.slice(-6) !== '.json5' || !filter(id)) return null; + + try { + const parsed = JSON5.parse(json); + return { + code: dataToEsm(parsed, { + preferConst: options.preferConst, + compact: options.compact, + namedExports: options.namedExports, + indent, + }), + map: { mappings: '' }, + }; + } catch (err) { + if (!(err instanceof SyntaxError)) { + throw err; + } + const message = 'Could not parse JSON5 file'; + const { lineNumber, columnNumber } = err as Json5SyntaxError; + this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } }); + return null; + } + }, + }; +} diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts new file mode 100644 index 000000000..eba994772 --- /dev/null +++ b/packages/frontend-embed/vue-shims.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module "*.vue" { + import { defineComponent } from "vue"; + const component: ReturnType; + export default component; +} diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore new file mode 100644 index 000000000..5f6be09d7 --- /dev/null +++ b/packages/frontend-shared/.gitignore @@ -0,0 +1,2 @@ +/storybook-static +js-built diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js new file mode 100644 index 000000000..17b6da8d3 --- /dev/null +++ b/packages/frontend-shared/build.js @@ -0,0 +1,106 @@ +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; +import { globSync } from 'glob'; +import { execa } from 'execa'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); +const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); + +const entryPoints = globSync('./js/**/**.{ts,tsx}'); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints, + minify: process.env.NODE_ENV === 'production', + outdir: './js-built', + target: 'es2022', + platform: 'browser', + format: 'esm', + sourcemap: 'linked', +}; + +// js-built配下をすべて削除する +fs.rmSync('./js-built', { recursive: true, force: true }); + +if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { + await watchSrc(); +} else { + await buildSrc(); +} + +async function buildSrc() { + console.log(`[${_package.name}] start building...`); + + await build(options) + .then(() => { + console.log(`[${_package.name}] build succeeded.`); + }) + .catch((err) => { + process.stderr.write(err.stderr); + process.exit(1); + }); + + if (process.env.NODE_ENV === 'production') { + console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`); + } else { + await buildDts(); + } + + fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json'); + + console.log(`[${_package.name}] finish building.`); +} + +function buildDts() { + return execa( + 'tsc', + [ + '--project', 'tsconfig.json', + '--outDir', 'js-built', + '--declaration', 'true', + '--emitDeclarationOnly', 'true', + ], + { + stdout: process.stdout, + stderr: process.stderr, + }, + ); +} + +async function watchSrc() { + const plugins = [{ + name: 'gen-dts', + setup(build) { + build.onStart(() => { + console.log(`[${_package.name}] detect changed...`); + }); + build.onEnd(async result => { + if (result.errors.length > 0) { + console.error(`[${_package.name}] watch build failed:`, result); + return; + } + await buildDts(); + }); + }, + }]; + + console.log(`[${_package.name}] start watching...`); + + const context = await esbuild.context({ ...options, plugins }); + await context.watch(); + + await new Promise((resolve, reject) => { + process.on('SIGHUP', resolve); + process.on('SIGINT', resolve); + process.on('SIGTERM', resolve); + process.on('uncaughtException', reject); + process.on('exit', resolve); + }).finally(async () => { + await context.dispose(); + console.log(`[${_package.name}] finish watching.`); + }); +} diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js new file mode 100644 index 000000000..a15fb29e3 --- /dev/null +++ b/packages/frontend-shared/eslint.config.js @@ -0,0 +1,96 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser from 'vue-eslint-parser'; +import pluginVue from 'eslint-plugin-vue'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; +import sharedConfig from '../shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + files: ['**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: ['js/**/*.{ts,vue}', '**/*.vue'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser, + + // Node.js + module: false, + require: false, + __dirname: false, + + // Misskey + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + _DATA_TRANSFER_DRIVE_FILE_: false, + _DATA_TRANSFER_DRIVE_FOLDER_: false, + _DATA_TRANSFER_DECK_COLUMN_: false, + }, + parser, + parserOptions: { + extraFileExtensions: ['.vue'], + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-empty-interface': ['error', { + allowSingleExtends: true, + }], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + alphabetical: false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + allowUsingIterationVar: false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + ignoreProperties: false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-reactivity-loss': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { + autofix: true, + }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + }, +]; diff --git a/packages/frontend/src/const.ts b/packages/frontend-shared/js/const.ts similarity index 98% rename from packages/frontend/src/const.ts rename to packages/frontend-shared/js/const.ts index e135bc69a..8391fb638 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -127,7 +127,7 @@ export const MFM_PARAMS: Record = { position: ['x=', 'y='], fg: ['color='], bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], + border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], blur: [], rainbow: ['speed=', 'delay='], diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts new file mode 100644 index 000000000..d5555a98c --- /dev/null +++ b/packages/frontend-shared/js/embed-page.ts @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//#region Embed関連の定義 + +/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */ +const embeddableEntities = [ + 'notes', + 'user-timeline', + 'clips', + 'tags', +] as const; + +/** 埋め込みの対象となるエンティティ */ +export type EmbeddableEntity = typeof embeddableEntities[number]; + +/** 内部でスクロールがあるページ */ +export const embedRouteWithScrollbar: EmbeddableEntity[] = [ + 'clips', + 'tags', + 'user-timeline', +]; + +/** 埋め込みコードのパラメータ */ +export type EmbedParams = { + maxHeight?: number; + colorMode?: 'light' | 'dark'; + rounded?: boolean; + border?: boolean; + autoload?: boolean; + header?: boolean; +}; + +/** 正規化されたパラメータ */ +export type ParsedEmbedParams = Required> & Pick; + +/** パラメータのデフォルトの値 */ +export const defaultEmbedParams = { + maxHeight: undefined, + colorMode: undefined, + rounded: true, + border: true, + autoload: false, + header: true, +} as const satisfies EmbedParams; + +//#endregion + +/** + * パラメータを正規化する(埋め込みページ初期化用) + * @param searchParams URLSearchParamsもしくはクエリ文字列 + * @returns 正規化されたパラメータ + */ +export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams { + let _searchParams: URLSearchParams; + if (typeof searchParams === 'string') { + _searchParams = new URLSearchParams(searchParams); + } else if (searchParams instanceof URLSearchParams) { + _searchParams = searchParams; + } else { + throw new Error('searchParams must be URLSearchParams or string'); + } + + function convertBoolean(value: string | null): boolean | undefined { + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } + return undefined; + } + + function convertNumber(value: string | null): number | undefined { + if (value != null && !isNaN(Number(value))) { + return Number(value); + } + return undefined; + } + + function convertColorMode(value: string | null): 'light' | 'dark' | undefined { + if (value != null && ['light', 'dark'].includes(value)) { + return value as 'light' | 'dark'; + } + return undefined; + } + + return { + maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight, + colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode, + rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded, + border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border, + autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload, + header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header, + }; +} diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts similarity index 100% rename from packages/frontend/src/scripts/emoji-base.ts rename to packages/frontend-shared/js/emoji-base.ts diff --git a/packages/frontend/src/emojilist.json b/packages/frontend-shared/js/emojilist.json similarity index 100% rename from packages/frontend/src/emojilist.json rename to packages/frontend-shared/js/emojilist.json diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend-shared/js/emojilist.ts similarity index 96% rename from packages/frontend/src/scripts/emojilist.ts rename to packages/frontend-shared/js/emojilist.ts index 6565feba9..bde30a864 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend-shared/js/emojilist.ts @@ -12,12 +12,12 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; +import _emojilist from './emojilist.json'; export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ name: x[1] as string, char: x[0] as string, - category: unicodeEmojiCategories[x[2]], + category: unicodeEmojiCategories[x[2] as number], })); const unicodeEmojisMap = new Map( diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts similarity index 100% rename from packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts rename to packages/frontend-shared/js/extract-avg-color-from-blurhash.ts diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend-shared/js/i18n.ts similarity index 91% rename from packages/frontend/src/scripts/i18n.ts rename to packages/frontend-shared/js/i18n.ts index b258a2a67..18232691f 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend-shared/js/i18n.ts @@ -2,7 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; +import type { ILocale, ParameterizedString } from '../../../locales/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TODO = any; type FlattenKeys = keyof { [K in keyof T as T[K] extends ILocale @@ -32,15 +35,18 @@ type Tsx = { export class I18n { private tsxCache?: Tsx; + private devMode: boolean; + + constructor(public locale: T, devMode = false) { + this.devMode = devMode; - constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } public get ts(): T { - if (_DEV_) { + if (this.devMode) { class Handler implements ProxyHandler { get(target: TTarget, p: string | symbol): unknown { const value = target[p as keyof TTarget]; @@ -72,7 +78,7 @@ export class I18n { } public get tsx(): Tsx { - if (_DEV_) { + if (this.devMode) { if (this.tsxCache) { return this.tsxCache; } @@ -113,7 +119,7 @@ export class I18n { return () => value; } - return (arg) => { + return (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -152,7 +158,7 @@ export class I18n { const value = target[k as keyof typeof target]; if (typeof value === 'object') { - result[k] = build(value as ILocale); + (result as TODO)[k] = build(value as ILocale); } else if (typeof value === 'string') { const quasis: string[] = []; const expressions: string[] = []; @@ -179,7 +185,7 @@ export class I18n { continue; } - result[k] = (arg) => { + (result as TODO)[k] = (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -208,9 +214,9 @@ export class I18n { let str: string | ParameterizedString | ILocale = this.locale; for (const k of key.split('.')) { - str = str[k]; + str = (str as TODO)[k]; - if (_DEV_) { + if (this.devMode) { if (typeof str === 'undefined') { console.error(`Unexpected locale key: ${key}`); return key; @@ -219,7 +225,7 @@ export class I18n { } if (args) { - if (_DEV_) { + if (this.devMode) { const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); if (missing.length) { @@ -230,7 +236,7 @@ export class I18n { for (const [k, v] of Object.entries(args)) { const search = `{${k}}`; - if (_DEV_) { + if (this.devMode) { if (!(str as string).includes(search)) { console.error(`Unexpected locale parameter: ${k} at ${key}`); } diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts new file mode 100644 index 000000000..2837870c9 --- /dev/null +++ b/packages/frontend-shared/js/media-proxy.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { query } from './url.js'; + +export class MediaProxy { + private serverMetadata: Misskey.entities.MetaDetailed; + private url: string; + + constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) { + this.serverMetadata = serverMetadata; + this.url = url; + } + + public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { + const localProxy = `${this.url}/proxy`; + let _imageUrl = imageUrl; + + if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // もう既にproxyっぽそうだったらurlを取り出す + _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; + } + + return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ + url: _imageUrl, + ...(!noFallback ? { 'fallback': '1' } : {}), + ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), + })}`; + } + + public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return this.getProxiedImageUrl(imageUrl, type); + } + + public getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url); + + if (u.href.startsWith(`${this.url}/emoji/`)) { + // もう既にemojiっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + return `${this.serverMetadata.mediaProxy}/static.webp?${query({ + url: u.href, + static: '1', + })}`; + } +} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend-shared/js/scroll.ts similarity index 98% rename from packages/frontend/src/scripts/scroll.ts rename to packages/frontend-shared/js/scroll.ts index f0274034b..1062e5252 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -45,7 +45,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o const container = getScrollContainer(el) ?? window; - const onScroll = ev => { + const onScroll = () => { if (!document.body.contains(el)) return; if (isTopVisible(el, tolerance)) { cb(); @@ -69,7 +69,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 } const containerOrWindow = container ?? window; - const onScroll = ev => { + const onScroll = () => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { cb(); diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend-shared/js/url.ts similarity index 70% rename from packages/frontend/src/scripts/url.ts rename to packages/frontend-shared/js/url.ts index 5a8265af9..eb830b1ee 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend-shared/js/url.ts @@ -8,18 +8,18 @@ * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) */ -export function query(obj: Record): string { +export function query(obj: Record): string { const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition + .reduce>((a, [k, v]) => (a[k] = v, a), {}); return Object.entries(params) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; +export function appendQuery(url: string, queryString: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; } export function extractDomain(url: string) { diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts similarity index 85% rename from packages/frontend/src/scripts/use-document-visibility.ts rename to packages/frontend-shared/js/use-document-visibility.ts index a8f4d5e03..b1197e68d 100644 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ b/packages/frontend-shared/js/use-document-visibility.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted, ref, Ref } from 'vue'; +import { onMounted, onUnmounted, ref } from 'vue'; +import type { Ref } from 'vue'; export function useDocumentVisibility(): Ref { const visibility = ref(document.visibilityState); diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend-shared/js/use-interval.ts similarity index 100% rename from packages/frontend/src/scripts/use-interval.ts rename to packages/frontend-shared/js/use-interval.ts diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json new file mode 100644 index 000000000..9981d10dd --- /dev/null +++ b/packages/frontend-shared/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend-shared", + "type": "module", + "main": "./js-built/index.js", + "types": "./js-built/index.d.ts", + "exports": { + ".": { + "import": "./js-built/index.js", + "types": "./js-built/index.d.ts" + }, + "./*": { + "import": "./js-built/*", + "types": "./js-built/*" + } + }, + "scripts": { + "build": "node ./build.js", + "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@types/node": "20.14.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "esbuild": "0.23.0", + "eslint-plugin-vue": "9.27.0", + "typescript": "5.5.4", + "vue-eslint-parser": "9.4.3" + }, + "files": [ + "js-built" + ], + "dependencies": { + "misskey-js": "workspace:*", + "vue": "3.4.37" + } +} diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 similarity index 100% rename from packages/frontend/src/themes/_dark.json5 rename to packages/frontend-shared/themes/_dark.json5 diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 similarity index 100% rename from packages/frontend/src/themes/_light.json5 rename to packages/frontend-shared/themes/_light.json5 diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5 similarity index 100% rename from packages/frontend/src/themes/d-astro.json5 rename to packages/frontend-shared/themes/d-astro.json5 diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/d-botanical.json5 rename to packages/frontend-shared/themes/d-botanical.json5 diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend-shared/themes/d-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/d-cherry.json5 rename to packages/frontend-shared/themes/d-cherry.json5 diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5 similarity index 100% rename from packages/frontend/src/themes/d-dark.json5 rename to packages/frontend-shared/themes/d-dark.json5 diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5 similarity index 100% rename from packages/frontend/src/themes/d-future.json5 rename to packages/frontend-shared/themes/d-future.json5 diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-lime.json5 rename to packages/frontend-shared/themes/d-green-lime.json5 diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-orange.json5 rename to packages/frontend-shared/themes/d-green-orange.json5 diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend-shared/themes/d-ice.json5 similarity index 100% rename from packages/frontend/src/themes/d-ice.json5 rename to packages/frontend-shared/themes/d-ice.json5 diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5 similarity index 100% rename from packages/frontend/src/themes/d-persimmon.json5 rename to packages/frontend-shared/themes/d-persimmon.json5 diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5 similarity index 100% rename from packages/frontend/src/themes/d-u0.json5 rename to packages/frontend-shared/themes/d-u0.json5 diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend-shared/themes/l-apricot.json5 similarity index 100% rename from packages/frontend/src/themes/l-apricot.json5 rename to packages/frontend-shared/themes/l-apricot.json5 diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/l-botanical.json5 rename to packages/frontend-shared/themes/l-botanical.json5 diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend-shared/themes/l-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/l-cherry.json5 rename to packages/frontend-shared/themes/l-cherry.json5 diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5 similarity index 100% rename from packages/frontend/src/themes/l-coffee.json5 rename to packages/frontend-shared/themes/l-coffee.json5 diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5 similarity index 100% rename from packages/frontend/src/themes/l-light.json5 rename to packages/frontend-shared/themes/l-light.json5 diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5 similarity index 100% rename from packages/frontend/src/themes/l-rainy.json5 rename to packages/frontend-shared/themes/l-rainy.json5 diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend-shared/themes/l-sushi.json5 similarity index 100% rename from packages/frontend/src/themes/l-sushi.json5 rename to packages/frontend-shared/themes/l-sushi.json5 diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5 similarity index 100% rename from packages/frontend/src/themes/l-u0.json5 rename to packages/frontend-shared/themes/l-u0.json5 diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5 similarity index 100% rename from packages/frontend/src/themes/l-vivid.json5 rename to packages/frontend-shared/themes/l-vivid.json5 diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json new file mode 100644 index 000000000..fa0b76553 --- /dev/null +++ b/packages/frontend-shared/tsconfig.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": false, + "outDir": "./js-built/", + "removeComments": true, + "resolveJsonModule": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "js/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index fb93d7be1..e5573f2ac 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -30,7 +30,7 @@ const keys = [ 'd-u0', ] -await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts index 0a7281898..70afc356c 100644 --- a/packages/frontend/@types/theme.d.ts +++ b/packages/frontend/@types/theme.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -declare module '@/themes/*.json5' { +declare module '@@/themes/*.json5' { import { Theme } from '@/scripts/theme.js'; const theme: Theme; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1464be18a..67be7f059 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -55,6 +55,7 @@ "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", + "frontend-shared": "workspace:*", "photoswipe": "5.4.4", "punycode": "2.3.1", "rollup": "4.19.1", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d86ae18ff..19d30f64c 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { setupRouter } from '@/router/definition.js'; +import { setupRouter } from '@/router/main.js'; +import { createMainRouter } from '@/router/definition.js'; export async function common(createVue: () => App) { console.info(`Misskey v${version}`); @@ -239,7 +240,7 @@ export async function common(createVue: () => App) { const app = createVue(); - setupRouter(app); + setupRouter(app, createMainRouter); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 3e7c4f26f..b31281dcf 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; +import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -62,6 +63,18 @@ export async function mainBoot() { } }); + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); + }); + + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); + + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 932c4ecb2..f54799136 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index c13164c29..fca7aa2f4 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only `, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) + * + * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b5d7350a4..e0ccea813 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -21,6 +21,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref; @@ -310,7 +324,7 @@ export function getNoteMenu(props: { action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode), ...(isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, @@ -443,14 +457,14 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? { + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink), + (appearNote.url || appearNote.uri) ? { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined] + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)] .filter(x => x !== undefined); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68a..035abc7bd 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { @@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (user.url == null) return; window.open(user.url, '_blank', 'noopener'); }, - }] : []), { + }] : [{ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent' as const, + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }]), { icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5..20f51660c 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/is-link.ts b/packages/frontend/src/scripts/is-link.ts new file mode 100644 index 000000000..946f86400 --- /dev/null +++ b/packages/frontend/src/scripts/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163..68a5a1dcf 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; import { url } from '@/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 9938e534c..bf59fe98a 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc2..ed49611b4 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; +import { appendQuery } from '@@/js/url.js'; import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad..11b6f52dd 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c3..000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eb..000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 000000000..cb0e607fc --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf> = T[keyof T]; +type OmitFirst = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時に使うStreamのモック(なにもしない) + */ +export class StreamMock extends EventEmitter implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters) { + super(); + // do nothing + } + + public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record | any[]): void + public send(typeOrPayload: string | Record | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index c7f8b3d59..9b9f1f030 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 437314074..0bf499bb4 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -458,10 +458,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -520,8 +520,8 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ -import lightTheme from '@/themes/l-light.json5'; -import darkTheme from '@/themes/d-green-lime.json5'; +import lightTheme from '@@/themes/l-light.json5'; +import darkTheme from '@@/themes/d-green-lime.json5'; export class ColdDeviceStorage { public static default = { @@ -558,7 +558,7 @@ export class ColdDeviceStorage { public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 0d5bd78b0..9d7edce89 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -7,17 +7,20 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; import { wsOrigin } from '@/config.js'; +// TODO: No WebsocketモードでStreamMockが使えそう +//import { StreamMock } from '@/scripts/stream-mock.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: ReturnType | null = null; +let stream: Misskey.IStream | null = null; +let timeoutHeartBeat: number | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.Stream { +export function useStream(): Misskey.IStream { if (stream) return stream; + // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 8dad66662..e234bb3a3 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -35,7 +35,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 6e1d06eec..550fc39b0 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 67f8b109c..078b595dc 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -35,7 +35,7 @@ import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 79c967191..e7ecf7fd2 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,7 +26,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 073acbd4d..00a6811fc 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -108,7 +108,7 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 49fd103d3..bcfaaf00a 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index b0ffac93d..6710d9826 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; @@ -69,7 +69,7 @@ async function initHighlighter() { ]); const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await getHighlighterCore({ + const highlighter = await createHighlighterCore({ themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), From 6b2072f4b1e6a191634b51b448442aaf57df5434 Mon Sep 17 00:00:00 2001 From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:13:46 +0900 Subject: [PATCH 037/159] =?UTF-8?q?fix(backend/antenna):=20=E3=82=AD?= =?UTF-8?q?=E3=83=BC=E3=83=AF=E3=83=BC=E3=83=89=E3=81=8C=E4=B8=8E=E3=81=88?= =?UTF-8?q?=E3=82=89=E3=82=8C=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92ApiError?= =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E6=8A=95=E3=81=92=E3=82=8B=20(#1449?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend/antenna): report validation failure as ApiError on update * test(backend/antenna): reflect change in previous commit * fix(backend/antenna): report validation failure as ApiError on create * test(backend/antenna): reflect change in previous commit * test(backend/antenna): semi * test(backend/antenna): bring being spread parameters first in object literal * chore: add CHANGELOG entry --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 4 +++- .../server/api/endpoints/antennas/create.ts | 8 ++++++- .../server/api/endpoints/antennas/update.ts | 8 ++++++- packages/backend/test/e2e/antennas.ts | 23 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2d9f102..62d9d4def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ - Fix: 月の違う同じ日はセパレータが表示されないのを修正 ### Server +- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 +- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように + - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 - Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) - ## 2024.8.0 ### General diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 577b9e1b1..e0c8ddcc8 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -34,6 +34,12 @@ export const meta = { code: 'TOO_MANY_ANTENNAS', id: 'faf47050-e8b5-438c-913c-db2b1576fde4', }, + + emptyKeyword: { + message: 'Either keywords or excludeKeywords is required.', + code: 'EMPTY_KEYWORD', + id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a', + }, }, res: { @@ -87,7 +93,7 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + throw new ApiError(meta.errors.emptyKeyword); } const currentAntennasCount = await this.antennasRepository.countBy({ diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 0c30bca9e..10f26b191 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -32,6 +32,12 @@ export const meta = { code: 'NO_SUCH_USER_LIST', id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }, + + emptyKeyword: { + message: 'Either keywords or excludeKeywords is required.', + code: 'EMPTY_KEYWORD', + id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4', + }, }, res: { @@ -85,7 +91,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { if (ps.keywords && ps.excludeKeywords) { if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + throw new ApiError(meta.errors.emptyKeyword); } } // Fetch the antenna diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 6ac14cd8d..a544db955 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -228,6 +228,17 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); + test('を作成する時キーワードが指定されていないとエラーになる', async () => { + await failedApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a' + }) + }); //#endregion //#region 更新(antennas/update) @@ -255,6 +266,18 @@ describe('アンテナ', () => { id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }); }); + test('を変更する時キーワードが指定されていないとエラーになる', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + await failedApiCall({ + endpoint: 'antennas/update', + parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4' + }) + }); //#endregion //#region 表示(antennas/show) From 366b79e4595b709f5a6b8b4700eb93510d41072a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:14:13 +0900 Subject: [PATCH 038/159] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d9d4def..ffe03c13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ - Fix: 月の違う同じ日はセパレータが表示されないのを修正 ### Server -- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 - Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 From 07f26bc8dd199ff366e6278a8ac1521497922b95 Mon Sep 17 00:00:00 2001 From: Juan Aguilar Santillana Date: Sun, 15 Sep 2024 10:43:24 +0200 Subject: [PATCH 039/159] refactor(backend): use Reflet for autobind deco (#14482) Using Reflect.defineProperty instead of Object.defineProperty gives a more consistent behavior with the rest of the modern JavaScript features. --- packages/backend/src/decorators.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 21777657d..42f925e12 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -10,8 +10,9 @@ * The getter will return a .bind version of the function * and memoize the result against a symbol on the instance */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function bindThis(target: any, key: string, descriptor: any) { - let fn = descriptor.value; + const fn = descriptor.value; if (typeof fn !== 'function') { throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`); @@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) { configurable: true, get() { // eslint-disable-next-line no-prototype-builtins - if (this === target.prototype || this.hasOwnProperty(key) || - typeof fn !== 'function') { + if (this === target.prototype || this.hasOwnProperty(key)) { return fn; } const boundFn = fn.bind(this); - Object.defineProperty(this, key, { + Reflect.defineProperty(this, key, { + value: boundFn, configurable: true, - get() { - return boundFn; - }, - set(value) { - fn = value; - delete this[key]; - }, + writable: true, }); + return boundFn; }, - set(value: any) { - fn = value; - }, }; } From 0e4b6d1dade90673af58af3480081c95984c0274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:50:25 +0900 Subject: [PATCH 040/159] =?UTF-8?q?enhance(frontend):=20admin=E3=81=AE?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=83=AA=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A7=E3=82=BB=E3=83=B3=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AB=E6=9E=A0=E7=B7=9A?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#14510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): adminのファイルリストでセンシティブファイルに枠線を追加 * Update Changelog --- CHANGELOG.md | 1 + .../src/components/MkDriveFileThumbnail.vue | 21 ++++++++++++++++++- .../src/components/MkFileListForAdmin.vue | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe03c13c..c01d284bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です - Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように - Enhance: アイコンデコレーション管理画面にプレビューを追加 +- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正 diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 2c47a7097..eb93aaab6 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> -
+
+ + + +
{{ i18n.ts.edit }} @@ -40,6 +44,7 @@ import { toRefs } from 'vue'; import MkFolder from '@/components/MkFolder.vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; const emit = defineEmits<{ (ev: 'edit', value: entities.SystemWebhook): void; From 023fa30280e561e9921a2c83138af4cac01068ab Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 12:53:13 +0900 Subject: [PATCH 071/159] refactor/perf(backend): provide metadata statically (#14601) * wip * Update ReactionService.ts * Update ApiCallService.ts * Update timeline.ts * Update GlobalModule.ts * Update GlobalModule.ts * Update NoteEntityService.ts * wip * wip * wip * Update ApPersonService.ts * wip * Update GlobalModule.ts * Update mock-resolver.ts * Update RoleService.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * clean up * Update utils.ts * Update UtilityService.ts * Revert "Update utils.ts" This reverts commit a27d4be764b78c1b5a9eac685e261fee49331d89. * Revert "Update UtilityService.ts" This reverts commit e5fd9e004c482cf099252201c0c1aa888e001430. * vuwa- * Revert "vuwa-" This reverts commit 0c3bd12472b4b9938cdff2d6f131e6800bc3724c. * Update entry.ts * Update entry.ts * Update entry.ts * Update entry.ts * Update jest.setup.ts --- packages/backend/src/GlobalModule.ts | 63 +++++++++++++++++- .../core/AbuseReportNotificationService.ts | 12 ++-- .../backend/src/core/AccountMoveService.ts | 9 +-- packages/backend/src/core/DriveService.ts | 61 ++++++++--------- packages/backend/src/core/EmailService.ts | 43 ++++++------ packages/backend/src/core/HashtagService.ts | 12 ++-- .../backend/src/core/NoteCreateService.ts | 60 ++++++++--------- .../backend/src/core/NoteDeleteService.ts | 15 ++--- .../backend/src/core/ProxyAccountService.ts | 13 ++-- .../src/core/PushNotificationService.ts | 16 ++--- packages/backend/src/core/ReactionService.ts | 19 +++--- packages/backend/src/core/RoleService.ts | 10 ++- packages/backend/src/core/SignupService.ts | 10 +-- .../backend/src/core/UserFollowingService.ts | 20 +++--- packages/backend/src/core/WebAuthnService.ts | 30 ++++----- .../src/core/activitypub/ApInboxService.ts | 10 +-- .../src/core/activitypub/ApResolverService.ts | 14 ++-- .../core/activitypub/models/ApImageService.ts | 11 ++-- .../core/activitypub/models/ApNoteService.ts | 12 ++-- .../activitypub/models/ApPersonService.ts | 12 ++-- .../src/core/chart/charts/federation.ts | 19 +++--- .../core/entities/InstanceEntityService.ts | 16 +++-- .../src/core/entities/MetaEntityService.ts | 9 +-- .../src/core/entities/NoteEntityService.ts | 16 ++--- .../backend/src/daemons/ServerStatsService.ts | 10 +-- packages/backend/src/di-symbols.ts | 1 + .../BakeBufferedReactionsProcessorService.ts | 10 +-- .../processors/DeliverProcessorService.ts | 14 ++-- .../queue/processors/InboxProcessorService.ts | 16 +++-- packages/backend/src/server/ServerService.ts | 9 +-- .../backend/src/server/api/ApiCallService.ts | 12 ++-- .../src/server/api/SignupApiService.ts | 35 +++++----- .../src/server/api/endpoints/ap/show.ts | 14 ++-- .../server/api/endpoints/channels/timeline.ts | 13 ++-- .../backend/src/server/api/endpoints/drive.ts | 5 -- .../api/endpoints/drive/files/create.ts | 15 +++-- .../server/api/endpoints/i/update-email.ts | 9 +-- .../src/server/api/endpoints/notes/create.ts | 2 - .../api/endpoints/notes/hybrid-timeline.ts | 13 ++-- .../api/endpoints/notes/local-timeline.ts | 15 ++--- .../server/api/endpoints/notes/timeline.ts | 13 ++-- .../server/api/endpoints/notes/translate.ts | 17 ++--- .../api/endpoints/notes/user-list-timeline.ts | 15 ++--- .../src/server/api/endpoints/pinned-users.ts | 11 ++-- .../src/server/api/endpoints/server-info.ts | 10 +-- .../src/server/api/endpoints/sw/register.ts | 13 ++-- .../api/endpoints/username/available.ts | 11 ++-- .../src/server/api/endpoints/users/notes.ts | 11 ++-- .../src/server/web/ClientServerService.ts | 66 +++++++------------ .../src/server/web/UrlPreviewService.ts | 17 +++-- packages/backend/test-server/entry.ts | 22 ++++++- packages/backend/test/jest.setup.ts | 6 +- packages/backend/test/misc/mock-resolver.ts | 3 +- packages/backend/test/unit/RoleService.ts | 45 +++++-------- packages/backend/test/unit/activitypub.ts | 21 +++--- 55 files changed, 499 insertions(+), 487 deletions(-) diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 2ecc1f474..0f69cf93a 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import { allSettled } from './misc/promise-tracker.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/Meta.js'; +import { GlobalEvents } from './core/GlobalEventService.js'; const $config: Provider = { provide: DI.config, @@ -86,11 +88,68 @@ const $redisForReactions: Provider = { inject: [DI.config], }; +const $meta: Provider = { + provide: DI.meta, + useFactory: async (db: DataSource, redisForSub: Redis.Redis) => { + const meta = await db.transaction(async transactionalEntityManager => { + // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する + const metas = await transactionalEntityManager.find(MiMeta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + return meta; + } else { + // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う + const saved = await transactionalEntityManager + .upsert( + MiMeta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + + return saved; + } + }); + + async function onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'metaUpdated': { + for (const key in body) { + (meta as any)[key] = (body as any)[key]; + } + meta.proxyAccount = null; // joinなカラムは通常取ってこないので + break; + } + default: + break; + } + } + } + + redisForSub.on('message', onMessage); + + return meta; + }, + inject: [DI.db, DI.redisForSub], +}; + @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], - exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], + providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], + exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index 7be533588..fe2c63e7d 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -14,10 +14,10 @@ import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient, MiAbuseUserReport, + MiMeta, MiUser, } from '@/models/_.js'; import { EmailService } from '@/core/EmailService.js'; -import { MetaService } from '@/core/MetaService.js'; import { RoleService } from '@/core/RoleService.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -27,15 +27,19 @@ import { IdService } from './IdService.js'; @Injectable() export class AbuseReportNotificationService implements OnApplicationShutdown { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.abuseReportNotificationRecipientRepository) private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + private idService: IdService, private roleService: RoleService, private systemWebhookService: SystemWebhookService, private emailService: EmailService, - private metaService: MetaService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { @@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .filter(x => x != null), ); - // 送信先の鮮度を保つため、毎回取得する - const meta = await this.metaService.fetch(true); recipientEMailAddresses.push( - ...(meta.email ? [meta.email] : []), + ...(this.meta.email ? [this.meta.email] : []), ); if (recipientEMailAddresses.length <= 0) { diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index b6b591d24..6e3125044 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { MetaService } from '@/core/MetaService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @Injectable() export class AccountMoveService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -57,7 +59,6 @@ export class AccountMoveService { private perUserFollowingChart: PerUserFollowingChart, private federatedInstanceService: FederatedInstanceService, private instanceChart: InstanceChart, - private metaService: MetaService, private relayService: RelayService, private queueService: QueueService, ) { @@ -276,7 +277,7 @@ export class AccountMoveService { if (this.userEntityService.isRemoteUser(oldAccount)) { this.federatedInstanceService.fetch(oldAccount.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 8aa04b4da..c332e5a0a 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -99,6 +98,9 @@ export class DriveService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -115,7 +117,6 @@ export class DriveService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, - private metaService: MetaService, private downloadService: DownloadService, private internalStorageService: InternalStorageService, private s3Service: S3Service, @@ -149,9 +150,7 @@ export class DriveService { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); - const meta = await this.metaService.fetch(); - - if (meta.useObjectStorage) { + if (this.meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); @@ -170,11 +169,11 @@ export class DriveService { ext = ''; } - const baseUrl = meta.objectStorageBaseUrl - ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; + const baseUrl = this.meta.objectStorageBaseUrl + ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; // for original - const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`; + const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -191,7 +190,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -199,7 +198,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -376,10 +375,8 @@ export class DriveService { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; - const meta = await this.metaService.fetch(); - const params = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, Body: stream, ContentType: type, @@ -392,9 +389,9 @@ export class DriveService { // 許可されているファイル形式でしか拡張子をつけない ext ? correctFilename(filename, ext) : filename, ); - if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; + if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - await this.s3Service.upload(meta, params) + await this.s3Service.upload(this.meta, params) .then( result => { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput @@ -460,32 +457,31 @@ export class DriveService { ext = null, }: AddFileArgs): Promise { let skipNsfwCheck = false; - const instance = await this.metaService.fetch(); const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; if (user == null) { skipNsfwCheck = true; } else if (userRoleNSFW) { skipNsfwCheck = true; } - if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; + if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true; + if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; + if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; const info = await this.fileInfoService.getFileInfo(path, { skipSensitiveDetection: skipNsfwCheck, sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : 0.5, sensitiveThresholdForPorn: 0.75, - enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos, }); this.registerLogger.info(`${JSON.stringify(info)}`); // 現状 false positive が多すぎて実用に耐えない - //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { + //if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) { // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); //} @@ -589,9 +585,9 @@ export class DriveService { sensitive ?? false : false; - if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; + if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; - if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; + if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; if (url !== null) { @@ -652,7 +648,7 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, true); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, true); } } @@ -798,7 +794,7 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, false); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, false); } } @@ -820,14 +816,13 @@ export class DriveService { @bindThis public async deleteObjectStorageFile(key: string) { - const meta = await this.metaService.fetch(); try { const param = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, } as DeleteObjectCommandInput; - await this.s3Service.delete(meta, param); + await this.s3Service.delete(this.meta, param); } catch (err: any) { if (err.name === 'NoSuchKey') { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 37fa58bb6..a176474b9 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -8,16 +8,14 @@ import * as nodemailer from 'nodemailer'; import juice from 'juice'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { QueueService } from '@/core/QueueService.js'; @Injectable() export class EmailService { @@ -27,38 +25,37 @@ export class EmailService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, - private queueService: QueueService, ) { this.logger = this.loggerService.getLogger('email'); } @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { - const meta = await this.metaService.fetch(true); - - if (!meta.enableEmail) return; + if (!this.meta.enableEmail) return; const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; - const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== ''; const transporter = nodemailer.createTransport({ - host: meta.smtpHost, - port: meta.smtpPort, - secure: meta.smtpSecure, + host: this.meta.smtpHost, + port: this.meta.smtpPort, + secure: this.meta.smtpSecure, ignoreTLS: !enableAuth, proxy: this.config.proxySmtp, auth: enableAuth ? { - user: meta.smtpUser, - pass: meta.smtpPass, + user: this.meta.smtpUser, + pass: this.meta.smtpPass, } : undefined, } as any); @@ -127,7 +124,7 @@ export class EmailService {
- +

${ subject }

@@ -148,7 +145,7 @@ export class EmailService { try { // TODO: htmlサニタイズ const info = await transporter.sendMail({ - from: meta.email!, + from: this.meta.email!, to: to, subject: subject, text: text, @@ -167,8 +164,6 @@ export class EmailService { available: boolean; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { - const meta = await this.metaService.fetch(); - const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, @@ -186,11 +181,11 @@ export class EmailService { reason?: string | null, } = { valid: true, reason: null }; - if (meta.enableActiveEmailValidation) { - if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { - validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); - } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { - validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); + if (this.meta.enableActiveEmailValidation) { + if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) { + validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey); + } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) { + validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey); } else { validated = await validateEmail({ email: emailAddress, @@ -220,7 +215,7 @@ export class EmailService { } const emailDomain: string = emailAddress.split('@')[1]; - const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); + const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); if (isBanned) { return { diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index eb192ee6d..793bbeecb 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; import type { MiHashtag } from '@/models/Hashtag.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository, MiMeta } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする @@ -29,7 +31,6 @@ export class HashtagService { private userEntityService: UserEntityService, private featuredService: FeaturedService, private idService: IdService, - private metaService: MetaService, private utilityService: UtilityService, ) { } @@ -160,10 +161,9 @@ export class HashtagService { @bindThis public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise { - const instance = await this.metaService.fetch(); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; + if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1d8d24832..18efc9d56 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -8,13 +8,12 @@ import * as mfm from 'mfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -23,11 +22,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { IPoll } from '@/models/Poll.js'; import { MiPoll } from '@/models/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -51,7 +47,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; @@ -156,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -210,7 +208,6 @@ export class NoteCreateService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, - private metaService: MetaService, private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, @@ -251,10 +248,8 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - const meta = await this.metaService.fetch(); - if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = meta.sensitiveWords; + const sensitiveWords = this.meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { @@ -262,17 +257,17 @@ export class NoteCreateService implements OnApplicationShutdown { } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ + const hasProhibitedWords = this.checkProhibitedWordsContain({ cw: data.cw, text: data.text, pollChoices: data.poll?.choices, - }, meta.prohibitedWords); + }, this.meta.prohibitedWords); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } - const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); + const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { data.visibility = 'home'; @@ -365,7 +360,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = []; tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); @@ -506,10 +501,8 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { - const meta = await this.metaService.fetch(); - this.notesChart.update(note, true); - if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) { + if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) { this.perUserNotesChart.update(user, note, true); } @@ -517,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, true); } }); @@ -853,15 +846,14 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { - const meta = await this.metaService.fetch(); - if (!meta.enableFanoutTimeline) return; + if (!this.meta.enableFanoutTimeline) return; const r = this.redisForTimelines.pipeline(); if (note.channelId) { this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -871,9 +863,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -911,9 +903,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -930,25 +922,25 @@ export class NoteCreateService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r); } } // 自分自身のHTL if (note.userHost == null) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } // 自分自身以外への返信 if (isReply(note)) { - this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); @@ -957,9 +949,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } } else { - this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { @@ -1018,9 +1010,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } - public async checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { + public checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { if (prohibitedWords == null) { - prohibitedWords = (await this.metaService.fetch()).prohibitedWords; + prohibitedWords = this.meta.prohibitedWords; } if ( diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index b7c01c64c..f9f8ace38 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +19,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; @@ -32,6 +30,9 @@ export class NoteDeleteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,13 +43,11 @@ export class NoteDeleteService { private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, - private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private metaService: MetaService, private searchService: SearchService, private moderationLogService: ModerationLogService, private notesChart: NotesChart, @@ -102,17 +101,15 @@ export class NoteDeleteService { } //#endregion - const meta = await this.metaService.fetch(); - this.notesChart.update(note, false); - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, false); } if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, false); } }); diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 71d663bf9..c3ff2a68d 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -4,26 +4,25 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import type { MiLocalUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class ProxyAccountService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - - private metaService: MetaService, ) { } @bindThis public async fetch(): Promise { - const meta = await this.metaService.fetch(); - if (meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser; + if (this.meta.proxyAccountId == null) return null; + return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser; } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 6a845b951..1479bb00d 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private metaService: MetaService, ) { this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { lifetime: 1000 * 60 * 60 * 1, // 1h @@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown { @bindThis public async pushNotification(userId: string, type: T, body: PushNotificationsTypes[T]) { - const meta = await this.metaService.fetch(); - - if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; + if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return; // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 push.setVapidDetails(this.config.url, - meta.swPublicKey, - meta.swPrivateKey); + this.meta.swPublicKey, + this.meta.swPrivateKey); const subscriptions = await this.subscriptionsCache.fetch(userId); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index db8fe1a83..f0a287645 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -20,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; @@ -71,6 +70,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; @Injectable() export class ReactionService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -84,7 +86,6 @@ export class ReactionService { private emojisRepository: EmojisRepository, private utilityService: UtilityService, - private metaService: MetaService, private customEmojiService: CustomEmojiService, private roleService: RoleService, private userEntityService: UserEntityService, @@ -103,8 +104,6 @@ export class ReactionService { @bindThis public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { - const meta = await this.metaService.fetch(); - // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -150,7 +149,7 @@ export class ReactionService { } // for media silenced host, custom emoji reactions are not allowed - if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { + if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) { reaction = FALLBACK; } } else { @@ -195,7 +194,7 @@ export class ReactionService { } // Increment reactions count - if (meta.enableReactionsBuffering) { + if (this.meta.enableReactionsBuffering) { await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache); } else { const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; @@ -228,7 +227,7 @@ export class ReactionService { } } - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } @@ -305,10 +304,8 @@ export class ReactionService { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - const meta = await this.metaService.fetch(); - // Decrement reactions count - if (meta.enableReactionsBuffering) { + if (this.meta.enableReactionsBuffering) { await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction); } else { const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 24752edcf..583eea1a3 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -8,6 +8,7 @@ import * as Redis from 'ioredis'; import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import type { + MiMeta, MiRole, MiRoleAssignment, RoleAssignmentsRepository, @@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -111,8 +111,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.meta) + private meta: MiMeta, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @@ -129,7 +129,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, - private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, @@ -349,8 +348,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @bindThis public async getUserPolicies(userId: MiUser['id'] | null): Promise { - const meta = await this.metaService.fetch(); - const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; + const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies }; if (userId == null) return basePolicies; diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index de4589832..cc8a3d646 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; @@ -20,7 +20,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserService } from '@/core/UserService.js'; @Injectable() @@ -29,6 +28,9 @@ export class SignupService { @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -39,7 +41,6 @@ export class SignupService { private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private instanceActorService: InstanceActorService, private usersChart: UsersChart, ) { @@ -88,8 +89,7 @@ export class SignupService { const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); if (!opts.ignorePreservedUsernames && !isTheFirstUser) { - const instance = await this.metaService.fetch(true); - const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new Error('USED_USERNAME'); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 6aab8fde7..3f1c6b712 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { UserWebhookService } from '@/core/UserWebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; @@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: UserWebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit { followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || - (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host)) + (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host)) ) { let autoAccept = false; @@ -307,14 +305,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, true); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, true); } }); @@ -439,14 +437,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, false); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ec9f4484a..a40c6ff1c 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -12,10 +12,9 @@ import { } from '@simplewebauthn/server'; import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; import { DI } from '@/di-symbols.js'; -import type { UserSecurityKeysRepository } from '@/models/_.js'; +import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiUser } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { @@ -23,7 +22,6 @@ import type { AuthenticatorTransportFuture, CredentialDeviceType, PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialDescriptorFuture, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/types'; @@ -31,33 +29,33 @@ import type { @Injectable() export class WebAuthnService { constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - - private metaService: MetaService, ) { } @bindThis - public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> { - const instance = await this.metaService.fetch(); + public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } { return { origin: this.config.url, rpId: this.config.hostname, - rpName: instance.name ?? this.config.host, - rpIcon: instance.iconUrl ?? undefined, + rpName: this.meta.name ?? this.config.host, + rpIcon: this.meta.iconUrl ?? undefined, }; } @bindThis public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -104,7 +102,7 @@ export class WebAuthnService { await this.redisClient.del(`webauthn:challenge:${userId}`); - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { @@ -143,7 +141,7 @@ export class WebAuthnService { @bindThis public async initiateAuthentication(userId: MiUser['id']): Promise { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -209,7 +207,7 @@ export class WebAuthnService { } } - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e2164fec1..90da03289 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -17,14 +17,13 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -48,6 +47,9 @@ export class ApInboxService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -64,7 +66,6 @@ export class ApInboxService { private noteEntityService: NoteEntityService, private utilityService: UtilityService, private idService: IdService, - private metaService: MetaService, private abuseReportService: AbuseReportService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, @@ -290,8 +291,7 @@ export class ApInboxService { } // アナウンス先をブロックしてたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bb3c40f09..fdef7a8ff 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; -import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -29,6 +28,7 @@ export class Resolver { constructor( private config: Config, + private meta: MiMeta, private usersRepository: UsersRepository, private notesRepository: NotesRepository, private pollsRepository: PollsRepository, @@ -36,7 +36,6 @@ export class Resolver { private followRequestsRepository: FollowRequestsRepository, private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -94,8 +93,7 @@ export class Resolver { return await this.resolveLocal(value); } - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { throw new Error('Instance is blocked'); } @@ -178,6 +176,9 @@ export class ApResolverService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -195,7 +196,6 @@ export class ApResolverService { private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -208,6 +208,7 @@ export class ApResolverService { public createResolver(): Resolver { return new Resolver( this.config, + this.meta, this.usersRepository, this.notesRepository, this.pollsRepository, @@ -215,7 +216,6 @@ export class ApResolverService { this.followRequestsRepository, this.utilityService, this.instanceActorService, - this.metaService, this.apRequestService, this.httpRequestService, this.apRendererService, diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 369196727..e7ece87b0 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -5,10 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; @@ -24,10 +23,12 @@ export class ApImageService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, @@ -63,12 +64,10 @@ export class ApImageService { this.logger.info(`Creating the Image: ${image.url}`); - const instance = await this.metaService.fetch(); - // Cache if remote file cache is on AND either // 1. remote sensitive file is also on // 2. or the image is not sensitive - const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive); const file = await this.driveService.uploadFromUrl({ url: image.url, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 5b75da22a..00acb19a0 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -6,13 +6,12 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -46,6 +45,9 @@ export class ApNoteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -65,7 +67,6 @@ export class ApNoteService { private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, - private metaService: MetaService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -182,7 +183,7 @@ export class ApNoteService { /** * 禁止ワードチェック */ - const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); + const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } @@ -336,8 +337,7 @@ export class ApNoteService { const uri = getApId(value); // ブロックしていたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) { throw new StatusError('blocked host', 451); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f3ddf3952..39c18e5e1 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js'; @@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; @@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private idService: IdService; private globalEventService: GlobalEventService; - private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private cacheService: CacheService; @@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); - this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.cacheService = this.moduleRef.get('CacheService'); @@ -407,10 +407,10 @@ export class ApPersonService implements OnModuleInit { this.cacheService.uriPersonCache.set(user.uri, user); // Register host - this.federatedInstanceService.fetch(host).then(async i => { + this.federatedInstanceService.fetch(host).then(i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.newUser(i.host); } }); diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index f40a26495..c9b43cc66 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -5,10 +5,9 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -24,13 +23,15 @@ export default class FederationChart extends Chart { // eslint-di @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private metaService: MetaService, private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { @@ -43,8 +44,6 @@ export default class FederationChart extends Chart { // eslint-di } protected async tickMinor(): Promise>> { - const meta = await this.metaService.fetch(); - const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') .where('instance.suspensionState != \'none\''); @@ -65,21 +64,21 @@ export default class FederationChart extends Chart { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -88,7 +87,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -96,7 +95,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 4956bc22c..284537b98 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -3,19 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { RoleService } from '@/core/RoleService.js'; import { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { MiMeta } from '@/models/_.js'; @Injectable() export class InstanceEntityService { constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private roleService: RoleService, private utilityService: UtilityService, @@ -27,7 +30,6 @@ export class InstanceEntityService { instance: MiInstance, me?: { id: MiUser['id']; } | null | undefined, ): Promise> { - const meta = await this.metaService.fetch(); const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; return { @@ -41,7 +43,7 @@ export class InstanceEntityService { isNotResponding: instance.isNotResponding, isSuspended: instance.suspensionState !== 'none', suspensionState: instance.suspensionState, - isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), + isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, @@ -49,8 +51,8 @@ export class InstanceEntityService { description: instance.description, maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, - isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), - isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), + isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host), + isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index f4b1e302d..fbd982eb3 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -10,7 +10,6 @@ import type { Packed } from '@/misc/json-schema.js'; import type { MiMeta } from '@/models/Meta.js'; import type { AdsRepository } from '@/models/_.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; @@ -24,11 +23,13 @@ export class MetaEntityService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.adsRepository) private adsRepository: AdsRepository, private userEntityService: UserEntityService, - private metaService: MetaService, private instanceActorService: InstanceActorService, ) { } @@ -37,7 +38,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const ads = await this.adsRepository.createQueryBuilder('ads') @@ -140,7 +141,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const packed = await this.pack(instance); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 0d0b80765..65a47d759 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; @@ -43,12 +43,14 @@ export class NoteEntityService implements OnModuleInit { private reactionService: ReactionService; private reactionsBufferingService: ReactionsBufferingService; private idService: IdService; - private metaService: MetaService; private noteLoader = new DebounceLoader(this.findNoteOrFail); constructor( private moduleRef: ModuleRef, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -76,7 +78,6 @@ export class NoteEntityService implements OnModuleInit { //private reactionService: ReactionService, //private reactionsBufferingService: ReactionsBufferingService, //private idService: IdService, - //private metaService: MetaService, ) { } @@ -87,7 +88,6 @@ export class NoteEntityService implements OnModuleInit { this.reactionService = this.moduleRef.get('ReactionService'); this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); this.idService = this.moduleRef.get('IdService'); - this.metaService = this.moduleRef.get('MetaService'); } @bindThis @@ -324,11 +324,9 @@ export class NoteEntityService implements OnModuleInit { const note = typeof src === 'object' ? src : await this.noteLoader.load(src); const host = note.userHost; - const meta = await this.metaService.fetch(); - const bufferdReactions = opts._hint_?.bufferdReactions != null ? (opts._hint_.bufferdReactions.get(note.id) ?? { deltas: {}, pairs: [] }) - : meta.enableReactionsBuffering + : this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.get(note.id) : { deltas: {}, pairs: [] }; const reactions = mergeReactions(note.reactions, bufferdReactions.deltas ?? {}); @@ -441,9 +439,7 @@ export class NoteEntityService implements OnModuleInit { ) { if (notes.length === 0) return []; - const meta = await this.metaService.fetch(); - - const bufferdReactions = meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; + const bufferdReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; const meId = me ? me.id : null; const myReactionsMap = new Map(); diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 2c70344c9..d229efb12 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; const ev = new Xev(); @@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown { private intervalId: NodeJS.Timeout | null = null; constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, ) { } @@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown { */ @bindThis public async start(): Promise { - if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; + if (!this.meta.enableServerMachineStats) return; const log = [] as any[]; diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index b6f003c2e..e599fc7b3 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -6,6 +6,7 @@ export const DI = { config: Symbol('config'), db: Symbol('db'), + meta: Symbol('meta'), meilisearch: Symbol('meilisearch'), redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts index cd56ba983..d49c99f69 100644 --- a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts +++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts @@ -7,17 +7,20 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; @Injectable() export class BakeBufferedReactionsProcessorService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + private reactionsBufferingService: ReactionsBufferingService, - private metaService: MetaService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions'); @@ -25,8 +28,7 @@ export class BakeBufferedReactionsProcessorService { @bindThis public async process(): Promise { - const meta = await this.metaService.fetch(); - if (!meta.enableReactionsBuffering) { + if (!this.meta.enableReactionsBuffering) { this.logger.info('Reactions buffering is disabled. Skipping...'); return; } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4076e9da9..fc9078251 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository, MiMeta } from '@/models/_.js'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -31,10 +30,12 @@ export class DeliverProcessorService { private latest: string | null; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private metaService: MetaService, private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -53,8 +54,7 @@ export class DeliverProcessorService { const { host } = new URL(job.data.to); // ブロックしてたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) { return 'skip (blocked)'; } @@ -88,7 +88,7 @@ export class DeliverProcessorService { this.apRequestChart.deliverSucc(); this.federationChart.deliverd(i.host, true); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, true); } }); @@ -120,7 +120,7 @@ export class DeliverProcessorService { this.apRequestChart.deliverFail(); this.federationChart.deliverd(i.host, false); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, false); } }); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index fa7009f8f..2df37bedf 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -4,11 +4,10 @@ */ import { URL } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; @@ -28,14 +27,18 @@ import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; +import { MiMeta } from '@/models/Meta.js'; +import { DI } from '@/di-symbols.js'; @Injectable() export class InboxProcessorService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + private utilityService: UtilityService, - private metaService: MetaService, private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -64,8 +67,7 @@ export class InboxProcessorService { const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); // ブロックしてたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { return `Blocked request: ${host}`; } @@ -166,7 +168,7 @@ export class InboxProcessorService { // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { @@ -197,7 +199,7 @@ export class InboxProcessorService { this.apRequestChart.inbox(); this.federationChart.inbox(i.host); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestReceived(i.host); } }); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 2d5535df0..fd2bd3267 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; -import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; @@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -44,6 +43,9 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -53,7 +55,6 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private apiServerService: ApiServerService, private openApiServerService: OpenApiServerService, @@ -193,7 +194,7 @@ export class ServerService implements OnApplicationShutdown { reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - if ((await this.metaService.fetch()).enableIdenticonGeneration) { + if (this.meta.enableIdenticonGeneration) { return await genIdenticon(request.params.x); } else { return reply.redirect('/static-assets/avatar.png'); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index e8d56ee50..aad833f12 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiAccessToken } from '@/models/AccessToken.js'; import type Logger from '@/logger.js'; -import type { UserIpsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta, UserIpsRepository } from '@/models/_.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; @@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown { private userIpHistoriesClearIntervalId: NodeJS.Timeout; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.config) private config: Config, @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, - private metaService: MetaService, private authenticateService: AuthenticateService, private rateLimiterService: RateLimiterService, private roleService: RoleService, @@ -265,9 +266,8 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: MiLocalUser) { - const meta = await this.metaService.fetch(); - if (!meta.enableIpLogging) return; + private logIp(request: FastifyRequest, user: MiLocalUser) { + if (!this.meta.enableIpLogging) return; const ip = request.ip; const ips = this.userIpHistories.get(user.id); if (ips == null || !ips.has(ip)) { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 632b0c62b..c49963801 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; @@ -28,6 +27,9 @@ export class SignupApiService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -45,7 +47,6 @@ export class SignupApiService { private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private captchaService: CaptchaService, private signupService: SignupService, private signinService: SigninService, @@ -72,31 +73,29 @@ export class SignupApiService { ) { const body = request.body; - const instance = await this.metaService.fetch(true); - // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { + if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableTurnstile && instance.turnstileSecretKey) { - await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => { + if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { throw new FastifyReplyError(400, err); }); } @@ -108,7 +107,7 @@ export class SignupApiService { const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; @@ -123,7 +122,7 @@ export class SignupApiService { let ticket: MiRegistrationTicket | null = null; - if (instance.disableRegistration) { + if (this.meta.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; @@ -144,7 +143,7 @@ export class SignupApiService { } // メアド認証が有効の場合 - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { // メアド認証済みならエラー if (ticket.usedBy) { reply.code(400); @@ -162,7 +161,7 @@ export class SignupApiService { } } - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } @@ -172,7 +171,7 @@ export class SignupApiService { throw new FastifyReplyError(400, 'USED_USERNAME'); } - const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index d3c40dba5..577ca0b24 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { MiNote } from '@/models/Note.js'; @@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/json-schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -20,6 +19,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -88,10 +89,12 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, - private metaService: MetaService, private apResolverService: ApResolverService, private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, @@ -112,9 +115,8 @@ export default class extends Endpoint { // eslint- */ @bindThis private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { - // ブロックしてたら中断 - const fetchedMeta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; + // ブロックしてたら中断 + if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 8c5567359..d4fd75e04 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -5,14 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; @@ -58,6 +56,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -68,16 +69,12 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -88,7 +85,7 @@ export default class extends Endpoint { // eslint- if (me) this.activeUsersChart.read(me); - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 7e9b0fa0e..eb45e29f9 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -41,14 +40,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const instance = await this.metaService.fetch(true); - - // Calculate drive usage const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); const policies = await this.roleService.getUserPolicies(me.id); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 9c17f93ab..74eb4dded 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -4,14 +4,15 @@ */ import ms from 'ms'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveService } from '@/core/DriveService.js'; import { ApiError } from '../../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -73,8 +74,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private driveFileEntityService: DriveFileEntityService, - private metaService: MetaService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { @@ -91,8 +94,6 @@ export default class extends Endpoint { // eslint- } } - const instance = await this.metaService.fetch(); - try { // Create file const driveFile = await this.driveService.addFile({ @@ -103,8 +104,8 @@ export default class extends Endpoint { // eslint- folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive, - requestIp: instance.enableIpLogging ? ip : null, - requestHeaders: instance.enableIpLogging ? headers : null, + requestIp: this.serverSettings.enableIpLogging ? ip : null, + requestHeaders: this.serverSettings.enableIpLogging ? headers : null, }); return await this.driveFileEntityService.pack(driveFile, { self: true }); } catch (err) { diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index eea657ebb..da1faee30 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; @@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -70,10 +69,12 @@ export default class extends Endpoint { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -105,7 +106,7 @@ export default class extends Endpoint { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } - } else if ((await this.metaService.fetch()).emailRequiredForSignup) { + } else if (this.serverSettings.emailRequiredForSignup) { throw new ApiError(meta.errors.emailRequired); } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index beb77ca7a..253a36081 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 2a2c65994..aed9065bf 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -74,6 +73,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -87,7 +89,6 @@ export default class extends Endpoint { // eslint- private cacheService: CacheService, private queryService: QueryService, private userFollowingService: UserFollowingService, - private metaService: MetaService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { @@ -101,9 +102,7 @@ export default class extends Endpoint { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -156,7 +155,7 @@ export default class extends Endpoint { // eslint- allowPartial: ps.allowPartial, me, redisTimelines: timelineConfig, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, noteFilter: note => { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index be82b5a8a..0b48f2c78 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,16 +5,14 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiMeta, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -66,6 +64,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -73,10 +74,8 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private cacheService: CacheService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -89,9 +88,7 @@ export default class extends Endpoint { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -115,7 +112,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c9b43b535..7cb11cc1e 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { @@ -56,6 +55,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -69,15 +71,12 @@ export default class extends Endpoint { // eslint- private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -108,7 +107,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 38a9660aa..e9a6a36b0 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -4,14 +4,15 @@ */ import { URLSearchParams } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -59,9 +60,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private noteEntityService: NoteEntityService, private getterService: GetterService, - private metaService: MetaService, private httpRequestService: HttpRequestService, private roleService: RoleService, ) { @@ -84,9 +87,7 @@ export default class extends Endpoint { // eslint- return; } - const instance = await this.metaService.fetch(); - - if (instance.deeplAuthKey == null) { + if (this.serverSettings.deeplAuthKey == null) { throw new ApiError(meta.errors.unavailable); } @@ -94,11 +95,11 @@ export default class extends Endpoint { // eslint- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); + params.append('auth_key', this.serverSettings.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); - const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const res = await this.httpRequestService.send(endpoint, { method: 'POST', diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 43877e61e..6c7185c9e 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,16 +5,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; -import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -69,6 +67,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -80,11 +81,9 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private activeUsersChart: ActiveUsersChart, - private cacheService: CacheService, private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -99,9 +98,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchList); } - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb(list, { untilId, sinceId, @@ -124,7 +121,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 15832ef7f..5b0b656c6 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -5,11 +5,10 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import * as Acct from '@/misc/acct.js'; import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,16 +37,16 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private metaService: MetaService, private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const meta = await this.metaService.fetch(); - - const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ + const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ usernameLower: acct.username.toLowerCase(), host: acct.host ?? IsNull(), }))); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c13802eb0..8301c85f2 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -5,9 +5,10 @@ import * as os from 'node:os'; import si from 'systeminformation'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: false, @@ -73,10 +74,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private metaService: MetaService, + @Inject(DI.meta) + private serverSettings: MiMeta, ) { super(meta, paramDef, async () => { - if (!(await this.metaService.fetch()).enableServerMachineStats) return { + if (!this.serverSettings.enableServerMachineStats) return { machine: '?', cpu: { model: '?', diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index a9a33149f..fd76df2d3 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -5,9 +5,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; -import type { SwSubscriptionsRepository } from '@/models/_.js'; +import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; @@ -62,11 +61,13 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, private idService: IdService, - private metaService: MetaService, private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { @@ -78,12 +79,10 @@ export default class extends Endpoint { // eslint- publickey: ps.publickey, }); - const instance = await this.metaService.fetch(true); - if (exist != null) { return { state: 'already-subscribed' as const, - key: instance.swPublicKey, + key: this.serverSettings.swPublicKey, userId: me.id, endpoint: exist.endpoint, sendReadMessage: exist.sendReadMessage, @@ -103,7 +102,7 @@ export default class extends Endpoint { // eslint- return { state: 'subscribed' as const, - key: instance.swPublicKey, + key: this.serverSettings.swPublicKey, userId: me.id, endpoint: ps.endpoint, sendReadMessage: ps.sendReadMessage, diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index affb0996f..4944be9b0 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -5,11 +5,10 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { localUsernameSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['users'], @@ -39,13 +38,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.usedUsernamesRepository) private usedUsernamesRepository: UsedUsernamesRepository, - - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const exist = await this.usersRepository.countBy({ @@ -55,8 +55,7 @@ export default class extends Endpoint { // eslint- const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); - const meta = await this.metaService.fetch(); - const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); + const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); return { available: exist === 0 && exist2 === 0 && !isPreserved, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index cc76c12f1..7fc11ba36 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,14 +5,13 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiMeta, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; @@ -67,6 +66,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -75,15 +77,12 @@ export default class extends Endpoint { // eslint- private cacheService: CacheService, private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const isSelf = me && (me.id === ps.userId); - const serverSettings = await this.metaService.fetch(); - if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); // early return if me is blocked by requesting user @@ -94,7 +93,7 @@ export default class extends Endpoint { // eslint- } } - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 1dc53a9a7..063141273 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -24,7 +24,6 @@ import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; -import { MetaService } from '@/core/MetaService.js'; import type { DbQueue, DeliverQueue, @@ -73,6 +72,9 @@ export class ClientServerService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -109,7 +111,6 @@ export class ClientServerService { private clipEntityService: ClipEntityService, private channelEntityService: ChannelEntityService, private reversiGameEntityService: ReversiGameEntityService, - private metaService: MetaService, private urlPreviewService: UrlPreviewService, private feedService: FeedService, private roleService: RoleService, @@ -129,32 +130,30 @@ export class ClientServerService { @bindThis private async manifestHandler(reply: FastifyReply) { - const instance = await this.metaService.fetch(true); - let manifest = { // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'short_name': instance.shortName || instance.name || this.config.host, + 'short_name': this.meta.shortName || this.meta.name || this.config.host, // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'name': instance.name || this.config.host, + 'name': this.meta.name || this.config.host, 'start_url': '/', 'display': 'standalone', 'background_color': '#313a42', // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'theme_color': instance.themeColor || '#86b300', + 'theme_color': this.meta.themeColor || '#86b300', 'icons': [{ // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': instance.app192IconUrl || '/static-assets/icons/192.png', + 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png', 'sizes': '192x192', 'type': 'image/png', 'purpose': 'maskable', }, { // 空文字列の場合右辺を使いたいため // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': instance.app512IconUrl || '/static-assets/icons/512.png', + 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png', 'sizes': '512x512', 'type': 'image/png', 'purpose': 'maskable', @@ -178,7 +177,7 @@ export class ClientServerService { manifest = { ...manifest, - ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride), + ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride), }; reply.header('Cache-Control', 'max-age=300'); @@ -453,9 +452,7 @@ export class ClientServerService { // OpenSearch XML fastify.get('/opensearch.xml', async (request, reply) => { - const meta = await this.metaService.fetch(); - - const name = meta.name ?? 'Misskey'; + const name = this.meta.name ?? 'Misskey'; let content = ''; content += ''; content += `${name}`; @@ -472,14 +469,13 @@ export class ClientServerService { //#endregion const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { - img: meta.bannerUrl, + img: this.meta.bannerUrl, url: this.config.url, - title: meta.name ?? 'Misskey', - desc: meta.description, - ...await this.generateCommonPugData(meta), + title: this.meta.name ?? 'Misskey', + desc: this.meta.description, + ...await this.generateCommonPugData(this.meta), ...data, }); }; @@ -557,7 +553,6 @@ export class ClientServerService { if (user != null) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const meta = await this.metaService.fetch(); const me = profile.fields ? profile.fields .filter(filed => filed.value != null && filed.value.match(/^https?:/)) @@ -573,7 +568,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { // リモートユーザーなので @@ -611,7 +606,6 @@ export class ClientServerService { if (note) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -623,7 +617,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -648,7 +642,6 @@ export class ClientServerService { if (page) { const _page = await this.pageEntityService.pack(page); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); - const meta = await this.metaService.fetch(); if (['public'].includes(page.visibility)) { reply.header('Cache-Control', 'public, max-age=15'); } else { @@ -662,7 +655,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -678,7 +671,6 @@ export class ClientServerService { if (flash) { const _flash = await this.flashEntityService.pack(flash); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -688,7 +680,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -704,7 +696,6 @@ export class ClientServerService { if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -714,7 +705,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -728,7 +719,6 @@ export class ClientServerService { if (post) { const _post = await this.galleryPostEntityService.pack(post); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -738,7 +728,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -753,11 +743,10 @@ export class ClientServerService { if (channel) { const _channel = await this.channelEntityService.pack(channel); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -772,11 +761,10 @@ export class ClientServerService { if (game) { const _game = await this.reversiGameEntityService.packDetail(game); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=3600'); return await reply.view('reversi-game', { game: _game, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -798,26 +786,22 @@ export class ClientServerService { //#region embed pages fastify.get('/embed/*', async (request, reply) => { - const meta = await this.metaService.fetch(); - reply.removeHeader('X-Frame-Options'); reply.header('Cache-Control', 'public, max-age=3600'); return await reply.view('base-embed', { - title: meta.name ?? 'Misskey', - ...await this.generateCommonPugData(meta), + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), }); }); fastify.get('/_info_card_', async (request, reply) => { - const meta = await this.metaService.fetch(true); - reply.removeHeader('X-Frame-Options'); return await reply.view('info-card', { version: this.config.version, host: this.config.host, - meta: meta, + meta: this.meta, originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), }); diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 8f8f08a30..5d493c2c4 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; @@ -26,7 +25,9 @@ export class UrlPreviewService { @Inject(DI.config) private config: Config, - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { @@ -62,9 +63,7 @@ export class UrlPreviewService { return; } - const meta = await this.metaService.fetch(); - - if (!meta.urlPreviewEnabled) { + if (!this.meta.urlPreviewEnabled) { reply.code(403); return { error: new ApiError({ @@ -75,14 +74,14 @@ export class UrlPreviewService { }; } - this.logger.info(meta.urlPreviewSummaryProxyUrl + this.logger.info(this.meta.urlPreviewSummaryProxyUrl ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); try { - const summary = meta.urlPreviewSummaryProxyUrl - ? await this.fetchSummaryFromProxy(url, meta, lang) - : await this.fetchSummary(url, meta, lang); + const summary = this.meta.urlPreviewSummaryProxyUrl + ? await this.fetchSummaryFromProxy(url, this.meta, lang) + : await this.fetchSummary(url, this.meta, lang); this.logger.succ(`Got preview of ${url}: ${summary.title}`); diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts index 866a7e1f5..04bf62d20 100644 --- a/packages/backend/test-server/entry.ts +++ b/packages/backend/test-server/entry.ts @@ -6,12 +6,16 @@ import { MainModule } from '@/MainModule.js'; import { ServerService } from '@/server/ServerService.js'; import { loadConfig } from '@/config.js'; import { NestLogger } from '@/NestLogger.js'; +import { INestApplicationContext } from '@nestjs/common'; const config = loadConfig(); const originEnv = JSON.stringify(process.env); process.env.NODE_ENV = 'test'; +let app: INestApplicationContext; +let serverService: ServerService; + /** * テスト用のサーバインスタンスを起動する */ @@ -20,10 +24,10 @@ async function launch() { console.log('starting application...'); - const app = await NestFactory.createApplicationContext(MainModule, { + app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); - const serverService = app.get(ServerService); + serverService = app.get(ServerService); await serverService.launch(); await startControllerEndpoints(); @@ -71,6 +75,20 @@ async function startControllerEndpoints(port = config.port + 1000) { fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { process.env = JSON.parse(originEnv); + + await serverService.dispose(); + await app.close(); + + await killTestServer(); + + console.log('starting application...'); + + app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + serverService = app.get(ServerService); + await serverService.launch(); + res.code(200).send({ success: true }); }); diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts index 861bc6db6..7c6dd6a55 100644 --- a/packages/backend/test/jest.setup.ts +++ b/packages/backend/test/jest.setup.ts @@ -6,8 +6,6 @@ import { initTestDb, sendEnvResetRequest } from './utils.js'; beforeAll(async () => { - await Promise.all([ - initTestDb(false), - sendEnvResetRequest(), - ]); + await initTestDb(false); + await sendEnvResetRequest(); }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 3c7e79670..c8f3db8aa 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -17,6 +17,7 @@ import type { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { FollowRequestsRepository, + MiMeta, NoteReactionsRepository, NotesRepository, PollsRepository, @@ -35,6 +36,7 @@ export class MockResolver extends Resolver { constructor(loggerService: LoggerService) { super( {} as Config, + {} as MiMeta, {} as UsersRepository, {} as NotesRepository, {} as PollsRepository, @@ -42,7 +44,6 @@ export class MockResolver extends Resolver { {} as FollowRequestsRepository, {} as UtilityService, {} as InstanceActorService, - {} as MetaService, {} as ApRequestService, {} as HttpRequestService, {} as ApRendererService, diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index b6cbe4c52..ef80d25f8 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -13,6 +13,7 @@ import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import { + MiMeta, MiRole, MiRoleAssignment, MiUser, @@ -41,7 +42,7 @@ describe('RoleService', () => { let usersRepository: UsersRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; - let metaService: jest.Mocked; + let meta: jest.Mocked; let notificationService: jest.Mocked; let clock: lolex.InstalledClock; @@ -142,7 +143,7 @@ describe('RoleService', () => { rolesRepository = app.get(DI.rolesRepository); roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); - metaService = app.get(MetaService) as jest.Mocked; + meta = app.get(DI.meta) as jest.Mocked; notificationService = app.get(NotificationService) as jest.Mocked; await roleService.onModuleInit(); @@ -164,11 +165,9 @@ describe('RoleService', () => { describe('getUserPolicies', () => { test('instance default policies', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -177,11 +176,9 @@ describe('RoleService', () => { test('instance default policies 2', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: true, - }, - } as any); + meta.policies = { + canManageCustomEmojis: true, + }; const result = await roleService.getUserPolicies(user.id); @@ -201,11 +198,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -236,11 +231,9 @@ describe('RoleService', () => { }); await roleService.assign(user.id, role1.id); await roleService.assign(user.id, role2.id); - metaService.fetch.mockResolvedValue({ - policies: { - driveCapacityMb: 50, - }, - } as any); + meta.policies = { + driveCapacityMb: 50, + }; const result = await roleService.getUserPolicies(user.id); @@ -260,11 +253,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 763ce2b33..2fc08aec9 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -24,7 +24,6 @@ import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { MetaService } from '@/core/MetaService.js'; import type { MiRemoteUser } from '@/models/User.js'; import { genAidx } from '@/misc/id/aidx.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -107,7 +106,14 @@ describe('ActivityPub', () => { sensitiveWords: [] as string[], prohibitedWords: [] as string[], } as MiMeta; - let meta = metaInitial; + const meta = { ...metaInitial }; + + function updateMeta(newMeta: Partial): void { + for (const key in meta) { + delete (meta as any)[key]; + } + Object.assign(meta, newMeta); + } beforeAll(async () => { const app = await Test.createTestingModule({ @@ -120,11 +126,8 @@ describe('ActivityPub', () => { }; }, }) - .overrideProvider(MetaService).useValue({ - async fetch(): Promise { - return meta; - }, - }).compile(); + .overrideProvider(DI.meta).useFactory({ factory: () => meta }) + .compile(); await app.init(); app.enableShutdownHooks(); @@ -367,7 +370,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteFiles=false disables caching', async () => { - meta = { ...metaInitial, cacheRemoteFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteFiles: false }); const imageObject: IApDocument = { type: 'Document', @@ -396,7 +399,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { - meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false }); const imageObject: IApDocument = { type: 'Document', From 891bbcf47597fb621917721f9b7e59c7fac9c9ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Sep 2024 03:56:51 +0000 Subject: [PATCH 072/159] Bump version to 2024.9.0-alpha.3 --- 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 68bb2fc1f..c8285c0d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.2", + "version": "2024.9.0-alpha.3", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 13bb114fb..be5f9a490 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.9.0-alpha.2", + "version": "2024.9.0-alpha.3", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 3df1bb2d71bfed3cc49f6576d5809ecfdbdb6fe1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:01:13 +0900 Subject: [PATCH 073/159] enhance(frontend): tweak control panel --- packages/frontend/src/pages/admin/index.vue | 5 - .../src/pages/admin/instance-block.vue | 84 -------- .../frontend/src/pages/admin/moderation.vue | 202 +++++++++++++----- .../frontend/src/pages/admin/settings.vue | 25 +++ packages/frontend/src/router/definition.ts | 4 - 5 files changed, 175 insertions(+), 145 deletions(-) delete mode 100644 packages/frontend/src/pages/admin/instance-block.vue diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 40dec55de..cd1dd2ca9 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -199,11 +199,6 @@ const menuDef = computed(() => [{ text: i18n.ts.relays, to: '/admin/relays', active: currentPage.value?.route.name === 'relays', - }, { - icon: 'ti ti-ban', - text: i18n.ts.instanceBlocking, - to: '/admin/instance-block', - active: currentPage.value?.route.name === 'instance-block', }, { icon: 'ti ti-ghost', text: i18n.ts.proxyAccount, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue deleted file mode 100644 index e090616b2..000000000 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index a75799696..82e053c2d 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -10,61 +10,102 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + {{ i18n.ts.serverRules }} - - - - - - - - - - - - - - - - - + + - - - +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
- +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
- +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
+ +
+ + + + {{ i18n.ts.save }} +
+ + + + + + +
+ + + + {{ i18n.ts.save }} +
+
+ + + + + +
+ + + + {{ i18n.ts.save }} +
+
+ + + + + +
+ + + + {{ i18n.ts.save }} +
+
-
@@ -83,6 +124,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; +import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); @@ -90,9 +132,9 @@ const sensitiveWords = ref(''); const prohibitedWords = ref(''); const hiddenTags = ref(''); const preservedUsernames = ref(''); -const tosUrl = ref(null); -const privacyPolicyUrl = ref(null); -const inquiryUrl = ref(null); +const blockedHosts = ref(''); +const silencedHosts = ref(''); +const mediaSilencedHosts = ref(''); async function init() { const meta = await misskeyApi('admin/meta'); @@ -102,27 +144,83 @@ async function init() { prohibitedWords.value = meta.prohibitedWords.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); - tosUrl.value = meta.tosUrl; - privacyPolicyUrl.value = meta.privacyPolicyUrl; - inquiryUrl.value = meta.inquiryUrl; + blockedHosts.value = meta.blockedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts.join('\n'); + mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); } -function save() { +function onChange_enableRegistration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disableRegistration: !value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_emailRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + emailRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_preservedUsernames() { os.apiWithDialog('admin/update-meta', { - disableRegistration: !enableRegistration.value, - emailRequiredForSignup: emailRequiredForSignup.value, - tosUrl: tosUrl.value, - privacyPolicyUrl: privacyPolicyUrl.value, - inquiryUrl: inquiryUrl.value, - sensitiveWords: sensitiveWords.value.split('\n'), - prohibitedWords: prohibitedWords.value.split('\n'), - hiddenTags: hiddenTags.value.split('\n'), preservedUsernames: preservedUsernames.value.split('\n'), }).then(() => { fetchInstance(true); }); } +function save_sensitiveWords() { + os.apiWithDialog('admin/update-meta', { + sensitiveWords: sensitiveWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_prohibitedWords() { + os.apiWithDialog('admin/update-meta', { + prohibitedWords: prohibitedWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_hiddenTags() { + os.apiWithDialog('admin/update-meta', { + hiddenTags: hiddenTags.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_blockedHosts() { + os.apiWithDialog('admin/update-meta', { + blockedHosts: blockedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_silencedHosts() { + os.apiWithDialog('admin/update-meta', { + silencedHosts: silencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_mediaSilencedHosts() { + os.apiWithDialog('admin/update-meta', { + mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + const headerTabs = computed(() => []); definePageMetadata(() => ({ diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index ffff57b45..6eaafed6d 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -34,6 +34,22 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + + + + + + + @@ -196,6 +212,9 @@ const shortName = ref(null); const description = ref(null); const maintainerName = ref(null); const maintainerEmail = ref(null); +const tosUrl = ref(null); +const privacyPolicyUrl = ref(null); +const inquiryUrl = ref(null); const repositoryUrl = ref(null); const impressumUrl = ref(null); const pinnedUsers = ref(''); @@ -219,6 +238,9 @@ async function init(): Promise { description.value = meta.description; maintainerName.value = meta.maintainerName; maintainerEmail.value = meta.maintainerEmail; + tosUrl.value = meta.tosUrl; + privacyPolicyUrl.value = meta.privacyPolicyUrl; + inquiryUrl.value = meta.inquiryUrl; repositoryUrl.value = meta.repositoryUrl; impressumUrl.value = meta.impressumUrl; pinnedUsers.value = meta.pinnedUsers.join('\n'); @@ -243,6 +265,9 @@ async function save() { description: description.value, maintainerName: maintainerName.value, maintainerEmail: maintainerEmail.value, + tosUrl: tosUrl.value, + privacyPolicyUrl: privacyPolicyUrl.value, + inquiryUrl: inquiryUrl.value, repositoryUrl: repositoryUrl.value, impressumUrl: impressumUrl.value, pinnedUsers: pinnedUsers.value.split('\n'), diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 8a29fd677..bcd1d9a15 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -462,10 +462,6 @@ const routes: RouteDef[] = [{ path: '/relays', name: 'relays', component: page(() => import('@/pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('@/pages/admin/instance-block.vue')), }, { path: '/proxy-account', name: 'proxy-account', From 8ad9f7209b8b1a4584428118dda98339d575a0d6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:16:50 +0900 Subject: [PATCH 074/159] enhance(frontend): tweak control panel --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + packages/frontend/src/pages/admin/index.vue | 8 +- .../{other-settings.vue => performance.vue} | 125 +++++++++++++----- packages/frontend/src/router/definition.ts | 6 +- 5 files changed, 107 insertions(+), 37 deletions(-) rename packages/frontend/src/pages/admin/{other-settings.vue => performance.vue} (66%) diff --git a/locales/index.d.ts b/locales/index.d.ts index f23426219..55e76e2e4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5092,6 +5092,10 @@ export interface Locale extends ILocale { * これ以上このクリップにノートを追加できません。 */ "clipNoteLimitExceeded": string; + /** + * パフォーマンス + */ + "performance": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8e48508e7..995bf8bc7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1269,6 +1269,7 @@ fromX: "{x}から" genEmbedCode: "埋め込みコードを生成" noteOfThisUser: "このユーザーのノート一覧" clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" +performance: "パフォーマンス" _delivery: status: "配信状態" diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index cd1dd2ca9..b9f72e6fb 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -215,10 +215,10 @@ const menuDef = computed(() => [{ to: '/admin/system-webhook', active: currentPage.value?.route.name === 'system-webhook', }, { - icon: 'ti ti-adjustments', - text: i18n.ts.other, - to: '/admin/other-settings', - active: currentPage.value?.route.name === 'other-settings', + icon: 'ti ti-bolt', + text: i18n.ts.performance, + to: '/admin/performance', + active: currentPage.value?.route.name === 'performance', }], }, { title: i18n.ts.info, diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/performance.vue similarity index 66% rename from packages/frontend/src/pages/admin/other-settings.vue rename to packages/frontend/src/pages/admin/performance.vue index cad111997..721a11e60 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -10,28 +10,28 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- +
- +
- + @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - + - + - +
@@ -77,12 +77,12 @@ SPDX-License-Identifier: AGPL-3.0-only - +
- + @@ -135,30 +135,95 @@ async function init() { enableReactionsBuffering.value = meta.enableReactionsBuffering; } -function save() { +function onChange_enableServerMachineStats(value: boolean) { os.apiWithDialog('admin/update-meta', { - enableServerMachineStats: enableServerMachineStats.value, - enableIdenticonGeneration: enableIdenticonGeneration.value, - enableChartsForRemoteUser: enableChartsForRemoteUser.value, - enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, - enableFanoutTimeline: enableFanoutTimeline.value, - enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value, - perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, - perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, - perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, - perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, - enableReactionsBuffering: enableReactionsBuffering.value, + enableServerMachineStats: value, }).then(() => { fetchInstance(true); }); } -const headerActions = computed(() => [{ - asFullButton: true, - icon: 'ti ti-check', - text: i18n.ts.save, - handler: save, -}]); +function onChange_enableIdenticonGeneration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableIdenticonGeneration: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForRemoteUser(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForRemoteUser: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForFederatedInstances(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForFederatedInstances: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableFanoutTimeline(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableFanoutTimeline: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableFanoutTimelineDbFallback(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableFanoutTimelineDbFallback: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perLocalUserUserTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perRemoteUserUserTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perUserHomeTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perUserListTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableReactionsBuffering(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableReactionsBuffering: value, + }).then(() => { + fetchInstance(true); + }); +} + +const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index bcd1d9a15..fa19e6cd9 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -471,9 +471,9 @@ const routes: RouteDef[] = [{ name: 'external-services', component: page(() => import('@/pages/admin/external-services.vue')), }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('@/pages/admin/other-settings.vue')), + path: '/performance', + name: 'performance', + component: page(() => import('@/pages/admin/performance.vue')), }, { path: '/server-rules', name: 'server-rules', From 0e92cbf9052898d17c6e5dec8027203c62dde687 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:24:53 +0900 Subject: [PATCH 075/159] enhance(frontend): tweak control panel --- .../src/pages/admin/external-services.vue | 23 ++++--------------- .../frontend/src/pages/admin/moderation.vue | 7 ------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index e0b82eb02..91f41166e 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -19,17 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only + Save
-
+
- @@ -40,12 +34,12 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref(''); const deeplIsPro = ref(false); @@ -56,7 +50,7 @@ async function init() { deeplIsPro.value = meta.deeplIsPro; } -function save() { +function save_deepl() { os.apiWithDialog('admin/update-meta', { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, @@ -74,10 +68,3 @@ definePageMetadata(() => ({ icon: 'ti ti-link', })); - - diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 82e053c2d..54eb95cd5 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -228,10 +228,3 @@ definePageMetadata(() => ({ icon: 'ti ti-shield', })); - - From 01ec7080205fadb80e658e2e2bb87f3aa14c796b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:50:54 +0900 Subject: [PATCH 076/159] ffix(frontend): lint fixes for tweak control panel (#14607) --- packages/frontend/src/pages/admin/performance.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 721a11e60..1e6135811 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -57,19 +57,19 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - +
From d435d04eaf992f994ff4e690a658207757c8bdf3 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:26:21 +0900 Subject: [PATCH 077/159] enhance(frontend): tweak control panel --- packages/frontend/src/components/MkFolder.vue | 18 ++ .../frontend/src/pages/admin/settings.vue | 210 +++++++++++------- 2 files changed, 151 insertions(+), 77 deletions(-) diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index f805be7b5..79676e835 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ +
@@ -56,9 +59,11 @@ import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ defaultOpen?: boolean; maxHeight?: number | null; + withFooter?: boolean; }>(), { defaultOpen: false, maxHeight: null, + withFooter: false }); const getBgColor = (el: HTMLElement) => { @@ -224,4 +229,17 @@ onMounted(() => { background: var(--bg); } } + +.footer { + position: sticky !important; + z-index: 1; + bottom: var(--stickyBottom, 0px); + left: 0; + padding: 9px 12px; + border-top: solid 0.5px var(--divider); + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + border-radius: 0 0 6px 6px; +} diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 6eaafed6d..1e9682775 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -10,71 +10,93 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - + + + + - - - - +
+ + + - - - + + + + - - - - + + + - - - - - + + + + - - - - + + + + + - - - - + + + + - - - - - + + + + - - - - - + + + + + - - {{ i18n.ts.repositoryUrlOrTarballRequired }} - + + + + + - - - - - + + {{ i18n.ts.repositoryUrlOrTarballRequired }} + - + + + + + +
+
+ + + - - + - + + + + + + + + + -
+
@@ -87,12 +109,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -110,12 +136,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -126,12 +156,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -173,17 +207,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
@@ -195,7 +222,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; -import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; @@ -258,8 +284,8 @@ async function init(): Promise { urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl; } -async function save() { - await os.apiWithDialog('admin/update-meta', { +function saveInfo() { + os.apiWithDialog('admin/update-meta', { name: name.value, shortName: shortName.value === '' ? null : shortName.value, description: description.value, @@ -270,22 +296,57 @@ async function save() { inquiryUrl: inquiryUrl.value, repositoryUrl: repositoryUrl.value, impressumUrl: impressumUrl.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_pinnedUsers() { + os.apiWithDialog('admin/update-meta', { pinnedUsers: pinnedUsers.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function saveFiles() { + os.apiWithDialog('admin/update-meta', { cacheRemoteFiles: cacheRemoteFiles.value, cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveServiceWorker() { + os.apiWithDialog('admin/update-meta', { enableServiceWorker: enableServiceWorker.value, swPublicKey: swPublicKey.value, swPrivateKey: swPrivateKey.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveAd() { + os.apiWithDialog('admin/update-meta', { notesPerOneAd: notesPerOneAd.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveUrlPreview() { + os.apiWithDialog('admin/update-meta', { urlPreviewEnabled: urlPreviewEnabled.value, urlPreviewTimeout: urlPreviewTimeout.value, urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value, urlPreviewRequireContentLength: urlPreviewRequireContentLength.value, urlPreviewUserAgent: urlPreviewUserAgent.value, urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value, + }).then(() => { + fetchInstance(true); }); - - fetchInstance(true); } const headerTabs = computed(() => []); @@ -297,11 +358,6 @@ definePageMetadata(() => ({ diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue new file mode 100644 index 000000000..1e88d59d8 --- /dev/null +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -0,0 +1,49 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 0f4d94aa4..57f68a2a2 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -7,103 +7,100 @@ SPDX-License-Identifier: AGPL-3.0-only - -
-
- - - - -
- -
- - - - -
- -
- - - - -
- -
- - - - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - -
- - - - -
-
+
+
+ + + +
- + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+
+
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 9bccee89a..975a4a126 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -7,119 +7,115 @@ SPDX-License-Identifier: AGPL-3.0-only - -
- - - - - - - - +
+ - - + + + + + + + + - - - - - - - +
+ {{ i18n.ts._sensitiveMediaDetection.description }} -
- {{ i18n.ts._sensitiveMediaDetection.description }} + + + + + + - - - - - - + + + + - - - - + + + + - - - - + + + + - - - - + +
+ - + + + + + - {{ i18n.ts.save }} -
-
+
+ {{ i18n.ts.activeEmailValidationDescription }} + + + + + + + + + + + + + + + + + + + + + +
+
- - - - + + + -
- {{ i18n.ts.activeEmailValidationDescription }} - - - - - - - - - - - - - - - - - - - - - - {{ i18n.ts.save }} -
-
+
+ + + +
+
- - + + + + + -
- - - - {{ i18n.ts.save }} -
-
- - - - - - -
- - - -
-
-
- +
+ + + +
+
+
@@ -131,83 +127,80 @@ import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import MkRange from '@/components/MkRange.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; -const enableHcaptcha = ref(false); -const enableMcaptcha = ref(false); -const enableRecaptcha = ref(false); -const enableTurnstile = ref(false); -const sensitiveMediaDetection = ref('none'); -const sensitiveMediaDetectionSensitivity = ref(0); -const setSensitiveFlagAutomatically = ref(false); -const enableSensitiveMediaDetectionForVideos = ref(false); -const enableIpLogging = ref(false); -const enableActiveEmailValidation = ref(false); -const enableVerifymailApi = ref(false); -const verifymailAuthKey = ref(null); -const enableTruemailApi = ref(false); -const truemailInstance = ref(null); -const truemailAuthKey = ref(null); -const bannedEmailDomains = ref(''); +const meta = await misskeyApi('admin/meta'); -async function init() { - const meta = await misskeyApi('admin/meta'); - enableHcaptcha.value = meta.enableHcaptcha; - enableMcaptcha.value = meta.enableMcaptcha; - enableRecaptcha.value = meta.enableRecaptcha; - enableTurnstile.value = meta.enableTurnstile; - sensitiveMediaDetection.value = meta.sensitiveMediaDetection; - sensitiveMediaDetectionSensitivity.value = - meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : - meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : - meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : - meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : - meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0; - setSensitiveFlagAutomatically.value = meta.setSensitiveFlagAutomatically; - enableSensitiveMediaDetectionForVideos.value = meta.enableSensitiveMediaDetectionForVideos; - enableIpLogging.value = meta.enableIpLogging; - enableActiveEmailValidation.value = meta.enableActiveEmailValidation; - enableVerifymailApi.value = meta.enableVerifymailApi; - verifymailAuthKey.value = meta.verifymailAuthKey; - enableTruemailApi.value = meta.enableTruemailApi; - truemailInstance.value = meta.truemailInstance; - truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - sensitiveMediaDetection: sensitiveMediaDetection.value, +const sensitiveMediaDetectionForm = useForm({ + sensitiveMediaDetection: meta.sensitiveMediaDetection, + sensitiveMediaDetectionSensitivity: meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : + meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : + meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : + meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : + meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0, + setSensitiveFlagAutomatically: meta.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: meta.enableSensitiveMediaDetectionForVideos, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + sensitiveMediaDetection: state.sensitiveMediaDetection, sensitiveMediaDetectionSensitivity: - sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' : - sensitiveMediaDetectionSensitivity.value === 1 ? 'low' : - sensitiveMediaDetectionSensitivity.value === 2 ? 'medium' : - sensitiveMediaDetectionSensitivity.value === 3 ? 'high' : - sensitiveMediaDetectionSensitivity.value === 4 ? 'veryHigh' : + state.sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' : + state.sensitiveMediaDetectionSensitivity === 1 ? 'low' : + state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' : + state.sensitiveMediaDetectionSensitivity === 3 ? 'high' : + state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' : 0, - setSensitiveFlagAutomatically: setSensitiveFlagAutomatically.value, - enableSensitiveMediaDetectionForVideos: enableSensitiveMediaDetectionForVideos.value, - enableIpLogging: enableIpLogging.value, - enableActiveEmailValidation: enableActiveEmailValidation.value, - enableVerifymailApi: enableVerifymailApi.value, - verifymailAuthKey: verifymailAuthKey.value, - enableTruemailApi: enableTruemailApi.value, - truemailInstance: truemailInstance.value, - truemailAuthKey: truemailAuthKey.value, - bannedEmailDomains: bannedEmailDomains.value.split('\n'), - }).then(() => { - fetchInstance(true); + setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos, }); -} + fetchInstance(true); +}); + +const ipLoggingForm = useForm({ + enableIpLogging: meta.enableIpLogging, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableIpLogging: state.enableIpLogging, + }); + fetchInstance(true); +}); + +const emailValidationForm = useForm({ + enableActiveEmailValidation: meta.enableActiveEmailValidation, + enableVerifymailApi: meta.enableVerifymailApi, + verifymailAuthKey: meta.verifymailAuthKey, + enableTruemailApi: meta.enableTruemailApi, + truemailInstance: meta.truemailInstance, + truemailAuthKey: meta.truemailAuthKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableActiveEmailValidation: state.enableActiveEmailValidation, + enableVerifymailApi: state.enableVerifymailApi, + verifymailAuthKey: state.verifymailAuthKey, + enableTruemailApi: state.enableTruemailApi, + truemailInstance: state.truemailInstance, + truemailAuthKey: state.truemailAuthKey, + }); + fetchInstance(true); +}); + +const bannedEmailDomainsForm = useForm({ + bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + bannedEmailDomains: state.bannedEmailDomains.split('\n'), + }); + fetchInstance(true); +}); const headerActions = computed(() => []); From 3f0aaaa41efe42776d70490ea213e3c8b194c152 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:49:52 +0900 Subject: [PATCH 094/159] perf(embed): improve embed performance (#14613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * refactor * refactor --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- .../src/server/web/ClientServerService.ts | 66 +++++++++++++++++++ .../src/server/web/views/base-embed.pug | 3 + packages/frontend-embed/src/boot.ts | 10 ++- .../src/components/EmNoteDetailed.vue | 4 +- .../frontend-embed/src/components/EmNotes.vue | 6 +- packages/frontend-embed/src/di.ts | 2 + packages/frontend-embed/src/pages/clip.vue | 44 +++++++------ packages/frontend-embed/src/pages/note.vue | 35 +++++----- packages/frontend-embed/src/pages/tag.vue | 7 +- .../src/pages/user-timeline.vue | 49 ++++++++------ packages/frontend-embed/src/server-context.ts | 21 ++++++ packages/frontend-embed/src/ui.vue | 16 +++-- 12 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 packages/frontend-embed/src/server-context.ts diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 063141273..5de1f8766 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -785,6 +785,72 @@ export class ClientServerService { //#endregion //#region embed pages + fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const user = await this.usersRepository.findOneBy({ + id: request.params.user, + }); + + if (user == null) return; + if (user.host != null) return; + + const _user = await this.userEntityService.pack(user); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + user: _user, + }), + }); + }); + + fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const note = await this.notesRepository.findOneBy({ + id: request.params.note, + }); + + if (note == null) return; + if (note.visibility !== 'public') return; + if (note.userHost != null) return; + + const _note = await this.noteEntityService.pack(note, null, { detail: true }); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + note: _note, + }), + }); + }); + + fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const clip = await this.clipsRepository.findOneBy({ + id: request.params.clip, + }); + + if (clip == null) return; + + const _clip = await this.clipEntityService.pack(clip); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + clip: _clip, + }), + }); + }); + fastify.get('/embed/*', async (request, reply) => { reply.removeHeader('X-Frame-Options'); diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug index d773f2676..2bab20a36 100644 --- a/packages/backend/src/server/web/views/base-embed.pug +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -43,6 +43,9 @@ html(class='embed') script(type='application/json' id='misskey_meta' data-generated-at=now) != metaJson + script(type='application/json' id='misskey_embedCtx' data-generated-at=now) + != embedCtx + script include ../boot.embed.js diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index fcea7d32e..00c7944eb 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -20,16 +20,19 @@ import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; +import { serverContext } from '@/server-context.js'; import type { Theme } from '@/theme.js'; console.log('Misskey Embed'); +//#region Embedパラメータの取得・パース const params = new URLSearchParams(location.search); const embedParams = parseEmbedParams(params); - if (_DEV_) console.log(embedParams); +//#endregion +//#region テーマ function parseThemeOrNull(theme: string | null): Theme | null { if (theme == null) return null; try { @@ -65,6 +68,7 @@ if (embedParams.colorMode === 'dark') { } }); } +//#endregion // サイズの制限 document.documentElement.style.maxWidth = '500px'; @@ -89,6 +93,10 @@ const app = createApp( app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url)); +app.provide(DI.serverMetadata, serverMetadata); + +app.provide(DI.serverContext, serverContext); + app.provide(DI.embedParams, embedParams); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue index 8169f500a..a233011af 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -142,8 +142,8 @@ import EmAcct from '@/components/EmAcct.vue'; import { userPage } from '@/utils.js'; import { notePage } from '@/utils.js'; import { i18n } from '@/i18n.js'; +import { DI } from '@/di.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; -import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import EmMfm from '@/components/EmMfm.js'; @@ -151,6 +151,8 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); +const serverMetadata = inject(DI.serverMetadata)!; + const inChannel = inject('inChannel', null); const note = ref(props.note); diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue index 6370f4aea..3418d97f7 100644 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only From 1b2b95e199938d30be546c1afa1088eecdc1097c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Sep 2024 01:22:57 +0000 Subject: [PATCH 107/159] Bump version to 2024.9.0-alpha.8 --- 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 3bfa95a20..63c22cdc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 6c07ce87d..0f797e825 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 4be307f22363ab594984c240a292509bfb6895fa Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:55:35 +0900 Subject: [PATCH 108/159] refactor --- packages/frontend/src/ui/_common_/navbar-for-mobile.vue | 2 +- packages/frontend/src/ui/_common_/navbar.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index e80d5fd39..5115d21d5 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -82,7 +82,7 @@ function more() { diff --git a/idea/README.md b/idea/README.md new file mode 100644 index 000000000..f64d16800 --- /dev/null +++ b/idea/README.md @@ -0,0 +1 @@ +使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 57f68a2a2..7e0a932f8 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
- - - - +
@@ -110,7 +112,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import MkLink from '@/components/MkLink.vue'; -import MkButton from '@/components/MkButton.vue'; import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 537c86cb1..5207f0e38 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -169,42 +169,44 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - +
@@ -230,7 +232,6 @@ SPDX-License-Identifier: AGPL-3.0-only From 8c3be57ab362b8b2a24dad9b42ac0c3762bcb34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:12:34 +0900 Subject: [PATCH 127/159] =?UTF-8?q?fix(frontend-embed):=20URL=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89=E3=81=95=E3=82=8C=E3=81=9F?= =?UTF-8?q?=E6=96=87=E5=AD=97=E5=88=97=E3=81=8C=E6=AD=A3=E5=B8=B8=E3=81=AB?= =?UTF-8?q?=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=81=E3=81=AA=E3=81=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend-embed): URLエンコードされた文字列が正常に読み込めない問題を修正 * fix(frontend-embed): bring back missing bits --- packages/frontend-embed/src/pages/user-timeline.vue | 13 ++++++++++++- packages/frontend-embed/src/ui.vue | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue index 2d5dbb687..85e6f52d5 100644 --- a/packages/frontend-embed/src/pages/user-timeline.vue +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+