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 01/12] =?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 f7ba1afc52..f065ed307f 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 25375c3015..da585ad68d 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 4bc5c799cf..2b181af675 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 02/12] =?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 f8770f33f2..4e681393c6 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 c0ec950181..f50c635bf0 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 03/12] =?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 45f933dc28..60164a7e3d 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 4e5ca7f559..68b6651a56 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 01a3dbbb30..50002f1983 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 6e34ec1508..fec6114cca 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 04/12] =?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 f065ed307f..10de8b5fc5 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 8afe88eec6..e203c51bba 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 0000000000..de6fa1d633 --- /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 2b386bebb8..ca92b27ff5 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 e7a92e2d5c..dc07ad477b 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 0000000000..2a15a74249 --- /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 0000000000..acc48826ea --- /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 0000000000..5f77edbb49 --- /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 05/12] =?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 10de8b5fc5..4e34049011 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 06/12] =?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 96cc6b028e..97f1c3d739 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 07/12] update changelog (#15236) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e34049011..174f1282cb 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 08/12] =?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 c1846b0589..e131cf5156 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 09/12] =?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 174f1282cb..24b5f6e661 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 8ab4ab32e6..c1b2b58beb 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 10/12] =?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 24b5f6e661..fd8eaf00e8 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 f34f6583d3..8c4b13a40a 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 11/12] 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 f0e4a852c9..68938f0bbf 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 12/12] =?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 cae2feb8fb..e84fd6f679 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 4664ad4880..680ab80167 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); + } } }