From 62299588d83dfaf3291b72c8d62d87d84ad1aea8 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: Sat, 4 Jan 2025 14:37:14 +0900 Subject: [PATCH 01/44] 2025 (#15203) --- .github/ISSUE_TEMPLATE/01_bug-report.yml | 4 ++-- COPYING | 2 +- packages/misskey-js/LICENSE | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml index 315e712c30e4..077855b5bf35 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml @@ -54,7 +54,7 @@ body: * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 * Browser: Chrome 113.0.5672.126 * Server URL: misskey.example.com - * Misskey: 2024.x.x + * Misskey: 2025.x.x value: | * Model and OS of the device(s): * Browser: @@ -74,7 +74,7 @@ body: Examples: * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment - * Misskey: 2024.x.x + * Misskey: 2025.x.x * Node: 20.x.x * PostgreSQL: 15.x.x * Redis: 7.x.x diff --git a/COPYING b/COPYING index 6a5f3ca1d598..7635bfc913a7 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2024 syuilo and contributors +Copyright © 2014-2025 syuilo and contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/packages/misskey-js/LICENSE b/packages/misskey-js/LICENSE index 63762b85d863..16352625dbb4 100644 --- a/packages/misskey-js/LICENSE +++ b/packages/misskey-js/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2024 syuilo and other contributors +Copyright (c) 2021-2025 syuilo and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 6649e58b562b424f6a1d8ca18d46f19399f7ab7d 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: Sat, 4 Jan 2025 14:40:41 +0900 Subject: [PATCH 02/44] =?UTF-8?q?enhance(frontend):=20=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=83=9F=E3=83=A5=E3=83=BC=E3=83=88=E3=83=BB=E3=83=8F?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=AF=E3=83=BC=E3=83=89=E3=83=9F=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(#15207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 8 ++++++++ locales/ja-JP.yml | 2 ++ packages/frontend/src/pages/settings/mute-block.vue | 10 ++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 63878d3d4771..e85d6a3bd5e9 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2754,10 +2754,18 @@ export interface Locale extends ILocale { * ワードミュート */ "wordMute": string; + /** + * 指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。 + */ + "wordMuteDescription": string; /** * ハードワードミュート */ "hardWordMute": string; + /** + * 指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。 + */ + "hardWordMuteDescription": string; /** * 正規表現エラー */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d78bd4ee656d..37e51b93982f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -684,7 +684,9 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" wordMute: "ワードミュート" +wordMuteDescription: "指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。" hardWordMute: "ハードワードミュート" +hardWordMuteDescription: "指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 4d413d53ab5d..797f0ec106a1 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,14 +9,20 @@ SPDX-License-Identifier: AGPL-3.0-only - +
+ {{ i18n.ts.wordMuteDescription }} + +
- +
+ {{ i18n.ts.hardWordMuteDescription }} + +
From 79b3d2a711cc659b9e6c1668b4d182906986bc50 Mon Sep 17 00:00:00 2001 From: Yuba Date: Sat, 4 Jan 2025 15:03:00 +0900 Subject: [PATCH 03/44] =?UTF-8?q?pg=5Fbigm=E3=81=8C=E5=88=A9=E7=94=A8?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=80=81=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E6=A4=9C=E7=B4=A2=E3=82=92ILIKE?= =?UTF-8?q?=E6=BC=94=E7=AE=97=E5=AD=90=E3=81=A7=E3=81=AA=E3=81=8FLIKE?= =?UTF-8?q?=E6=BC=94=E7=AE=97=E5=AD=90=E3=81=A7LOWER()=E3=82=92=E3=81=8B?= =?UTF-8?q?=E3=81=91=E3=81=9F=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E3=81=97=E3=81=A6=E8=A1=8C=E3=81=86=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=20(#15205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use LIKE-LOWER instead of ILIKE, which pg_bigm doesn't support. * changelog: Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように --- CHANGELOG.md | 1 + packages/backend/src/core/SearchService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd56700e1f2c..fc0d1e89c50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) +- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように ## 2024.11.0 diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index edfc47037523..f3c11277c548 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -215,7 +215,7 @@ export class SearchService { } query - .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` }) + .andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') From f4e025170e43b29be5ce25e0d6de8da372c0a048 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: Sat, 4 Jan 2025 15:06:03 +0900 Subject: [PATCH 04/44] =?UTF-8?q?fix(frontend/AiScript):=20Ui:C:select?= =?UTF-8?q?=E3=81=AE=E5=80=A4=E3=81=8C=E5=88=87=E3=82=8A=E6=9B=BF=E3=82=8F?= =?UTF-8?q?=E3=82=89=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(#15184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend/AiScript): Ui:C:selectの値が切り替わらない問題を修正 * Update Changelog --- CHANGELOG.md | 1 + packages/frontend/src/components/MkAsUi.vue | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0d1e89c50e..435d67d0089b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) - Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正 +- Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 ### Server - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index e52ab5ccad15..365b767bd646 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -77,8 +77,8 @@ import MkPostForm from '@/components/MkPostForm.vue'; const props = withDefaults(defineProps<{ component: AsUiComponent; components: Ref[]; - size: 'small' | 'medium' | 'large'; - align: 'left' | 'center' | 'right'; + size?: 'small' | 'medium' | 'large'; + align?: 'left' | 'center' | 'right'; }>(), { size: 'medium', align: 'left', @@ -86,7 +86,7 @@ const props = withDefaults(defineProps<{ const c = props.component; -function g(id) { +function g(id: string) { const v = props.components.find(x => x.value.id === id)?.value; if (v) return v; @@ -122,13 +122,22 @@ const containerStyle = computed(() => { const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false); -function onSwitchUpdate(v) { +function onSwitchUpdate(v: boolean) { valueForSwitch.value = v; if ('onChange' in c && c.onChange) { c.onChange(v as never); } } +const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null); + +function onSelectUpdate(v) { + valueForSelect.value = v; + if ('onChange' in c && c.onChange) { + c.onChange(v as never); + } +} + function openPostForm() { const form = (c as AsUiPostFormButton).form; if (!form) return; From 020882edcf2d79bf403c97b860008bf306fe5f90 Mon Sep 17 00:00:00 2001 From: 4ster1sk <146138447+4ster1sk@users.noreply.github.com> Date: Sat, 4 Jan 2025 15:06:19 +0900 Subject: [PATCH 05/44] =?UTF-8?q?fix(backend):=20=E3=82=A2=E3=83=97?= =?UTF-8?q?=E3=83=AA=E4=BD=9C=E6=88=90=E6=96=B9=E5=BC=8F=E3=81=A7=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=97=E3=81=9F=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E6=A8=A9=E9=99=90=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#15177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/endpoints/i/apps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 91c8597b1bd2..055b5cc061d1 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -87,7 +87,7 @@ export default class extends Endpoint { // eslint- name: token.name ?? token.app?.name, createdAt: this.idService.parse(token.id).date.toISOString(), lastUsedAt: token.lastUsedAt?.toISOString(), - permission: token.permission, + permission: token.app ? token.app.permission : token.permission, }))); }); } From 574034a2dd71da540711930ed743073aab3f71ee 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: Sat, 4 Jan 2025 15:08:29 +0900 Subject: [PATCH 06/44] =?UTF-8?q?fix(frontend):=20MiAuth=E8=AA=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=A7=E3=80=81=E8=AA=8D=E5=8F=AF=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=97=E3=81=9F=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=A7=E3=82=82=E3=82=B3=E3=83=BC=E3=83=AB=E3=83=90?= =?UTF-8?q?=E3=83=83=E3=82=AFURL=E3=81=AB=E9=81=B7=E7=A7=BB=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#15154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(MiAuth): アクセストークンの発行に失敗した場合コールバックに遷移しないようにする (MisskeyIO#831) Cherry-picked from TeamNijimiss/misskey@800359623e41a662551d774de15b0437b6849bb4 Co-authored-by: nafu-at * Update Changelog * Update Changelog --------- Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com> Co-authored-by: nafu-at --- CHANGELOG.md | 2 ++ packages/frontend/src/pages/miauth.vue | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 435d67d0089b..4b8c2428c2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) - Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正 - Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 +- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 + (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) ### Server - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 7fb6653c13f8..ab060587c54e 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -59,18 +59,18 @@ async function onAccept(token: string) { name: props.name, iconUrl: props.icon, permission: _permissions.value, - }, token).catch(() => { + }, token).then(() => { + if (props.callback && props.callback !== '') { + const cbUrl = new URL(props.callback); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url'); + cbUrl.searchParams.set('session', props.session); + location.href = cbUrl.toString(); + } else { + authRoot.value?.showUI('success'); + } + }).catch(() => { authRoot.value?.showUI('failed'); }); - - if (props.callback && props.callback !== '') { - const cbUrl = new URL(props.callback); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url'); - cbUrl.searchParams.set('session', props.session); - location.href = cbUrl.toString(); - } else { - authRoot.value?.showUI('success'); - } } function onDeny() { From 84bf90d1bda65e31966b004e06e0d2b30b926bbd Mon Sep 17 00:00:00 2001 From: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com> Date: Sat, 4 Jan 2025 15:08:52 +0900 Subject: [PATCH 07/44] =?UTF-8?q?Chore:=20Docker=E5=86=85=E3=81=AEDebian?= =?UTF-8?q?=E3=81=AE=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=92?= =?UTF-8?q?=E4=B8=8A=E3=81=92=E3=82=8B(bookworm)=20(#15073)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ee765abe7cb2..13f690946285 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=22.11.0-bullseye +ARG NODE_VERSION=22.11.0-bookworm # build assets & compile TypeScript From faea401dcc5c840487a19f26858ff4f252563b87 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: Sat, 4 Jan 2025 15:14:37 +0900 Subject: [PATCH 08/44] =?UTF-8?q?fix(frontend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E6=A4=9C=E7=B4=A2=E3=81=8C=E4=BD=BF=E7=94=A8=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=81=A7=E3=82=82?= =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB=E3=81=AE=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E6=A4=9C=E7=B4=A2=E6=AC=84=E3=81=8C=E3=81=A7?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=9F=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(#15082)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正 * Update Changelog --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/frontend/src/pages/channel.vue | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8c2428c2bc..8231015a2a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) - Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正 +- Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正 - Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 - Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index b61054118d3e..655e69461fe0 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts.notesSearchNotAvailable }} +
@@ -94,6 +97,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { notesSearchAvailable } from '@/scripts/check-permissions.js'; import { miLocalStorage } from '@/local-storage.js'; import { useRouter } from '@/router/supplier.js'; From 256560e8ba491d32dcae1cf00d1b7fefc40b4a1e 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: Sat, 4 Jan 2025 15:16:58 +0900 Subject: [PATCH 09/44] =?UTF-8?q?enhance(frontend/aiscript):=20=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=83=96=E5=86=85=E5=AE=B9=E3=82=92=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8BMk:remove=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(#15158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend/aiscript): セーブ内容を削除できる`Mk:remove`を追加 * fix * Update Changelog --- CHANGELOG.md | 1 + packages/frontend/src/scripts/aiscript/api.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8231015a2a41..057177a854a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Enhance: PC画面でチャンネルが複数列で表示されるように (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) - Enhance: 照会に失敗した場合、その理由を表示するように +- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加 - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 - Fix: サーバー情報メニューに区切り線が不足していたのを修正 - Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 46aed49330f7..8afe88eec6d3 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -75,12 +75,18 @@ export function createAiScriptEnv(opts) { */ 'Mk:save': values.FN_NATIVE(([key, value]) => { utils.assertString(key); + utils.expectAny(value); miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); return values.NULL; }), 'Mk:load': values.FN_NATIVE(([key]) => { utils.assertString(key); - return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); + return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null); + }), + 'Mk:remove': values.FN_NATIVE(([key]) => { + utils.assertString(key); + miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`); + return values.NULL; }), 'Mk:url': values.FN_NATIVE(() => { return values.STR(window.location.href); From 1fbc129d7ba0bffe3911baa085e274b7151b9561 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: Sat, 4 Jan 2025 15:24:25 +0900 Subject: [PATCH 10/44] =?UTF-8?q?fix(frontend):=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E6=B7=BB=E4=BB=98=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=95=B0=E3=82=92=E8=B6=85=E3=81=88=E3=81=9F=E3=82=89=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E6=8A=BC?= =?UTF-8?q?=E3=81=9B=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=20(#1512?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): ファイルの添付可能数を超えたらノートボタンを押せないように * Update Changelog * Update MkPostForm.vue --- CHANGELOG.md | 1 + packages/frontend/src/components/MkPostForm.vue | 1 + packages/frontend/src/components/MkPostFormAttaches.vue | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057177a854a5..8a4576a76cbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 - Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) +- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 ### Server - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 0b5794d1e309..ba988ee71512 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -251,6 +251,7 @@ const canPost = computed((): boolean => { quoteId.value != null ) && (textLength.value <= maxTextLength.value) && + (files.value.length <= 16) && (!poll.value || poll.value.choices.length >= 2); }); diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 56e026aa3cde..1336b61356fb 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -22,7 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-

{{ 16 - props.modelValue.length }}/16

+

{{ 16 - props.modelValue.length }}/16

@@ -239,5 +241,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar margin: 0; padding: 0; font-size: 90%; + + &.exceeded { + color: var(--MI_THEME-error); + } } From 4120c9ab10bd2974b95006656a424f1428348f6c 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: Sat, 4 Jan 2025 15:44:31 +0900 Subject: [PATCH 11/44] =?UTF-8?q?fix(frontend):=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=A7=E3=80=81=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=81=AE=E5=8F=96=E5=BE=97=E3=81=AB=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#15183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): アカウント一覧画面で、ユーザー情報の取得に失敗したアカウントが表示されない問題を修正 * Update Changelog * :art: --- CHANGELOG.md | 1 + .../frontend/src/pages/settings/accounts.vue | 107 ++++++++++++++---- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4576a76cbf..875d14ef683a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 +- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 ### Server - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index 97e960675faf..c2588736b35a 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -12,7 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.reloadAccountsList }} - + @@ -29,9 +38,10 @@ import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWith import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import { MenuItem } from '@/types/menu'; const storedAccounts = ref<{ id: string, token: string }[] | null>(null); -const accounts = ref([]); +const accounts = ref(new Map()); const init = async () => { getAccounts().then(accounts => { @@ -41,21 +51,35 @@ const init = async () => { userIds: storedAccounts.value.map(x => x.id), }); }).then(response => { - accounts.value = response; + if (storedAccounts.value == null) return; + accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null])); }); }; -function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) { - os.popupMenu([{ - text: i18n.ts.switch, - icon: 'ti ti-switch-horizontal', - action: () => switchAccount(account), - }, { - text: i18n.ts.logout, - icon: 'ti ti-trash', - danger: true, - action: () => removeAccount(account), - }], ev.currentTarget ?? ev.target); +function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) { + let menu: MenuItem[]; + + if (typeof account === 'string') { + menu = [{ + text: i18n.ts.logout, + icon: 'ti ti-trash', + danger: true, + action: () => removeAccount(account), + }]; + } else { + menu = [{ + text: i18n.ts.switch, + icon: 'ti ti-switch-horizontal', + action: () => switchAccount(account.id), + }, { + text: i18n.ts.logout, + icon: 'ti ti-trash', + danger: true, + action: () => removeAccount(account.id), + }]; + } + + os.popupMenu(menu, ev.currentTarget ?? ev.target); } function addAccount(ev: MouseEvent) { @@ -68,9 +92,9 @@ function addAccount(ev: MouseEvent) { }], ev.currentTarget ?? ev.target); } -async function removeAccount(account: Misskey.entities.UserDetailed) { - await _removeAccount(account.id); - accounts.value = accounts.value.filter(x => x.id !== account.id); +async function removeAccount(id: string) { + await _removeAccount(id); + accounts.value.delete(id); } function addExistingAccount() { @@ -90,9 +114,9 @@ function createAccount() { }); } -async function switchAccount(account: Misskey.entities.UserDetailed) { +async function switchAccount(id: string) { const fetchedAccounts = await getAccounts(); - const token = fetchedAccounts.find(x => x.id === account.id)!.token; + const token = fetchedAccounts.find(x => x.id === id)!.token; switchAccountWithToken(token); } @@ -112,6 +136,49 @@ definePageMetadata(() => ({ From 6c9eea2c0f3cda05101060bd2b85d76f6af23ceb 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: Sat, 4 Jan 2025 15:55:22 +0900 Subject: [PATCH 12/44] =?UTF-8?q?[ci=20skip]=20Update=20CHANGELOG.md=20(?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E6=96=B9=E3=82=92=E6=8F=83=E3=81=88=E3=82=8B?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875d14ef683a..f7ba1afc5262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,10 @@ - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 ### Server +- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) -- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように ## 2024.11.0 From 8ad97e5ede6b1cdfe9472073352ede46e585e7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:19:59 +0900 Subject: [PATCH 13/44] =?UTF-8?q?fix(backend):=20disableClustering?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E6=99=82=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96?= =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E8=AA=BF=E6=95=B4?= =?UTF-8?q?=20(#15224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): disableClustering設定時の初期化ロジックを調整 * onlyServer かつ enableCluster な場合、メインプロセスでlistenするとワーカープロセス側のlistenと衝突するため、メインプロセスはforkのみに制限する(listenしない) * ログの追加 * fix CHANGELOG.md * fix comment --- CHANGELOG.md | 2 +- packages/backend/src/boot/entry.ts | 20 +++++++++++++------- packages/backend/src/boot/master.ts | 24 ++++++++++++++++++------ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ba1afc5262..f065ed307f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) - +- Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) ## 2024.11.0 diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 25375c3015f7..da585ad68d0e 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -68,16 +68,22 @@ process.on('exit', code => { //#endregion -if (cluster.isPrimary || envOption.disableClustering) { - await masterMain(); - +if (!envOption.disableClustering) { if (cluster.isPrimary) { + logger.info(`Start main process... pid: ${process.pid}`); + await masterMain(); ev.mount(); + } else if (cluster.isWorker) { + logger.info(`Start worker process... pid: ${process.pid}`); + await workerMain(); + } else { + throw new Error('Unknown process type'); } -} - -if (cluster.isWorker || envOption.disableClustering) { - await workerMain(); +} else { + // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない) + logger.info(`Start main process... pid: ${process.pid}`); + await masterMain(); + ev.mount(); } readyRef.value = true; diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 4bc5c799cfb9..2b181af6757a 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -91,25 +91,37 @@ export async function masterMain() { }); } - if (envOption.disableClustering) { + bootLogger.info( + `mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]` + ); + + if (!envOption.disableClustering) { + // clusterモジュール有効時 + if (envOption.onlyServer) { - await server(); + // onlyServer かつ enableCluster な場合、メインプロセスはforkのみに制限する(listenしない)。 + // ワーカープロセス側でlistenすると、メインプロセスでポートへの着信を受け入れてワーカープロセスへの分配を行う動作をする。 + // そのため、メインプロセスでも直接listenするとポートの競合が発生して起動に失敗してしまう。 + // see: https://nodejs.org/api/cluster.html#cluster } else if (envOption.onlyQueue) { await jobQueue(); } else { await server(); await jobQueue(); } + + await spawnWorkers(config.clusterLimit); } else { + // clusterモジュール無効時 + if (envOption.onlyServer) { - // nop + await server(); } else if (envOption.onlyQueue) { - // nop + await jobQueue(); } else { await server(); + await jobQueue(); } - - await spawnWorkers(config.clusterLimit); } if (envOption.onlyQueue) { From 99ba7ebaa2ea60c4077e0f5af9638ab569314bfc Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Tue, 7 Jan 2025 21:21:05 +0900 Subject: [PATCH 14/44] =?UTF-8?q?fix(frontend-shared):=20nodemon=E3=82=92d?= =?UTF-8?q?evDependencies=E3=81=AB=E8=BF=BD=E5=8A=A0=20(#15225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend-shared/package.json | 1 + pnpm-lock.yaml | 123 +++++++++++--------------- 2 files changed, 52 insertions(+), 72 deletions(-) diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index f8770f33f2a5..4e681393c6bc 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -26,6 +26,7 @@ "@typescript-eslint/parser": "7.17.0", "esbuild": "0.24.0", "eslint-plugin-vue": "9.31.0", + "nodemon": "3.1.7", "typescript": "5.6.3", "vue-eslint-parser": "9.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0ec95018130..f50c635bf067 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/testing': specifier: 10.4.7 - version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)) + version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -1163,7 +1163,7 @@ importers: version: 7.17.0(eslint@9.14.0)(typescript@5.6.3) '@vitest/coverage-v8': specifier: 1.6.0 - version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0)) + version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0)) '@vue/runtime-core': specifier: 3.5.12 version: 3.5.12 @@ -1240,6 +1240,9 @@ importers: eslint-plugin-vue: specifier: 9.31.0 version: 9.31.0(eslint@9.14.0) + nodemon: + specifier: 3.1.7 + version: 3.1.7 typescript: specifier: 5.6.3 version: 5.6.3 @@ -10942,6 +10945,9 @@ packages: vue-component-type-helpers@2.1.10: resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==} + vue-component-type-helpers@2.2.0: + resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==} + vue-demi@0.14.7: resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} @@ -11780,7 +11786,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11800,7 +11806,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -12059,7 +12065,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.25.6 '@babel/types': 7.24.7 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12074,7 +12080,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.25.6 '@babel/types': 7.25.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12465,7 +12471,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -12475,7 +12481,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.1 @@ -12985,7 +12991,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.0 + semver: 7.6.3 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -13180,7 +13186,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))': + '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)': dependencies: '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -14554,7 +14560,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.12(typescript@5.6.3) - vue-component-type-helpers: 2.1.10 + vue-component-type-helpers: 2.2.0 '@swc/cli@0.3.12(@swc/core@1.9.2)(chokidar@3.5.3)': dependencies: @@ -15264,7 +15270,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3) '@typescript-eslint/utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3) - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) eslint: 9.14.0 ts-api-utils: 1.0.1(typescript@5.6.3) optionalDependencies: @@ -15276,7 +15282,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3) '@typescript-eslint/utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3) - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) eslint: 9.14.0 ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: @@ -15292,11 +15298,11 @@ snapshots: dependencies: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -15307,11 +15313,11 @@ snapshots: dependencies: '@typescript-eslint/types': 7.17.0 '@typescript-eslint/visitor-keys': 7.17.0 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.4 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -15327,7 +15333,7 @@ snapshots: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3) eslint: 9.14.0 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -15384,7 +15390,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))': + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))': dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 @@ -15399,7 +15405,7 @@ snapshots: std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0) + vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0) transitivePeerDependencies: - supports-color @@ -15637,14 +15643,14 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color optional: true agent-base@7.1.0: dependencies: - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -17248,7 +17254,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.24.0): dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.24.0 transitivePeerDependencies: - supports-color @@ -17490,7 +17496,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -17935,7 +17941,7 @@ snapshots: follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) for-each@0.3.3: dependencies: @@ -18138,7 +18144,7 @@ snapshots: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 + minimatch: 9.0.4 minipass: 7.0.4 path-scurry: 1.10.1 @@ -18405,7 +18411,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18444,7 +18450,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color optional: true @@ -18452,14 +18458,14 @@ snapshots: https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.0 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18805,7 +18811,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -18814,7 +18820,7 @@ snapshots: istanbul-lib-source-maps@5.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -19108,7 +19114,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -19215,35 +19221,6 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} - jsdom@24.1.1: - dependencies: - cssstyle: 4.0.1 - data-urls: 5.0.0 - decimal.js: 10.4.3 - form-data: 4.0.1 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.12 - parse5: 7.2.1 - rrweb-cssom: 0.7.1 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.0.0 - ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - optional: true - jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3): dependencies: cssstyle: 4.0.1 @@ -19936,7 +19913,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -20276,7 +20253,7 @@ snapshots: make-fetch-happen: 13.0.0 nopt: 7.2.0 proc-log: 4.2.0 - semver: 7.6.0 + semver: 7.6.3 tar: 6.2.1 which: 4.0.0 transitivePeerDependencies: @@ -21396,7 +21373,7 @@ snapshots: require-in-the-middle@7.3.0: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -21732,7 +21709,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.6.0 + semver: 7.6.3 sinon@16.1.3: dependencies: @@ -21821,7 +21798,7 @@ snapshots: socks-proxy-agent@8.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -21930,7 +21907,7 @@ snapshots: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -22677,7 +22654,7 @@ snapshots: vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.0.1 vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0) @@ -22695,7 +22672,7 @@ snapshots: vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.5 + debug: 4.3.7(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.0.1 vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0) @@ -22777,7 +22754,7 @@ snapshots: - supports-color - terser - vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0): + vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -22802,7 +22779,7 @@ snapshots: optionalDependencies: '@types/node': 22.9.0 happy-dom: 10.0.3 - jsdom: 24.1.1 + jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - less - lightningcss @@ -22853,6 +22830,8 @@ snapshots: vue-component-type-helpers@2.1.10: {} + vue-component-type-helpers@2.2.0: {} + vue-demi@0.14.7(vue@3.5.12(typescript@5.6.3)): dependencies: vue: 3.5.12(typescript@5.6.3) From f7da2bad6f0b25652ded11e6a9f86efc40872200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:23:05 +0900 Subject: [PATCH 15/44] =?UTF-8?q?fix(frontend):=20frontend=20/=20frontend-?= =?UTF-8?q?embed=E3=81=AB=E3=81=82=E3=82=8Btsconfig.json=E3=81=AEmodule?= =?UTF-8?q?=E3=82=92ES2022=E3=81=AB=E3=81=99=E3=82=8B=20(#15215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): frontend / frontend-embedにあるtsconfig.jsonのmoduleをES2022にする * fixed errors * fixed errors * fixed errors --- packages/frontend-embed/tsconfig.json | 4 ++-- packages/frontend/tsconfig.json | 4 ++-- packages/misskey-js/etc/misskey-js.api.md | 4 ++-- packages/misskey-js/src/streaming.ts | 12 +++++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json index 45f933dc2844..60164a7e3d37 100644 --- a/packages/frontend-embed/tsconfig.json +++ b/packages/frontend-embed/tsconfig.json @@ -10,8 +10,8 @@ "declaration": false, "sourceMap": false, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "ES2022", + "moduleResolution": "Bundler", "removeComments": false, "noLib": false, "strict": true, diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 4e5ca7f55944..68b6651a56ee 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -10,8 +10,8 @@ "declaration": false, "sourceMap": false, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "ES2022", + "moduleResolution": "Bundler", "removeComments": false, "noLib": false, "strict": true, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 01a3dbbb3074..50002f198332 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -6,8 +6,8 @@ import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import { EventEmitter } from 'eventemitter3'; +import { Options } from 'reconnecting-websocket'; import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; -import _ReconnectingWebsocket from 'reconnecting-websocket'; // Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts // @@ -3150,7 +3150,7 @@ export class Stream extends EventEmitter implements IStream { constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: _ReconnectingWebsocket.Options['WebSocket']; + WebSocket?: Options['WebSocket']; }); // (undocumented) close(): void; diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts index 6e34ec150817..fec6114ccaee 100644 --- a/packages/misskey-js/src/streaming.ts +++ b/packages/misskey-js/src/streaming.ts @@ -1,8 +1,10 @@ import { EventEmitter } from 'eventemitter3'; -import _ReconnectingWebsocket from 'reconnecting-websocket'; +import _ReconnectingWebSocket, { Options } from 'reconnecting-websocket'; import type { BroadcastEvents, Channels } from './streaming.types.js'; -const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default']; +// コンストラクタとクラスそのものの定義が上手く解決出来ないため再定義 +const ReconnectingWebSocketConstructor = _ReconnectingWebSocket as unknown as typeof _ReconnectingWebSocket.default; +type ReconnectingWebSocket = _ReconnectingWebSocket.default; export function urlQuery(obj: Record): string { const params = Object.entries(obj) @@ -43,7 +45,7 @@ export interface IStream extends EventEmitter { */ // eslint-disable-next-line import/no-default-export export default class Stream extends EventEmitter implements IStream { - private stream: _ReconnectingWebsocket.default; + private stream: ReconnectingWebSocket; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; private sharedConnections: SharedConnection[] = []; @@ -51,7 +53,7 @@ export default class Stream extends EventEmitter implements IStrea private idCounter = 0; constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: _ReconnectingWebsocket.Options['WebSocket']; + WebSocket?: Options['WebSocket']; }) { super(); @@ -80,7 +82,7 @@ export default class Stream extends EventEmitter implements IStrea const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://'); - this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', { + this.stream = new ReconnectingWebSocketConstructor(`${wsOrigin}/streaming?${query}`, '', { minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91 WebSocket: options.WebSocket, }); From bbe80af1dde195ff0ac6713db967b556acebb30c Mon Sep 17 00:00:00 2001 From: Take-John Date: Tue, 7 Jan 2025 21:28:48 +0900 Subject: [PATCH 16/44] =?UTF-8?q?Fix:=20aiscript=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E5=86=85=E3=81=AE=E5=9E=8B?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E8=A7=A3=E6=B6=88=E3=81=A8=E5=8D=98?= =?UTF-8?q?=E4=BD=93=E3=83=86=E3=82=B9=E3=83=88=20(#15191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * AiScript APIの型エラーに対処 * AiScript UI APIのテスト作成 * onInputなどがPromiseを返すように * AiScript共通APIのテスト作成 * CHANGELOG記載 * 定数のテストをconcurrentに * vi.mockを使用 * misskeyApiをmisskeyApiUntypedのエイリアスとする * 期待されるエラーメッセージを修正 * Mk:removeのテスト * misskeyApiの型を変更 --- CHANGELOG.md | 1 + packages/frontend/src/scripts/aiscript/api.ts | 38 +- .../frontend/src/scripts/aiscript/common.ts | 15 + packages/frontend/src/scripts/aiscript/ui.ts | 139 +-- packages/frontend/src/scripts/misskey-api.ts | 18 +- packages/frontend/test/aiscript/api.test.ts | 401 +++++++++ .../frontend/test/aiscript/common.test.ts | 23 + packages/frontend/test/aiscript/ui.test.ts | 825 ++++++++++++++++++ 8 files changed, 1396 insertions(+), 64 deletions(-) create mode 100644 packages/frontend/src/scripts/aiscript/common.ts create mode 100644 packages/frontend/test/aiscript/api.test.ts create mode 100644 packages/frontend/test/aiscript/common.test.ts create mode 100644 packages/frontend/test/aiscript/ui.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f065ed307f85..10de8b5fc57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 +- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 8afe88eec6d3..e203c51bba62 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -3,14 +3,24 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { utils, values } from '@syuilo/aiscript'; +import { errors, utils, values } from '@syuilo/aiscript'; import * as Misskey from 'misskey-js'; +import { url, lang } from '@@/js/config.js'; +import { assertStringAndIsIn } from './common.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@@/js/config.js'; + +const DIALOG_TYPES = [ + 'error', + 'info', + 'success', + 'warning', + 'waiting', + 'question', +] as const; export function aiScriptReadline(q: string): Promise { return new Promise(ok => { @@ -22,15 +32,20 @@ export function aiScriptReadline(q: string): Promise { }); } -export function createAiScriptEnv(opts) { +export function createAiScriptEnv(opts: { storageKey: string, token?: string }) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, - USER_NAME: $i ? values.STR($i.name) : values.NULL, + USER_NAME: $i?.name ? values.STR($i.name) : values.NULL, USER_USERNAME: $i ? values.STR($i.username) : values.NULL, CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), LOCALE: values.STR(lang), SERVER_URL: values.STR(url), 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { + utils.assertString(title); + utils.assertString(text); + if (type != null) { + assertStringAndIsIn(type, DIALOG_TYPES); + } await os.alert({ type: type ? type.value : 'info', title: title.value, @@ -39,6 +54,11 @@ export function createAiScriptEnv(opts) { return values.NULL; }), 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { + utils.assertString(title); + utils.assertString(text); + if (type != null) { + assertStringAndIsIn(type, DIALOG_TYPES); + } const confirm = await os.confirm({ type: type ? type.value : 'question', title: title.value, @@ -48,14 +68,20 @@ export function createAiScriptEnv(opts) { }), 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { utils.assertString(ep); - if (ep.value.includes('://')) throw new Error('invalid endpoint'); + if (ep.value.includes('://')) { + throw new errors.AiScriptRuntimeError('invalid endpoint'); + } if (token) { utils.assertString(token); // バグがあればundefinedもあり得るため念のため if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { + if (param == null) { + throw new errors.AiScriptRuntimeError('expected param'); + } + utils.assertObject(param); + return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/scripts/aiscript/common.ts new file mode 100644 index 000000000000..de6fa1d63361 --- /dev/null +++ b/packages/frontend/src/scripts/aiscript/common.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { errors, utils, type values } from '@syuilo/aiscript'; + +export function assertStringAndIsIn(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } { + utils.assertString(value); + const str = value.value; + if (!expects.includes(str)) { + const expected = expects.map((expect) => `"${expect}"`).join(', '); + throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`); + } +} diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 2b386bebb81a..ca92b27ff54e 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -7,6 +7,15 @@ import { utils, values } from '@syuilo/aiscript'; import { v4 as uuid } from 'uuid'; import { ref, Ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { assertStringAndIsIn } from './common.js'; + +const ALIGNS = ['left', 'center', 'right'] as const; +const FONTS = ['serif', 'sans-serif', 'monospace'] as const; +const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const; + +type Align = (typeof ALIGNS)[number]; +type Font = (typeof FONTS)[number]; +type BorderStyle = (typeof BORDER_STYLES)[number]; export type AsUiComponentBase = { id: string; @@ -21,13 +30,13 @@ export type AsUiRoot = AsUiComponentBase & { export type AsUiContainer = AsUiComponentBase & { type: 'container'; children?: AsUiComponent['id'][]; - align?: 'left' | 'center' | 'right'; + align?: Align; bgColor?: string; fgColor?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; + font?: Font; borderWidth?: number; borderColor?: string; - borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; + borderStyle?: BorderStyle; borderRadius?: number; padding?: number; rounded?: boolean; @@ -40,7 +49,7 @@ export type AsUiText = AsUiComponentBase & { size?: number; bold?: boolean; color?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; + font?: Font; }; export type AsUiMfm = AsUiComponentBase & { @@ -49,14 +58,14 @@ export type AsUiMfm = AsUiComponentBase & { size?: number; bold?: boolean; color?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; - onClickEv?: (evId: string) => void + font?: Font; + onClickEv?: (evId: string) => Promise; }; export type AsUiButton = AsUiComponentBase & { type: 'button'; text?: string; - onClick?: () => void; + onClick?: () => Promise; primary?: boolean; rounded?: boolean; disabled?: boolean; @@ -69,7 +78,7 @@ export type AsUiButtons = AsUiComponentBase & { export type AsUiSwitch = AsUiComponentBase & { type: 'switch'; - onChange?: (v: boolean) => void; + onChange?: (v: boolean) => Promise; default?: boolean; label?: string; caption?: string; @@ -77,7 +86,7 @@ export type AsUiSwitch = AsUiComponentBase & { export type AsUiTextarea = AsUiComponentBase & { type: 'textarea'; - onInput?: (v: string) => void; + onInput?: (v: string) => Promise; default?: string; label?: string; caption?: string; @@ -85,7 +94,7 @@ export type AsUiTextarea = AsUiComponentBase & { export type AsUiTextInput = AsUiComponentBase & { type: 'textInput'; - onInput?: (v: string) => void; + onInput?: (v: string) => Promise; default?: string; label?: string; caption?: string; @@ -93,7 +102,7 @@ export type AsUiTextInput = AsUiComponentBase & { export type AsUiNumberInput = AsUiComponentBase & { type: 'numberInput'; - onInput?: (v: number) => void; + onInput?: (v: number) => Promise; default?: number; label?: string; caption?: string; @@ -105,7 +114,7 @@ export type AsUiSelect = AsUiComponentBase & { text: string; value: string; }[]; - onChange?: (v: string) => void; + onChange?: (v: string) => Promise; default?: string; label?: string; caption?: string; @@ -140,11 +149,15 @@ export type AsUiPostForm = AsUiComponentBase & { export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm; +type Options = T extends AsUiButtons + ? Omit & { 'buttons'?: Options[] } + : Omit; + export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise) { // TODO } -function getRootOptions(def: values.Value | undefined): Omit { +function getRootOptions(def: values.Value | undefined): Options { utils.assertObject(def); const children = def.value.get('children'); @@ -153,30 +166,32 @@ function getRootOptions(def: values.Value | undefined): Omit { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }), }; } -function getContainerOptions(def: values.Value | undefined): Omit { +function getContainerOptions(def: values.Value | undefined): Options { utils.assertObject(def); const children = def.value.get('children'); if (children) utils.assertArray(children); const align = def.value.get('align'); - if (align) utils.assertString(align); + if (align) assertStringAndIsIn(align, ALIGNS); const bgColor = def.value.get('bgColor'); if (bgColor) utils.assertString(bgColor); const fgColor = def.value.get('fgColor'); if (fgColor) utils.assertString(fgColor); const font = def.value.get('font'); - if (font) utils.assertString(font); + if (font) assertStringAndIsIn(font, FONTS); const borderWidth = def.value.get('borderWidth'); if (borderWidth) utils.assertNumber(borderWidth); const borderColor = def.value.get('borderColor'); if (borderColor) utils.assertString(borderColor); const borderStyle = def.value.get('borderStyle'); - if (borderStyle) utils.assertString(borderStyle); + if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES); const borderRadius = def.value.get('borderRadius'); if (borderRadius) utils.assertNumber(borderRadius); const padding = def.value.get('padding'); @@ -189,7 +204,9 @@ function getContainerOptions(def: values.Value | undefined): Omit { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }) : [], align: align?.value, fgColor: fgColor?.value, @@ -205,7 +222,7 @@ function getContainerOptions(def: values.Value | undefined): Omit { +function getTextOptions(def: values.Value | undefined): Options { utils.assertObject(def); const text = def.value.get('text'); @@ -217,7 +234,7 @@ function getTextOptions(def: values.Value | undefined): Omit Promise): Omit { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const text = def.value.get('text'); @@ -240,7 +257,7 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg const color = def.value.get('color'); if (color) utils.assertString(color); const font = def.value.get('font'); - if (font) utils.assertString(font); + if (font) assertStringAndIsIn(font, FONTS); const onClickEv = def.value.get('onClickEv'); if (onClickEv) utils.assertFunction(onClickEv); @@ -250,13 +267,13 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg bold: bold?.value, color: color?.value, font: font?.value, - onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, [values.STR(evId)]); + onClickEv: async (evId: string) => { + if (onClickEv) await call(onClickEv, [values.STR(evId)]); }, }; } -function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -269,8 +286,8 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -278,7 +295,7 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF }; } -function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -291,8 +308,8 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -300,7 +317,7 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn }; } -function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -313,8 +330,8 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values. if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -322,7 +339,7 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values. }; } -function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const text = def.value.get('text'); @@ -338,8 +355,8 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, return { text: text?.value, - onClick: () => { - if (onClick) call(onClick, []); + onClick: async () => { + if (onClick) await call(onClick, []); }, primary: primary?.value, rounded: rounded?.value, @@ -347,7 +364,7 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const buttons = def.value.get('buttons'); @@ -369,8 +386,8 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, return { text: text.value, - onClick: () => { - call(onClick, []); + onClick: async () => { + await call(onClick, []); }, primary: primary?.value, rounded: rounded?.value, @@ -380,7 +397,7 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const onChange = def.value.get('onChange'); @@ -393,8 +410,8 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, if (caption) utils.assertString(caption); return { - onChange: (v) => { - if (onChange) call(onChange, [utils.jsToVal(v)]); + onChange: async (v) => { + if (onChange) await call(onChange, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -402,7 +419,7 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const items = def.value.get('items'); @@ -428,8 +445,8 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, value: value ? value.value : text.value, }; }) : [], - onChange: (v) => { - if (onChange) call(onChange, [utils.jsToVal(v)]); + onChange: async (v) => { + if (onChange) await call(onChange, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -437,7 +454,7 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getFolderOptions(def: values.Value | undefined): Omit { +function getFolderOptions(def: values.Value | undefined): Options { utils.assertObject(def); const children = def.value.get('children'); @@ -450,7 +467,9 @@ function getFolderOptions(def: values.Value | undefined): Omit { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }) : [], title: title?.value ?? '', opened: opened?.value ?? true, @@ -475,7 +494,7 @@ function getPostFormProps(form: values.VObj): PostFormPropsForAsUi { }; } -function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const text = def.value.get('text'); @@ -497,7 +516,7 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu }; } -function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { +function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { utils.assertObject(def); const form = def.value.get('form'); @@ -511,18 +530,26 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn } export function registerAsUiLib(components: Ref[], done: (root: Ref) => void) { + type OptionsConverter = (def: values.Value | undefined, call: C) => Options; + const instances = {}; - function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise) => any, call: (fn: values.VFn, args: values.Value[]) => Promise) { + function createComponentInstance( + type: T['type'], + def: values.Value | undefined, + id: values.Value | undefined, + getOptions: OptionsConverter, + call: C, + ) { if (id) utils.assertString(id); const _id = id?.value ?? uuid(); const component = ref({ ...getOptions(def, call), type, id: _id, - }); + } as T); components.push(component); - const instance = values.OBJ(new Map([ + const instance = values.OBJ(new Map([ ['id', values.STR(_id)], ['update', values.FN_NATIVE(([def], opts) => { utils.assertObject(def); @@ -547,7 +574,7 @@ export function registerAsUiLib(components: Ref[], done: (root: R 'Ui:patch': values.FN_NATIVE(([id, val], opts) => { utils.assertString(id); utils.assertArray(val); - patch(id.value, val.value, opts.call); + // patch(id.value, val.value, opts.call); // TODO }), 'Ui:get': values.FN_NATIVE(([id], opts) => { @@ -566,7 +593,9 @@ export function registerAsUiLib(components: Ref[], done: (root: R rootComponent.value.children = children.value.map(v => { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }); }), diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index e7a92e2d5cda..dc07ad477b4a 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -9,12 +9,24 @@ import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); +export type Endpoint = keyof Misskey.Endpoints; + +export type Request = Misskey.Endpoints[E]['req']; + +export type AnyRequest = + (E extends Endpoint ? Request : never) | object; + +export type Response> = + E extends Endpoint + ? P extends Request ? Misskey.api.SwitchCaseResponseType : never + : object; + // 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, + E extends Endpoint | NonNullable = Endpoint, + P extends AnyRequest = E extends Endpoint ? Request : never, + _ResT = ResT extends void ? Response : ResT, >( endpoint: E, data: P & { i?: string | null; } = {} as any, diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts new file mode 100644 index 000000000000..2a15a74249a4 --- /dev/null +++ b/packages/frontend/test/aiscript/api.test.ts @@ -0,0 +1,401 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { miLocalStorage } from '@/local-storage.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { errors, Interpreter, Parser, values } from '@syuilo/aiscript'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, + vi +} from 'vitest'; + +async function exe(script: string): Promise { + const outputs: values.Value[] = []; + const interpreter = new Interpreter( + createAiScriptEnv({ storageKey: 'widget' }), + { + in: aiScriptReadline, + out: (value) => { + outputs.push(value); + } + } + ); + const ast = Parser.parse(script); + await interpreter.exec(ast); + return outputs; +} + +let $iMock = vi.hoisted | null >( + () => null +); + +vi.mock('@/account.js', () => { + return { + get $i() { + return $iMock; + }, + }; +}); + +const osMock = vi.hoisted(() => { + return { + inputText: vi.fn(), + alert: vi.fn(), + confirm: vi.fn(), + }; +}); + +vi.mock('@/os.js', () => { + return osMock; +}); + +const misskeyApiMock = vi.hoisted(() => vi.fn()); + +vi.mock('@/scripts/misskey-api.js', () => { + return { misskeyApi: misskeyApiMock }; +}); + +describe('AiScript common API', () => { + afterAll(() => { + vi.unstubAllGlobals(); + }); + + describe('readline', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.inputText.mockImplementationOnce(async ({ title }) => { + expect(title).toBe('question'); + return { + canceled: false, + result: 'Hello', + }; + }); + const [res] = await exe(` + <: readline('question') + `); + expect(res).toStrictEqual(values.STR('Hello')); + expect(osMock.inputText).toHaveBeenCalledOnce(); + }); + + test.sequential('cancelled', async () => { + osMock.inputText.mockImplementationOnce(async ({ title }) => { + expect(title).toBe('question'); + return { + canceled: true, + result: undefined, + }; + }); + const [res] = await exe(` + <: readline('question') + `); + expect(res).toStrictEqual(values.STR('')); + expect(osMock.inputText).toHaveBeenCalledOnce(); + }); + }); + + describe('user constants', () => { + describe.sequential('logged in', () => { + beforeAll(() => { + $iMock = { + id: 'xxxxxxxx', + name: '藍', + username: 'ai', + }; + }); + + test.concurrent('USER_ID', async () => { + const [res] = await exe(` + <: USER_ID + `); + expect(res).toStrictEqual(values.STR('xxxxxxxx')); + }); + + test.concurrent('USER_NAME', async () => { + const [res] = await exe(` + <: USER_NAME + `); + expect(res).toStrictEqual(values.STR('藍')); + }); + + test.concurrent('USER_USERNAME', async () => { + const [res] = await exe(` + <: USER_USERNAME + `); + expect(res).toStrictEqual(values.STR('ai')); + }); + }); + + describe.sequential('not logged in', () => { + beforeAll(() => { + $iMock = null; + }); + + test.concurrent('USER_ID', async () => { + const [res] = await exe(` + <: USER_ID + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.concurrent('USER_NAME', async () => { + const [res] = await exe(` + <: USER_NAME + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.concurrent('USER_USERNAME', async () => { + const [res] = await exe(` + <: USER_USERNAME + `); + expect(res).toStrictEqual(values.NULL); + }); + }); + }); + + describe('dialog', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.alert.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('success'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + }); + const [res] = await exe(` + <: Mk:dialog('Hello', 'world', 'success') + `); + expect(res).toStrictEqual(values.NULL); + expect(osMock.alert).toHaveBeenCalledOnce(); + }); + + test.sequential('omit type', async () => { + osMock.alert.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('info'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + }); + const [res] = await exe(` + <: Mk:dialog('Hello', 'world') + `); + expect(res).toStrictEqual(values.NULL); + expect(osMock.alert).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid type', async () => { + await expect(() => exe(` + <: Mk:dialog('Hello', 'world', 'invalid') + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + expect(osMock.alert).not.toHaveBeenCalled(); + }); + }); + + describe('confirm', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('success'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: false }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world', 'success') + `); + expect(res).toStrictEqual(values.TRUE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('omit type', async () => { + osMock.confirm + .mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('question'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: false }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world') + `); + expect(res).toStrictEqual(values.TRUE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('canceled', async () => { + osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('question'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: true }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world') + `); + expect(res).toStrictEqual(values.FALSE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid type', async () => { + const confirm = osMock.confirm; + await expect(() => exe(` + <: Mk:confirm('Hello', 'world', 'invalid') + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + expect(confirm).not.toHaveBeenCalled(); + }); + }); + + describe('api', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('successful', async () => { + misskeyApiMock.mockImplementationOnce( + async (endpoint, data, token) => { + expect(endpoint).toBe('ping'); + expect(data).toStrictEqual({}); + expect(token).toBeNull(); + return { pong: 1735657200000 }; + } + ); + const [res] = await exe(` + <: Mk:api('ping', {}) + `); + expect(res).toStrictEqual(values.OBJ(new Map([ + ['pong', values.NUM(1735657200000)], + ]))); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('with token', async () => { + misskeyApiMock.mockImplementationOnce( + async (endpoint, data, token) => { + expect(endpoint).toBe('ping'); + expect(data).toStrictEqual({}); + expect(token).toStrictEqual('xxxxxxxx'); + return { pong: 1735657200000 }; + } + ); + const [res] = await exe(` + <: Mk:api('ping', {}, 'xxxxxxxx') + `); + expect(res).toStrictEqual(values.OBJ(new Map([ + ['pong', values.NUM(1735657200000 )], + ]))); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('request failed', async () => { + misskeyApiMock.mockRejectedValueOnce('Not Found'); + const [res] = await exe(` + <: Mk:api('this/endpoint/should/not/be/found', {}) + `); + expect(res).toStrictEqual( + values.ERROR('request_failed', values.STR('Not Found')) + ); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid endpoint', async () => { + await expect(() => exe(` + Mk:api('https://example.com/api/ping', {}) + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('invalid endpoint'), + ); + expect(misskeyApiMock).not.toHaveBeenCalled(); + }); + + test.sequential('missing param', async () => { + await expect(() => exe(` + Mk:api('ping') + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('expected param'), + ); + expect(misskeyApiMock).not.toHaveBeenCalled(); + }); + }); + + describe('save and load', () => { + beforeEach(() => { + miLocalStorage.removeItem('aiscript:widget:key'); + }); + + afterEach(() => { + miLocalStorage.removeItem('aiscript:widget:key'); + }); + + test.sequential('successful', async () => { + const [res] = await exe(` + Mk:save('key', 'value') + <: Mk:load('key') + `); + expect(miLocalStorage.getItem('aiscript:widget:key')).toBe('"value"'); + expect(res).toStrictEqual(values.STR('value')); + }); + + test.sequential('missing value to save', async () => { + await expect(() => exe(` + Mk:save('key') + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('Expect anything, but got nothing.'), + ); + }); + + test.sequential('not value found to load', async () => { + const [res] = await exe(` + <: Mk:load('key') + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.sequential('remove existing', async () => { + const res = await exe(` + Mk:save('key', 'value') + <: Mk:load('key') + <: Mk:remove('key') + <: Mk:load('key') + `); + expect(res).toStrictEqual([values.STR('value'), values.NULL, values.NULL]); + }); + + test.sequential('remove nothing', async () => { + const res = await exe(` + <: Mk:load('key') + <: Mk:remove('key') + <: Mk:load('key') + `); + expect(res).toStrictEqual([values.NULL, values.NULL, values.NULL]); + }); + }); + + test.concurrent('url', async () => { + vi.stubGlobal('location', { href: 'https://example.com/' }); + const [res] = await exe(` + <: Mk:url() + `); + expect(res).toStrictEqual(values.STR('https://example.com/')); + }); + + test.concurrent('nyaize', async () => { + const [res] = await exe(` + <: Mk:nyaize('な') + `); + expect(res).toStrictEqual(values.STR('にゃ')); + }); +}); diff --git a/packages/frontend/test/aiscript/common.test.ts b/packages/frontend/test/aiscript/common.test.ts new file mode 100644 index 000000000000..acc48826eaf7 --- /dev/null +++ b/packages/frontend/test/aiscript/common.test.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { assertStringAndIsIn } from "@/scripts/aiscript/common.js"; +import { values } from "@syuilo/aiscript"; +import { describe, expect, test } from "vitest"; + +describe('AiScript common script', () => { + test('assertStringAndIsIn', () => { + expect( + () => assertStringAndIsIn(values.STR('a'), ['a', 'b']) + ).not.toThrow(); + expect( + () => assertStringAndIsIn(values.STR('c'), ['a', 'b']) + ).toThrow('"c" is not in "a", "b"'); + expect(() => assertStringAndIsIn( + values.STR('invalid'), + ['left', 'center', 'right'] + )).toThrow('"invalid" is not in "left", "center", "right"'); + }); +}); diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts new file mode 100644 index 000000000000..5f77edbb4973 --- /dev/null +++ b/packages/frontend/test/aiscript/ui.test.ts @@ -0,0 +1,825 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { registerAsUiLib } from '@/scripts/aiscript/ui.js'; +import { errors, Interpreter, Parser, values } from '@syuilo/aiscript'; +import { describe, expect, test } from 'vitest'; +import { type Ref, ref } from 'vue'; +import type { + AsUiButton, + AsUiButtons, + AsUiComponent, + AsUiMfm, + AsUiNumberInput, + AsUiRoot, + AsUiSelect, + AsUiSwitch, + AsUiText, + AsUiTextarea, + AsUiTextInput, +} from '@/scripts/aiscript/ui.js'; + +type ExeResult = { + root: AsUiRoot; + get: (id: string) => AsUiComponent; + outputs: values.Value[]; +} +async function exe(script: string): Promise { + const rootRef = ref(); + const componentRefs = ref[]>([]); + const outputs: values.Value[] = []; + + const interpreter = new Interpreter( + registerAsUiLib(componentRefs.value, (root) => { + rootRef.value = root.value; + }), + { + out: (value) => { + outputs.push(value); + } + } + ); + const ast = Parser.parse(script); + await interpreter.exec(ast); + + const root = rootRef.value; + if (root === undefined) { + expect.unreachable('root must not be undefined'); + } + const components = componentRefs.value.map( + (componentRef) => componentRef.value, + ); + expect(root).toBe(components[0]); + expect(root.type).toBe('root'); + const get = (id: string) => { + const component = componentRefs.value.find( + (componentRef) => componentRef.value.id === id, + ); + if (component === undefined) { + expect.unreachable(`component "${id}" is not defined`); + } + return component.value; + }; + return { root, get, outputs }; +} + +describe('AiScript UI API', () => { + test.concurrent('root', async () => { + const { root } = await exe(''); + expect(root.children).toStrictEqual([]); + }); + + describe('get', () => { + test.concurrent('some', async () => { + const { outputs } = await exe(` + Ui:C:text({}, 'id') + <: Ui:get('id') + `); + const output = outputs[0] as values.VObj; + expect(output.type).toBe('obj'); + expect(output.value.size).toBe(2); + expect(output.value.get('id')).toStrictEqual(values.STR('id')); + expect(output.value.get('update')!.type).toBe('fn'); + }); + + test.concurrent('none', async () => { + const { outputs } = await exe(` + <: Ui:get('id') + `); + expect(outputs).toStrictEqual([values.NULL]); + }); + }); + + describe('update', () => { + test.concurrent('normal', async () => { + const { get } = await exe(` + let text = Ui:C:text({ text: 'a' }, 'id') + text.update({ text: 'b' }) + `); + const text = get('id') as AsUiText; + expect(text.text).toBe('b'); + }); + + test.concurrent('skip unknown key', async () => { + const { get } = await exe(` + let text = Ui:C:text({ text: 'a' }, 'id') + text.update({ + text: 'b' + unknown: null + }) + `); + const text = get('id') as AsUiText; + expect(text.text).toBe('b'); + expect('unknown' in text).toBeFalsy(); + }); + }); + + describe('container', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let text = Ui:C:text({ + text: 'text' + }, 'id1') + let container = Ui:C:container({ + children: [text] + align: 'left' + bgColor: '#fff' + fgColor: '#000' + font: 'sans-serif' + borderWidth: 1 + borderColor: '#f00' + borderStyle: 'hidden' + borderRadius: 2 + padding: 3 + rounded: true + hidden: false + }, 'id2') + Ui:render([container]) + `); + expect(root.children).toStrictEqual(['id2']); + expect(get('id2')).toStrictEqual({ + type: 'container', + id: 'id2', + children: ['id1'], + align: 'left', + bgColor: '#fff', + fgColor: '#000', + font: 'sans-serif', + borderColor: '#f00', + borderWidth: 1, + borderStyle: 'hidden', + borderRadius: 2, + padding: 3, + rounded: true, + hidden: false, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:container({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'container', + id: 'id', + children: [], + align: undefined, + fgColor: undefined, + bgColor: undefined, + font: undefined, + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + borderRadius: undefined, + padding: undefined, + rounded: undefined, + hidden: undefined, + }); + }); + + test.concurrent('invalid children', async () => { + await expect(() => exe(` + Ui:C:container({ + children: 0 + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid align', async () => { + await expect(() => exe(` + Ui:C:container({ + align: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:container({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid borderStyle', async () => { + await expect(() => exe(` + Ui:C:container({ + borderStyle: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('text', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let text = Ui:C:text({ + text: 'a' + size: 1 + bold: true + color: '#000' + font: 'sans-serif' + }, 'id') + Ui:render([text]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'text', + id: 'id', + text: 'a', + size: 1, + bold: true, + color: '#000', + font: 'sans-serif', + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:text({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'text', + id: 'id', + text: undefined, + size: undefined, + bold: undefined, + color: undefined, + font: undefined, + }); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:text({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('mfm', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let mfm = Ui:C:mfm({ + text: 'text' + size: 1 + bold: true + color: '#000' + font: 'sans-serif' + onClickEv: print + }, 'id') + Ui:render([mfm]) + `); + expect(root.children).toStrictEqual(['id']); + const { onClickEv, ...mfm } = get('id') as AsUiMfm; + expect(mfm).toStrictEqual({ + type: 'mfm', + id: 'id', + text: 'text', + size: 1, + bold: true, + color: '#000', + font: 'sans-serif', + }); + await onClickEv!('a'); + expect(outputs).toStrictEqual([values.STR('a')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:mfm({}, 'id') + `); + const { onClickEv, ...mfm } = get('id') as AsUiMfm; + expect(onClickEv).toBeTypeOf('function'); + expect(mfm).toStrictEqual({ + type: 'mfm', + id: 'id', + text: undefined, + size: undefined, + bold: undefined, + color: undefined, + font: undefined, + }); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:mfm({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('textInput', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let text_input = Ui:C:textInput({ + onInput: print + default: 'a' + label: 'b' + caption: 'c' + }, 'id') + Ui:render([text_input]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...textInput } = get('id') as AsUiTextInput; + expect(textInput).toStrictEqual({ + type: 'textInput', + id: 'id', + default: 'a', + label: 'b', + caption: 'c', + }); + await onInput!('d'); + expect(outputs).toStrictEqual([values.STR('d')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:textInput({}, 'id') + `); + const { onInput, ...textInput } = get('id') as AsUiTextInput; + expect(onInput).toBeTypeOf('function'); + expect(textInput).toStrictEqual({ + type: 'textInput', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('textarea', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let textarea = Ui:C:textarea({ + onInput: print + default: 'a' + label: 'b' + caption: 'c' + }, 'id') + Ui:render([textarea]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...textarea } = get('id') as AsUiTextarea; + expect(textarea).toStrictEqual({ + type: 'textarea', + id: 'id', + default: 'a', + label: 'b', + caption: 'c', + }); + await onInput!('d'); + expect(outputs).toStrictEqual([values.STR('d')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:textarea({}, 'id') + `); + const { onInput, ...textarea } = get('id') as AsUiTextarea; + expect(onInput).toBeTypeOf('function'); + expect(textarea).toStrictEqual({ + type: 'textarea', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('numberInput', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let number_input = Ui:C:numberInput({ + onInput: print + default: 1 + label: 'a' + caption: 'b' + }, 'id') + Ui:render([number_input]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...numberInput } = get('id') as AsUiNumberInput; + expect(numberInput).toStrictEqual({ + type: 'numberInput', + id: 'id', + default: 1, + label: 'a', + caption: 'b', + }); + await onInput!(2); + expect(outputs).toStrictEqual([values.NUM(2)]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:numberInput({}, 'id') + `); + const { onInput, ...numberInput } = get('id') as AsUiNumberInput; + expect(onInput).toBeTypeOf('function'); + expect(numberInput).toStrictEqual({ + type: 'numberInput', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('button', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let button = Ui:C:button({ + text: 'a' + onClick: @() { <: 'clicked' } + primary: true + rounded: false + disabled: false + }, 'id') + Ui:render([button]) + `); + expect(root.children).toStrictEqual(['id']); + const { onClick, ...button } = get('id') as AsUiButton; + expect(button).toStrictEqual({ + type: 'button', + id: 'id', + text: 'a', + primary: true, + rounded: false, + disabled: false, + }); + await onClick!(); + expect(outputs).toStrictEqual([values.STR('clicked')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:button({}, 'id') + `); + const { onClick, ...button } = get('id') as AsUiButton; + expect(onClick).toBeTypeOf('function'); + expect(button).toStrictEqual({ + type: 'button', + id: 'id', + text: undefined, + primary: undefined, + rounded: undefined, + disabled: undefined, + }); + }); + }); + + describe('buttons', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let buttons = Ui:C:buttons({ + buttons: [] + }, 'id') + Ui:render([buttons]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'buttons', + id: 'id', + buttons: [], + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:buttons({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'buttons', + id: 'id', + buttons: [], + }); + }); + + test.concurrent('some buttons', async () => { + const { root, get, outputs } = await exe(` + let buttons = Ui:C:buttons({ + buttons: [ + { + text: 'a' + onClick: @() { <: 'clicked a' } + primary: true + rounded: false + disabled: false + } + { + text: 'b' + onClick: @() { <: 'clicked b' } + primary: true + rounded: false + disabled: false + } + ] + }, 'id') + Ui:render([buttons]) + `); + expect(root.children).toStrictEqual(['id']); + const { buttons, ...buttonsOptions } = get('id') as AsUiButtons; + expect(buttonsOptions).toStrictEqual({ + type: 'buttons', + id: 'id', + }); + expect(buttons!.length).toBe(2); + const { onClick: onClickA, ...buttonA } = buttons![0]; + expect(buttonA).toStrictEqual({ + text: 'a', + primary: true, + rounded: false, + disabled: false, + }); + const { onClick: onClickB, ...buttonB } = buttons![1]; + expect(buttonB).toStrictEqual({ + text: 'b', + primary: true, + rounded: false, + disabled: false, + }); + await onClickA!(); + await onClickB!(); + expect(outputs).toStrictEqual( + [values.STR('clicked a'), values.STR('clicked b')] + ); + }); + }); + + describe('switch', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let switch = Ui:C:switch({ + onChange: print + default: false + label: 'a' + caption: 'b' + }, 'id') + Ui:render([switch]) + `); + expect(root.children).toStrictEqual(['id']); + const { onChange, ...switchOptions } = get('id') as AsUiSwitch; + expect(switchOptions).toStrictEqual({ + type: 'switch', + id: 'id', + default: false, + label: 'a', + caption: 'b', + }); + await onChange!(true); + expect(outputs).toStrictEqual([values.TRUE]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:switch({}, 'id') + `); + const { onChange, ...switchOptions } = get('id') as AsUiSwitch; + expect(onChange).toBeTypeOf('function'); + expect(switchOptions).toStrictEqual({ + type: 'switch', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('select', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let select = Ui:C:select({ + items: [ + { text: 'A', value: 'a' } + { text: 'B', value: 'b' } + ] + onChange: print + default: 'a' + label: 'c' + caption: 'd' + }, 'id') + Ui:render([select]) + `); + expect(root.children).toStrictEqual(['id']); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [ + { text: 'A', value: 'a' }, + { text: 'B', value: 'b' }, + ], + default: 'a', + label: 'c', + caption: 'd', + }); + await onChange!('b'); + expect(outputs).toStrictEqual([values.STR('b')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:select({}, 'id') + `); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(onChange).toBeTypeOf('function'); + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [], + default: undefined, + label: undefined, + caption: undefined, + }); + }); + + test.concurrent('omit item values', async () => { + const { get } = await exe(` + let select = Ui:C:select({ + items: [ + { text: 'A' } + { text: 'B' } + ] + }, 'id') + `); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(onChange).toBeTypeOf('function'); + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [ + { text: 'A', value: 'A' }, + { text: 'B', value: 'B' }, + ], + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('folder', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let folder = Ui:C:folder({ + children: [] + title: 'a' + opened: true + }, 'id') + Ui:render([folder]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'folder', + id: 'id', + children: [], + title: 'a', + opened: true, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:folder({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'folder', + id: 'id', + children: [], + title: '', + opened: true, + }); + }); + + test.concurrent('some children', async () => { + const { get } = await exe(` + let text = Ui:C:text({ + text: 'text' + }, 'id1') + Ui:C:folder({ + children: [text] + }, 'id2') + `); + expect(get('id2')).toStrictEqual({ + type: 'folder', + id: 'id2', + children: ['id1'], + title: '', + opened: true, + }); + }); + }); + + describe('postFormButton', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let post_form_button = Ui:C:postFormButton({ + text: 'a' + primary: true + rounded: false + form: { + text: 'b' + cw: 'c' + visibility: 'public' + localOnly: true + } + }, 'id') + Ui:render([post_form_button]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'postFormButton', + id: 'id', + text: 'a', + primary: true, + rounded: false, + form: { + text: 'b', + cw: 'c', + visibility: 'public', + localOnly: true, + }, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:postFormButton({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postFormButton', + id: 'id', + text: undefined, + primary: undefined, + rounded: undefined, + form: { text: '' }, + }); + }); + }); + + describe('postForm', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let post_form = Ui:C:postForm({ + form: { + text: 'a' + cw: 'b' + visibility: 'public' + localOnly: true + } + }, 'id') + Ui:render([post_form]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { + text: 'a', + cw: 'b', + visibility: 'public', + localOnly: true, + }, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:postForm({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { text: '' }, + }); + }); + + test.concurrent('minimum options for form', async () => { + const { get } = await exe(` + Ui:C:postForm({ + form: { text: '' } + }, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { + text: '', + cw: undefined, + visibility: undefined, + localOnly: undefined, + }, + }); + }); + }); +}); From 79b851fe562c3e6be601b5b25a744d86798d4747 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, 7 Jan 2025 22:38:43 +0900 Subject: [PATCH 17/44] =?UTF-8?q?Update=20CHANGELOG.md=20(=E6=9B=B8?= =?UTF-8?q?=E3=81=8D=E6=96=B9=E3=82=92=E3=81=9D=E3=82=8D=E3=81=88=E3=82=8B?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10de8b5fc57a..4e340490118d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) - Enhance: 照会に失敗した場合、その理由を表示するように - Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加 +- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 - Fix: サーバー情報メニューに区切り線が不足していたのを修正 - Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 @@ -20,7 +21,6 @@ (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 -- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように From d7835313c35be8565d2cfb1a7cc724f95ddf3db7 Mon Sep 17 00:00:00 2001 From: taichan <40626578+tai-cha@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:33:08 +0900 Subject: [PATCH 18/44] =?UTF-8?q?fix(backend):=20=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=83=80=E3=82=A6=E3=83=B3=E3=81=95=E3=82=8C=E3=81=9F?= =?UTF-8?q?=E6=9C=9F=E9=96=93=E6=8C=87=E5=AE=9A=E3=81=AE=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=8CStreaming=E7=B5=8C=E7=94=B1=E3=81=A7LTL?= =?UTF-8?q?=E3=81=AB=E5=87=BA=E7=8F=BE=E3=81=99=E3=82=8B=E3=81=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#15200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): skipHideなときにもロックダウンされたノートのprivate化をするように * fix linting * Update packages/backend/src/core/entities/NoteEntityService.ts * Fix: type error * Remove unneeded await * Fix: typo * Remove skipTreatVisibillity --- packages/backend/src/core/entities/NoteEntityService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 96cc6b028ec0..97f1c3d739e3 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -102,8 +102,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { - // FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある) + private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] { if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore; if ((followersOnlyBefore != null) @@ -115,7 +114,11 @@ export class NoteEntityService implements OnModuleInit { packedNote.visibility = 'followers'; } } + return packedNote.visibility; + } + @bindThis + private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { if (meId === packedNote.userId) return; // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) @@ -458,6 +461,8 @@ export class NoteEntityService implements OnModuleInit { } : {}), }); + this.treatVisibility(packed); + if (!opts.skipHide) { await this.hideNote(packed, meId); } From f6808711af282ad62ff2f7adedf96431586ca077 Mon Sep 17 00:00:00 2001 From: taichan <40626578+tai-cha@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:52:08 +0900 Subject: [PATCH 19/44] update changelog (#15236) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e340490118d..174f1282cbf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) +- Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 ) - Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) ## 2024.11.0 From 8652ce7cc030cf5d21b86da9cee453dd82667978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B4=87=E5=B3=B0=20=E6=9C=94=E8=8F=AF?= <160555157+sakuhanight@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:58:29 +0900 Subject: [PATCH 20/44] =?UTF-8?q?fix(frontend):=20=E8=87=AA=E5=88=86?= =?UTF-8?q?=E4=BB=A5=E5=A4=96=E3=81=AE=E3=83=8E=E3=83=BC=E3=83=88=E3=82=92?= =?UTF-8?q?=E6=B6=88=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AB=E5=AE=9F?= =?UTF-8?q?=E7=B8=BE=E3=82=92=E8=A7=A3=E9=99=A4=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3=20(#15071)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/scripts/get-note-menu.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index c1846b058957..e131cf515608 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -5,19 +5,19 @@ import { defineAsyncComponent, Ref, ShallowRef } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; import { claimAchievement } from './achievements.js'; +import type { MenuItem } from '@/types/menu.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@@/js/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import type { 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'; @@ -194,7 +194,7 @@ export function getNoteMenu(props: { noteId: appearNote.id, }); - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { claimAchievement('noteDeletedWithin1min'); } }); @@ -213,7 +213,7 @@ export function getNoteMenu(props: { os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { claimAchievement('noteDeletedWithin1min'); } }); From c49a13de65a678e56a6350c6a8562d73a7caa282 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, 8 Jan 2025 19:33:43 +0900 Subject: [PATCH 21/44] =?UTF-8?q?fix(frontend-embed):=20locale=E3=81=AE?= =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=8C=E6=8A=9C=E3=81=91=E3=81=A6=E3=81=8A?= =?UTF-8?q?=E3=82=8A=E8=B5=B7=E5=8B=95=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C=E3=81=82=E3=82=8B=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#15212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend-embed): localeのバージョンチェックが抜けており起動に失敗することがある問題を修正 * Update Changelog --- CHANGELOG.md | 1 + packages/frontend-embed/src/boot.ts | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 174f1282cbf1..24b5f6e661e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 +- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正 ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 8ab4ab32e6f8..c1b2b58bebdf 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -17,11 +17,11 @@ import { applyTheme, assertIsTheme } from '@/theme.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { DI } from '@/di.js'; import { serverMetadata } from '@/server-metadata.js'; -import { url } from '@@/js/config.js'; +import { url, version, locale, lang, updateLocale } 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 { i18n } from '@/i18n.js'; +import { i18n, updateI18n } from '@/i18n.js'; import type { Theme } from '@/theme.js'; @@ -71,6 +71,22 @@ if (embedParams.colorMode === 'dark') { } //#endregion +//#region Detect language & fetch translations +const localeVersion = localStorage.getItem('localeVersion'); +const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); +if (localeOutdated) { + const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + if (res.status === 200) { + const newLocale = await res.text(); + const parsedNewLocale = JSON.parse(newLocale); + localStorage.setItem('locale', newLocale); + localStorage.setItem('localeVersion', version); + updateLocale(parsedNewLocale); + updateI18n(parsedNewLocale); + } +} +//#endregion + // サイズの制限 document.documentElement.style.maxWidth = '500px'; From 55713fcd657983add090a7788c6ffd984cbbd15f 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, 8 Jan 2025 19:35:09 +0900 Subject: [PATCH 22/44] =?UTF-8?q?fix(backend):=20apOrHtml=20Constraint?= =?UTF-8?q?=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F=E8=A9=95=E4=BE=A1=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(#15213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend/ActivityPubServerService): apOrHtml Constraintが正しく評価されない問題を修正 (MisskeyIO#869) * Update Changelog * indent --------- Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- CHANGELOG.md | 2 ++ packages/backend/src/server/ActivityPubServerService.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b5f6e661e1..fd8eaf00e8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) - Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 ) - Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) +- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正 + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869) ## 2024.11.0 diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index f34f6583d359..8c4b13a40a88 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -519,8 +519,8 @@ export class ActivityPubServerService { }, deriveConstraint(request: IncomingMessage) { const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]); - const isAp = typeof accepted === 'string' && !accepted.match(/html/); - return isAp ? 'ap' : 'html'; + if (accepted === false) return null; + return accepted !== 'html' ? 'ap' : 'html'; }, }); From bb4457266dc6f51831bf54b5071dd84928366f4f Mon Sep 17 00:00:00 2001 From: Rsplwe Date: Wed, 8 Jan 2025 18:51:23 +0800 Subject: [PATCH 23/44] feat(frontend): Do not display blocked instances on the welcome page (#15178) --- packages/frontend/src/pages/welcome.entrance.a.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index f0e4a852c971..68938f0bbf7b 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -59,6 +59,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string misskeyApiGet('federation/instances', { sort: '+pubSub', limit: 20, + blocked: 'false', }).then(_instances => { instances.value = _instances; }); From 13439e04c426d48c1700b0fd476dcf453965318a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:00:02 +0900 Subject: [PATCH 24/44] =?UTF-8?q?fix(frontend-embed):=20=E5=9E=8B=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#15216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): frontend / frontend-embedにあるtsconfig.jsonのmoduleをES2022にする * fixed errors * fixed errors * fixed errors * fix(frontend-embed): 型チェックエラーを修正 --- .../frontend-embed/src/components/EmMfm.ts | 2 -- .../frontend-embed/src/components/EmNotes.vue | 3 ++- packages/frontend-embed/src/theme.ts | 25 +++++++++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts index cae2feb8fb28..e84fd6f679e0 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -415,8 +415,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext
- +
@@ -24,6 +24,7 @@ import { useTemplateRef } from 'vue'; import EmNote from '@/components/EmNote.vue'; import EmPagination, { Paging } from '@/components/EmPagination.vue'; import { i18n } from '@/i18n.js'; +import * as Misskey from 'misskey-js'; withDefaults(defineProps<{ pagination: Paging; diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts index 4664ad48804e..680ab80167ec 100644 --- a/packages/frontend-embed/src/theme.ts +++ b/packages/frontend-embed/src/theme.ts @@ -75,16 +75,21 @@ function compile(theme: Theme): Record { 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); + const funcTxt = parts.shift(); + const argTxt = parts.shift(); + + if (funcTxt && argTxt) { + const func = funcTxt.substring(1); + const arg = parseFloat(argTxt); + 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); + } } } From c4192e81ed30292a06b85e7b00578ca87c97d076 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: Sat, 11 Jan 2025 22:43:42 +0900 Subject: [PATCH 25/44] =?UTF-8?q?enhance(backend):=20=E3=83=81=E3=83=A3?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92=E4=B8=80?= =?UTF-8?q?=E3=81=A4=E3=81=9A=E3=81=A4=E8=A1=8C=E3=81=86=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=81=A7DB=E3=81=AE=E5=90=8C=E6=99=82=E6=8E=A5=E7=B6=9A?= =?UTF-8?q?=E3=81=A8=E3=82=BF=E3=82=A4=E3=83=A0=E3=82=A2=E3=82=A6=E3=83=88?= =?UTF-8?q?=E3=82=92=E5=89=8A=E6=B8=9B=20(#15239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sync charts one-at-a-time to reduce database contention and timeouts * fix merge resolve failure * Update Changelog * update changelog * add comments --------- Co-authored-by: Hazelnoot --- CHANGELOG.md | 2 ++ .../src/core/chart/ChartManagementService.ts | 10 +++---- .../processors/CleanChartsProcessorService.ts | 27 +++++++++---------- .../ResyncChartsProcessorService.ts | 9 +++---- .../processors/TickChartsProcessorService.ts | 27 +++++++++---------- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8eaf00e8d7..75dd37bc24ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように +- Enhance: チャート更新時にDBに同時接続しないように + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830) - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 79681370a17d..f04c561063d1 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -58,9 +58,9 @@ export class ChartManagementService implements OnApplicationShutdown { @bindThis public async start() { // 20分おきにメモリ情報をDBに書き込み - this.saveIntervalId = setInterval(() => { + this.saveIntervalId = setInterval(async () => { for (const chart of this.charts) { - chart.save(); + await chart.save(); } }, 1000 * 60 * 20); } @@ -69,9 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown { public async dispose(): Promise { clearInterval(this.saveIntervalId); if (process.env.NODE_ENV !== 'test') { - await Promise.all( - this.charts.map(chart => chart.save()), - ); + for (const chart of this.charts) { + await chart.save(); + } } } diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 110468801ce2..8c5faa8d0711 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -48,20 +48,19 @@ export class CleanChartsProcessorService { public async process(): Promise { this.logger.info('Clean charts...'); - await Promise.all([ - this.federationChart.clean(), - this.notesChart.clean(), - this.usersChart.clean(), - this.activeUsersChart.clean(), - this.instanceChart.clean(), - this.perUserNotesChart.clean(), - this.perUserPvChart.clean(), - this.driveChart.clean(), - this.perUserReactionsChart.clean(), - this.perUserFollowingChart.clean(), - this.perUserDriveChart.clean(), - this.apRequestChart.clean(), - ]); + // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する + await this.federationChart.clean(); + await this.notesChart.clean(); + await this.usersChart.clean(); + await this.activeUsersChart.clean(); + await this.instanceChart.clean(); + await this.perUserNotesChart.clean(); + await this.perUserPvChart.clean(); + await this.driveChart.clean(); + await this.perUserReactionsChart.clean(); + await this.perUserFollowingChart.clean(); + await this.perUserDriveChart.clean(); + await this.apRequestChart.clean(); this.logger.succ('All charts successfully cleaned.'); } diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 570cdf9a7504..0c47fdedb3b0 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -29,13 +29,12 @@ export class ResyncChartsProcessorService { public async process(): Promise { this.logger.info('Resync charts...'); + // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する // TODO: ユーザーごとのチャートも更新する // TODO: インスタンスごとのチャートも更新する - await Promise.all([ - this.driveChart.resync(), - this.notesChart.resync(), - this.usersChart.resync(), - ]); + await this.driveChart.resync(); + await this.notesChart.resync(); + await this.usersChart.resync(); this.logger.succ('All charts successfully resynced.'); } diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 93ec34162db4..fc8856a2710e 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -48,20 +48,19 @@ export class TickChartsProcessorService { public async process(): Promise { this.logger.info('Tick charts...'); - await Promise.all([ - this.federationChart.tick(false), - this.notesChart.tick(false), - this.usersChart.tick(false), - this.activeUsersChart.tick(false), - this.instanceChart.tick(false), - this.perUserNotesChart.tick(false), - this.perUserPvChart.tick(false), - this.driveChart.tick(false), - this.perUserReactionsChart.tick(false), - this.perUserFollowingChart.tick(false), - this.perUserDriveChart.tick(false), - this.apRequestChart.tick(false), - ]); + // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する + await this.federationChart.tick(false); + await this.notesChart.tick(false); + await this.usersChart.tick(false); + await this.activeUsersChart.tick(false); + await this.instanceChart.tick(false); + await this.perUserNotesChart.tick(false); + await this.perUserPvChart.tick(false); + await this.driveChart.tick(false); + await this.perUserReactionsChart.tick(false); + await this.perUserFollowingChart.tick(false); + await this.perUserDriveChart.tick(false); + await this.apRequestChart.tick(false); this.logger.succ('All charts successfully ticked.'); } From d60c307c4e4c3eaba2a40b46ba41c4d684d5d370 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: Sat, 11 Jan 2025 22:47:15 +0900 Subject: [PATCH 26/44] =?UTF-8?q?refactor/deps(frontend):=20shiki=E3=81=AE?= =?UTF-8?q?deprecated=E8=A1=A8=E7=8F=BE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#151?= =?UTF-8?q?69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): shikiのdeprecated表現を修正 * update aiscript-vscode * :v: * fix * remove unused imports * bump aiscript-vscode to 0.1.15 --- packages/frontend/package.json | 2 +- packages/frontend/src/scripts/code-highlighter.ts | 6 +++--- packages/frontend/src/scripts/merge.ts | 8 ++++---- pnpm-lock.yaml | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 4f132ab50011..843e4373caa9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -29,7 +29,7 @@ "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.2.0", "@vue/compiler-sfc": "3.5.12", - "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", + "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "astring": "1.9.0", "broadcast-channel": "7.0.0", "buraha": "0.0.1", diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 6710d9826e8d..4d57dcd9444d 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore } from 'shiki/core'; +import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; @@ -60,8 +61,6 @@ export async function getHighlighter(): Promise { } async function initHighlighter() { - await loadWasm(import('shiki/onig.wasm?init')); - // テーマの重複を消す const themes = unique([ darkPlus, @@ -70,6 +69,7 @@ async function initHighlighter() { const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); const highlighter = await createHighlighterCore({ + engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')), themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 9794a300da02..004b6d42a4cb 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -7,10 +7,10 @@ import { deepClone } from './clone.js'; import type { Cloneable } from './clone.js'; export type DeepPartial = { - [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; + [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; }; -function isPureObject(value: unknown): value is Record { +function isPureObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -18,14 +18,14 @@ function isPureObject(value: unknown): value is Record>(value: DeepPartial, def: X): X { +export function deepMerge>(value: DeepPartial, def: X): X { if (isPureObject(value) && isPureObject(def)) { const result = deepClone(value as Cloneable) as X; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { result[k] = v; } else if (isPureObject(v) && isPureObject(result[k])) { - const child = deepClone(result[k] as Cloneable) as DeepPartial>; + const child = deepClone(result[k] as Cloneable) as DeepPartial>; result[k] = deepMerge(child, v); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f50c635bf067..d9464ba600bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -728,8 +728,8 @@ importers: specifier: 3.5.12 version: 3.5.12 aiscript-vscode: - specifier: github:aiscript-dev/aiscript-vscode#v0.1.11 - version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9 + specifier: github:aiscript-dev/aiscript-vscode#v0.1.15 + version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7 astring: specifier: 1.9.0 version: 1.9.0 @@ -4938,9 +4938,9 @@ packages: resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} engines: {node: '>=18'} - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: - resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9} - version: 0.1.11 + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7: + resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7} + version: 0.1.15 engines: {vscode: ^1.83.0} ajv-draft-04@1.0.0: @@ -15664,7 +15664,7 @@ snapshots: clean-stack: 5.2.0 indent-string: 5.0.0 - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7: dependencies: '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz vscode-languageclient: 9.0.1 From d86c77260e59cc684d8bf1b993e3aaf75e90eb2b 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, 14 Jan 2025 09:13:57 +0900 Subject: [PATCH 27/44] =?UTF-8?q?fix(frontend):=20RSS=E3=82=A6=E3=82=A3?= =?UTF-8?q?=E3=82=B8=E3=82=A7=E3=83=83=E3=83=88=E3=81=A7URL=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E4=BA=8C=E9=87=8D?= =?UTF-8?q?=E3=81=AB=E8=A1=8C=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#15272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * don't double-url-encode rss urls `url.searchParams.set()` already encodes the values passed! (this is a partial revert of 0472d43ee97f1ac0fd13969b2111d67b322a947f, the change in `statusbar-rss.vue` was correct) * Update Changelog --------- Co-authored-by: dakkar --- CHANGELOG.md | 2 ++ packages/frontend/src/widgets/WidgetRss.vue | 2 +- packages/frontend/src/widgets/WidgetRssTicker.vue | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75dd37bc24ab..0c63226c587c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 - Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正 +- Fix: RSSウィジェットが正しく表示されない問題を修正 + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857) ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 92dc6d148e29..3e4368770924 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries)); const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); - url.searchParams.set('url', encodeURIComponent(widgetProps.url)); + url.searchParams.set('url', widgetProps.url); return url; }); const intervalClear = ref<(() => void) | undefined>(); diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 6957878572cd..4f594b720f85 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -99,7 +99,7 @@ const items = computed(() => { const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); - url.searchParams.set('url', encodeURIComponent(widgetProps.url)); + url.searchParams.set('url', widgetProps.url); return url; }); const intervalClear = ref<(() => void) | undefined>(); From b161601863662e3c40571fd9958d4b43a202974a 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, 14 Jan 2025 09:35:19 +0900 Subject: [PATCH 28/44] =?UTF-8?q?fix(frontend):=20=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=88=E5=BF=98=E3=82=8C=20(#1527?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/settings/mute-block.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 797f0ec106a1..19dd2468f88b 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -144,6 +144,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { infoImageUrl } from '@/instance.js'; import { signinRequired } from '@/account.js'; +import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; const $i = signinRequired(); From 6820878676f5f531bc596076ed5c5ea022051631 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 14 Jan 2025 19:36:35 +0900 Subject: [PATCH 29/44] fix: unable to use AiService on arm64 (#15261) --- CHANGELOG.md | 1 + packages/backend/src/core/AiService.ts | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c63226c587c..b9f0d6495ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) - Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869) +- Fix: AIセンシティブ判定が arm64 環境で動作しない問題を修正 ## 2024.11.0 diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index ad852fdd6e6e..33ddabb5e03c 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -15,7 +15,7 @@ import { bindThis } from '@/decorators.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); -const REQUIRED_CPU_FLAGS = ['avx2', 'fma']; +const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma']; let isSupportedCpu: undefined | boolean = undefined; @Injectable() @@ -31,8 +31,7 @@ export class AiService { public async detectSensitive(path: string): Promise { try { if (isSupportedCpu === undefined) { - const cpuFlags = await this.getCpuFlags(); - isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); + isSupportedCpu = await this.computeIsSupportedCpu(); } if (!isSupportedCpu) { @@ -64,6 +63,22 @@ export class AiService { } } + private async computeIsSupportedCpu(): Promise { + switch (process.arch) { + case 'x64': { + const cpuFlags = await this.getCpuFlags(); + return REQUIRED_CPU_FLAGS_X64.every(required => cpuFlags.includes(required)); + } + case 'arm64': { + // As far as I know, no required CPU flags for ARM64. + return true; + } + default: { + return false; + } + } + } + @bindThis private async getCpuFlags(): Promise { const str = await si.cpuFlags(); From 759b9f4cf133dfa6b06c6fcebf22aefc017363bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:37:41 +0900 Subject: [PATCH 30/44] =?UTF-8?q?feat(backend):=20config(default.yml)?= =?UTF-8?q?=E3=81=8B=E3=82=89SQL=E3=83=AD=E3=82=B0=E5=85=A8=E6=96=87?= =?UTF-8?q?=E3=82=92=E5=87=BA=E5=8A=9B=E3=81=99=E3=82=8B=E3=81=8B=E5=90=A6?= =?UTF-8?q?=E3=81=8B=E3=82=92=E8=A8=AD=E5=AE=9A=E5=8F=AF=E8=83=BD=E3=81=AB?= =?UTF-8?q?=20(#15268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature(backend): config(default.yml)からSQLログ全文を出力するか否かを設定可能に * disableHighlightやめる * refactor --- .config/docker_example.yml | 10 ++++++ .config/example.yml | 10 ++++++ CHANGELOG.md | 1 + packages/backend/src/config.ts | 14 ++++++++ packages/backend/src/postgres.ts | 59 +++++++++++++++++++++++++++----- 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 3f8e5734ce87..0f5ba9696d22 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -219,3 +219,13 @@ signToActivityPubGet: true # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# Log settings +# logging: +# sql: +# # Outputs query parameters during SQL execution to the log. +# # default: false +# enableQueryParamLogging: false +# # Disable query truncation. If set to true, the full text of the query will be output to the log. +# # default: false +# disableQueryTruncation: false diff --git a/.config/example.yml b/.config/example.yml index 60a6a0aa71b7..ea29cedd10e5 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -321,3 +321,13 @@ signToActivityPubGet: true # PID File of master process #pidFile: /tmp/misskey.pid + +# Log settings +# logging: +# sql: +# # Outputs query parameters during SQL execution to the log. +# # default: false +# enableQueryParamLogging: false +# # Disable query truncation. If set to true, the full text of the query will be output to the log. +# # default: false +# disableQueryTruncation: false diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f0d6495ae1..071cf22a5a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように - Enhance: チャート更新時にDBに同時接続しないように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830) +- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 ) - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 42f1033b9d0a..a5dc61db137f 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -99,6 +99,13 @@ type Source = { perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; pidFile: string; + + logging?: { + sql?: { + disableQueryTruncation? : boolean, + enableQueryParamLogging? : boolean, + } + } }; export type Config = { @@ -151,6 +158,12 @@ export type Config = { inboxJobMaxAttempts: number | undefined; proxyRemoteFiles: boolean | undefined; signToActivityPubGet: boolean | undefined; + logging?: { + sql?: { + disableQueryTruncation? : boolean, + enableQueryParamLogging? : boolean, + } + } version: string; publishTarballInsteadOfProvideRepositoryUrl: boolean; @@ -293,6 +306,7 @@ export function loadConfig(): Config { perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), pidFile: config.pidFile, + logging: config.logging, }; } diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 251a03c303a7..d09240eba1c9 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -89,27 +89,65 @@ export const dbLogger = new MisskeyLogger('db'); const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); +export type LoggerProps = { + disableQueryTruncation?: boolean; + enableQueryParamLogging?: boolean; +} + +function highlightSql(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); +} + +function truncateSql(sql: string) { + return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql; +} + +function stringifyParameter(param: any) { + if (param instanceof Date) { + return param.toISOString(); + } else { + return param; + } +} + class MyCustomLogger implements Logger { + constructor(private props: LoggerProps = {}) { + } + + @bindThis + private transformQueryLog(sql: string) { + let modded = sql; + if (!this.props.disableQueryTruncation) { + modded = truncateSql(modded); + } + + return highlightSql(modded); + } + @bindThis - private highlight(sql: string) { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); + private transformParameters(parameters?: any[]) { + if (this.props.enableQueryParamLogging && parameters && parameters.length > 0) { + return parameters.map(stringifyParameter); + } + + return undefined; } @bindThis public logQuery(query: string, parameters?: any[]) { - sqlLogger.info(this.highlight(query).substring(0, 100)); + sqlLogger.info(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis public logQueryError(error: string, query: string, parameters?: any[]) { - sqlLogger.error(this.highlight(query)); + sqlLogger.error(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis public logQuerySlow(time: number, query: string, parameters?: any[]) { - sqlLogger.warn(this.highlight(query)); + sqlLogger.warn(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis @@ -247,7 +285,12 @@ export function createPostgresDataSource(config: Config) { }, } : false, logging: log, - logger: log ? new MyCustomLogger() : undefined, + logger: log + ? new MyCustomLogger({ + disableQueryTruncation: config.logging?.sql?.disableQueryTruncation, + enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging, + }) + : undefined, maxQueryExecutionTime: 300, entities: entities, migrations: ['../../migration/*.js'], From d082a1dd34fa84a0a6c5afa99a984437d9d1e686 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, 14 Jan 2025 19:40:14 +0900 Subject: [PATCH 31/44] =?UTF-8?q?fix(frontend/dev):=20=E3=83=90=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=83=B3=E3=83=89=E7=B5=8C=E7=94=B1=E3=81=A7?= =?UTF-8?q?=E3=81=AE=E9=96=8B=E7=99=BA=E6=99=82=E3=81=ABHMR=E3=81=8C?= =?UTF-8?q?=E5=8A=B9=E3=81=8B=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#15255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend-embed/vite.config.ts | 6 ++++++ packages/frontend/vite.config.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index 2dbee488c5f0..151d3161906c 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -63,6 +63,12 @@ export function getConfig(): UserConfig { server: { port: 5174, + hmr: { + // バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される + // そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない + // クライアント側のWSポートをViteサーバーのポートに強制させることで、正しくHMRが機能するようになる + clientPort: 5174, + }, }, plugins: [ diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 504562a91e9f..3c4b19a57178 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -65,6 +65,12 @@ export function getConfig(): UserConfig { server: { port: 5173, + hmr: { + // バックエンド経由での起動時、Viteは5173経由でアセットを参照していると思い込んでいるが実際は3000から配信される + // そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない + // クライアント側のWSポートをViteサーバーのポートに強制させることで、正しくHMRが機能するようになる + clientPort: 5173, + }, headers: { // なんか効かない 'X-Frame-Options': 'DENY', }, From dd6743dda44abd27740421b47e27604a02f55c2c Mon Sep 17 00:00:00 2001 From: taichan <40626578+tai-cha@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:46:57 +0900 Subject: [PATCH 32/44] =?UTF-8?q?Fix(frontend):=20=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=97=E3=81=A6=E7=B7=A8=E9=9B=86=E3=81=A7=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E3=81=82=E3=82=8A=E3=82=92=E6=B6=88=E3=81=9B=E3=81=AA=E3=81=84?= =?UTF-8?q?=20(#15249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix(frontend): 削除して編集で引用ありを消せない * docs(changelog): update CHANGELOG.md * rename noteToRenote -> renoteTargetNote with type fix * Update Changelog --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + .../frontend/src/components/MkPostForm.vue | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 071cf22a5a11..f5fd73923281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 - Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正 +- Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 ) - Fix: RSSウィジェットが正しく表示されない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index ba988ee71512..07b6869bf6d7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -46,14 +46,14 @@ SPDX-License-Identifier: AGPL-3.0-only - + - -
{{ i18n.ts.quoteAttached }}
+ +
{{ i18n.ts.quoteAttached }}
{{ i18n.ts.recipient }}
@@ -100,12 +100,13 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 50002f198332..3ff8fdc844d1 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -136,6 +136,12 @@ type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations // @public (undocumented) type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; @@ -1261,6 +1267,8 @@ declare namespace entities { AdminAvatarDecorationsListRequest, AdminAvatarDecorationsListResponse, AdminAvatarDecorationsUpdateRequest, + AdminCaptchaCurrentResponse, + AdminCaptchaSaveRequest, AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 1837f3db4f39..3bcdae6a4ab3 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -250,6 +250,28 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:meta* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:meta* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index cb1f4dbe96b5..b016d5bbcf49 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -36,6 +36,8 @@ import type { AdminAvatarDecorationsListRequest, AdminAvatarDecorationsListResponse, AdminAvatarDecorationsUpdateRequest, + AdminCaptchaCurrentResponse, + AdminCaptchaSaveRequest, AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, @@ -604,6 +606,8 @@ export type Endpoints = { 'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse }; 'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse }; 'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse }; + 'admin/captcha/current': { req: EmptyRequest; res: AdminCaptchaCurrentResponse }; + 'admin/captcha/save': { req: AdminCaptchaSaveRequest; res: EmptyResponse }; 'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse }; 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse }; 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a8f474c25c25..02be4848c72e 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -39,6 +39,8 @@ export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-dec export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json']; export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; +export type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json']; +export type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json']; export type AdminDeleteAllFilesOfAUserRequest = operations['admin___delete-all-files-of-a-user']['requestBody']['content']['application/json']; export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json']; export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 42ca05e05761..e6a9df3f5a16 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -215,6 +215,24 @@ export type paths = { */ post: operations['admin___avatar-decorations___update']; }; + '/admin/captcha/current': { + /** + * admin/captcha/current + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:meta* + */ + post: operations['admin___captcha___current']; + }; + '/admin/captcha/save': { + /** + * admin/captcha/save + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:meta* + */ + post: operations['admin___captcha___save']; + }; '/admin/delete-all-files-of-a-user': { /** * admin/delete-all-files-of-a-user @@ -6564,6 +6582,128 @@ export type operations = { }; }; }; + /** + * admin/captcha/current + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:meta* + */ + admin___captcha___current: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** @enum {string} */ + provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha'; + hcaptcha: { + siteKey: string | null; + secretKey: string | null; + }; + mcaptcha: { + siteKey: string | null; + secretKey: string | null; + instanceUrl: string | null; + }; + recaptcha: { + siteKey: string | null; + secretKey: string | null; + }; + turnstile: { + siteKey: string | null; + secretKey: string | null; + }; + }; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/captcha/save + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:meta* + */ + admin___captcha___save: { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha'; + captchaResult?: string | null; + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/delete-all-files-of-a-user * @description No description provided. From 5445b023e5cedb7228710637c895c63328e3db74 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, 14 Jan 2025 20:08:54 +0900 Subject: [PATCH 35/44] =?UTF-8?q?enhance:=20=E9=80=A3=E5=90=88=E3=83=A2?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AB=E3=81=82=E3=82=8F=E3=81=9B=E3=81=A6?= =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=B3=E3=83=88=E3=82=A8=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=82=92=E5=A4=89=E5=8C=96=E3=81=95=E3=81=9B=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=20(#15112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(backend): metaにfederation modeに関する情報を公開 * enhance(frontend): 登録画面の注意書きを追加 * enhance(frontend): aboutページ・サーバー情報 * enhance(frontend): サーバー統計 * enhance(frontend): みつけるページ * enhance(frontend): 検索 * enhance(frontend): ユーザー選択 * enhance(frontend): 設定画面 * enhance(frontend): ウィジェット * enhance(frontend): リモートで開くオプション * Update Changelog * enhance(frontend): ステータスバー * i18n --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + locales/index.d.ts | 8 ++++ locales/ja-JP.yml | 2 + .../src/core/entities/MetaEntityService.ts | 1 + .../backend/src/models/json-schema/meta.ts | 5 ++ .../src/components/MkInstanceStats.vue | 18 ++++--- .../src/components/MkSignupDialog.rules.vue | 6 ++- .../src/components/MkUserSelectDialog.vue | 11 +++-- .../src/components/MkVisitorDashboard.vue | 6 ++- .../frontend/src/components/MkWidgets.vue | 19 ++++++-- packages/frontend/src/pages/about.vue | 47 ++++++++++++------- packages/frontend/src/pages/explore.users.vue | 3 +- packages/frontend/src/pages/search.note.vue | 22 +++++---- packages/frontend/src/pages/search.user.vue | 5 +- .../frontend/src/pages/settings/general.vue | 3 +- .../src/pages/settings/mute-block.vue | 4 +- .../frontend/src/pages/settings/privacy.vue | 9 ++-- .../pages/settings/statusbar.statusbar.vue | 3 +- packages/frontend/src/scripts/please-login.ts | 12 ++++- packages/frontend/src/ui/_common_/common.ts | 18 ++++--- .../frontend/src/ui/_common_/statusbars.vue | 3 +- packages/frontend/src/widgets/index.ts | 10 +++- packages/misskey-js/src/autogen/types.ts | 2 + 23 files changed, 150 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af5d333927d6..287d3904534c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Enhance: PC画面でチャンネルが複数列で表示されるように (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) - Enhance: 照会に失敗した場合、その理由を表示するように +- Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正 - Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加 - Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 7c3ef5d93cd6..453d40feea3a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5230,6 +5230,14 @@ export interface Locale extends ILocale { * 注意事項を理解した上でオンにします。 */ "acknowledgeNotesAndEnable": string; + /** + * このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。 + */ + "federationSpecified": string; + /** + * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。 + */ + "federationDisabled": string; "_accountSettings": { /** * コンテンツの表示にログインを必須にする diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 57a88062c1d3..a3cb9d052a25 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1303,6 +1303,8 @@ lockdown: "ロックダウン" pleaseSelectAccount: "アカウントを選択してください" availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" +federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" +federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 409dca34263b..ec0b5360f4a2 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -132,6 +132,7 @@ export class MetaEntityService { enableUrlPreview: instance.urlPreviewEnabled, noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', maxFileSize: this.config.maxFileSize, + federation: this.meta.federation, }; return packed; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index e3fd63464a81..e7ae2ee8e560 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -261,6 +261,11 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + federation: { + type: 'string', + enum: ['all', 'specified', 'none'], + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 8ccbf61e48f3..d8066857fe17 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -46,9 +46,9 @@ SPDX-License-Identifier: AGPL-3.0-only - - - + + +
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -84,13 +84,15 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -101,4 +111,8 @@ const isThumbnailAvailable = computed(() => { font-size: 32px; color: #777; } + +.large .icon { + font-size: 40px; +} diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue new file mode 100644 index 000000000000..520421bfb7a4 --- /dev/null +++ b/packages/frontend/src/components/MkNoteMediaGrid.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue new file mode 100644 index 000000000000..b6c7c1c777d8 --- /dev/null +++ b/packages/frontend/src/pages/user/files.vue @@ -0,0 +1,56 @@ + + + + + + + diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 2794db2821ab..a6a49f0ab9b7 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.userPagePinTip }}