summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-03-14 16:28:56 +0000
committerdakkar <dakkar@thenautilus.net>2024-03-14 16:28:56 +0000
commit9478fc0095806ecf4a7535d61d5bbbb607122b41 (patch)
tree99e7166e05e780e189f6a1762f101b196ed38c92
parentcopy changes to SkNote* (diff)
parentfix(frontend): update locales/index.d.ts (diff)
downloadsharkey-9478fc0095806ecf4a7535d61d5bbbb607122b41.tar.gz
sharkey-9478fc0095806ecf4a7535d61d5bbbb607122b41.tar.bz2
sharkey-9478fc0095806ecf4a7535d61d5bbbb607122b41.zip
Merge remote-tracking branch 'misskey/develop' into future-2024-03-14
-rw-r--r--.config/example.yml2
-rw-r--r--.devcontainer/devcontainer.json1
-rw-r--r--.github/workflows/check-spdx-license-id.yml75
-rw-r--r--.vscode/extensions.json2
-rw-r--r--.vscode/settings.json2
-rw-r--r--CHANGELOG.md5
-rw-r--r--cypress/e2e/basic.cy.js5
-rw-r--r--cypress/e2e/router.cy.js5
-rw-r--r--cypress/e2e/widgets.cy.js5
-rw-r--r--locales/index.d.ts4
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/backend/generate_api_json.js7
-rw-r--r--packages/backend/migration/1689325027964-UserBlacklistAnntena.js5
-rw-r--r--packages/backend/migration/1690417561185-fix-renote-muting.js5
-rw-r--r--packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js5
-rw-r--r--packages/backend/migration/1690417561187-Fix.js5
-rw-r--r--packages/backend/migration/1690569881926-user-2fa-backup-codes.js5
-rw-r--r--packages/backend/migration/1691649257651-refine-announcement.js5
-rw-r--r--packages/backend/migration/1691657412740-refine-announcement-2.js5
-rw-r--r--packages/backend/migration/1695260774117-verified-links.js5
-rw-r--r--packages/backend/migration/1695288787870-following-notify.js5
-rw-r--r--packages/backend/migration/1695440131671-short-name.js5
-rw-r--r--packages/backend/migration/1695605508898-mutingNotificationTypes.js5
-rw-r--r--packages/backend/migration/1696323464251-user-list-membership.js5
-rw-r--r--packages/backend/migration/1696331570827-hibernation.js5
-rw-r--r--packages/backend/migration/1696332072038-clean.js5
-rw-r--r--packages/backend/migration/1700383825690-hard-mute.js5
-rw-r--r--packages/backend/src/core/ChannelFollowingService.ts5
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts2
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts230
-rw-r--r--packages/backend/src/misc/fastify-hook-handlers.ts5
-rw-r--r--packages/backend/src/misc/is-pure-renote.ts5
-rw-r--r--packages/backend/src/misc/loader.ts5
-rw-r--r--packages/backend/src/models/ReversiGame.ts5
-rw-r--r--packages/backend/src/models/json-schema/signin.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/update.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts8
-rw-r--r--packages/backend/test/jest.setup.ts5
-rw-r--r--packages/backend/test/unit/ApMfmService.ts5
-rw-r--r--packages/backend/test/unit/entities/UserEntityService.ts528
-rw-r--r--packages/backend/test/unit/misc/loader.ts5
-rw-r--r--packages/frontend/.storybook/preview-head.html5
-rw-r--r--packages/frontend/src/components/MkLink.vue4
-rw-r--r--packages/frontend/src/components/global/I18n.vue5
-rw-r--r--packages/frontend/src/components/global/MkA.vue8
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/filters/kmg.ts5
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue20
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue27
-rw-r--r--packages/frontend/src/plugin.ts25
-rw-r--r--packages/frontend/src/scripts/check-reaction-permissions.ts5
-rw-r--r--packages/frontend/src/scripts/clear-cache.ts5
-rw-r--r--packages/frontend/src/scripts/code-highlighter.ts5
-rw-r--r--packages/frontend/src/scripts/media-has-audio.ts5
-rw-r--r--packages/frontend/src/type.ts5
-rw-r--r--packages/misskey-js/src/autogen/types.ts18
-rw-r--r--scripts/tarball.mjs5
57 files changed, 1082 insertions, 81 deletions
diff --git a/.config/example.yml b/.config/example.yml
index c037a280b6..73ee1c5346 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -38,7 +38,7 @@
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
-# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
+# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
# license again carefully. Anyway, enabling this
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e409adf644..f8d9905ecd 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -19,7 +19,6 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
- "Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml
new file mode 100644
index 0000000000..6cd8bf60d5
--- /dev/null
+++ b/.github/workflows/check-spdx-license-id.yml
@@ -0,0 +1,75 @@
+name: Check SPDX-License-Identifier
+
+on:
+ push:
+ branches:
+ - master
+ - develop
+ pull_request:
+
+jobs:
+ check-spdx-license-id:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.1.1
+ - name: Check
+ run: |
+ counter=0
+
+ search() {
+ local directory="$1"
+ find "$directory" -type f \
+ '(' \
+ -name "*.cjs" -and -not -name '*.config.cjs' -o \
+ -name "*.html" -o \
+ -name "*.js" -and -not -name '*.config.js' -o \
+ -name "*.mjs" -and -not -name '*.config.mjs' -o \
+ -name "*.scss" -o \
+ -name "*.ts" -and -not -name '*.config.ts' -o \
+ -name "*.vue" \
+ ')' -and \
+ -not -name '*eslint*'
+ }
+
+ check() {
+ local file="$1"
+ if ! (
+ grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" ||
+ grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file"
+ ); then
+ echo "Missing: $file"
+ ((counter++))
+ fi
+ }
+
+ directories=(
+ "cypress/e2e"
+ "packages/backend/migration"
+ "packages/backend/src"
+ "packages/backend/test"
+ "packages/frontend/.storybook"
+ "packages/frontend/@types"
+ "packages/frontend/lib"
+ "packages/frontend/public"
+ "packages/frontend/src"
+ "packages/frontend/test"
+ "packages/misskey-bubble-game/src"
+ "packages/misskey-reversi/src"
+ "packages/sw/src"
+ "scripts"
+ )
+
+ for directory in "${directories[@]}"; do
+ for file in $(search $directory); do
+ check "$file"
+ done
+ done
+
+ if [ $counter -gt 0 ]; then
+ echo "SPDX-License-Identifier is missing in $counter files."
+ exit 1
+ else
+ echo "SPDX-License-Identifier is certainly described in all target files!"
+ exit 0
+ fi
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index baca8db246..3cdf81e339 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,9 +3,7 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
- "Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
- "dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e2a82b1ffe..0ceec23acd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,7 +7,7 @@
"*.test.ts": "typescript"
},
"jest.jestCommandLine": "pnpm run jest",
- "jest.autoRun": "off",
+ "jest.runMode": "on-demand",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be621e1ebd..f1e863a8f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,11 +8,14 @@
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
+- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
+ - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
+- Fix: ローカルURLのプレビューポップアップが左上に表示される
### Server
--
+- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
## 2024.3.1
diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js
index 604241d13c..d2525e0a7d 100644
--- a/cypress/e2e/basic.cy.js
+++ b/cypress/e2e/basic.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
describe('Before setup instance', () => {
beforeEach(() => {
cy.resetState();
diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js
index 6de27be5f4..8d8fb3af31 100644
--- a/cypress/e2e/router.cy.js
+++ b/cypress/e2e/router.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
describe('Router transition', () => {
describe('Redirect', () => {
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js
index df6ec8357d..847801a69f 100644
--- a/cypress/e2e/widgets.cy.js
+++ b/cypress/e2e/widgets.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
/* flaky
describe('After user signed in', () => {
beforeEach(() => {
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 15b13708ac..36e4cbae28 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -7018,6 +7018,10 @@ export interface Locale extends ILocale {
* ソースを表示
*/
"viewSource": string;
+ /**
+ * ログを表示
+ */
+ "viewLog": string;
};
"_preferencesBackups": {
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a94cebb9f9..d014f99210 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1825,6 +1825,7 @@ _plugin:
installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理"
viewSource: "ソースを表示"
+ viewLog: "ログを表示"
_preferencesBackups:
list: "作成したバックアップ"
diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js
index 4079b3bb0a..602ced1d75 100644
--- a/packages/backend/generate_api_json.js
+++ b/packages/backend/generate_api_json.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { loadConfig } from './built/config.js'
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
@@ -5,4 +10,4 @@ import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config, true);
-writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); \ No newline at end of file
+writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
index ce246b20f8..2dc7774493 100644
--- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
+++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class UserBlacklistAnntena1689325027964 {
name = 'UserBlacklistAnntena1689325027964'
diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js
index 14150b0362..d9604ca26c 100644
--- a/packages/backend/migration/1690417561185-fix-renote-muting.js
+++ b/packages/backend/migration/1690417561185-fix-renote-muting.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class FixRenoteMuting1690417561185 {
name = 'FixRenoteMuting1690417561185'
diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
index 7eda5debe5..9bccdb3bb5 100644
--- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
+++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class ChangeCacheRemoteFilesDefault1690417561186 {
name = 'ChangeCacheRemoteFilesDefault1690417561186'
diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js
index e780e66d7b..7f1d62d68c 100644
--- a/packages/backend/migration/1690417561187-Fix.js
+++ b/packages/backend/migration/1690417561187-Fix.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Fix1690417561187 {
name = 'Fix1690417561187'
diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
index 2049df8ea2..a3ef8dcf08 100644
--- a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
+++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class User2faBackupCodes1690569881926 {
name = 'User2faBackupCodes1690569881926'
diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js
index d8d63f3103..ac621155d5 100644
--- a/packages/backend/migration/1691649257651-refine-announcement.js
+++ b/packages/backend/migration/1691649257651-refine-announcement.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class RefineAnnouncement1691649257651 {
name = 'RefineAnnouncement1691649257651'
diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js
index 8791f99f44..67edf19659 100644
--- a/packages/backend/migration/1691657412740-refine-announcement-2.js
+++ b/packages/backend/migration/1691657412740-refine-announcement-2.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class RefineAnnouncement21691657412740 {
name = 'RefineAnnouncement21691657412740'
diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js
index 18e0571d81..64c8a9ad8f 100644
--- a/packages/backend/migration/1695260774117-verified-links.js
+++ b/packages/backend/migration/1695260774117-verified-links.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class VerifiedLinks1695260774117 {
name = 'VerifiedLinks1695260774117'
diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js
index e7e2194b15..b3f78d5f2a 100644
--- a/packages/backend/migration/1695288787870-following-notify.js
+++ b/packages/backend/migration/1695288787870-following-notify.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class FollowingNotify1695288787870 {
name = 'FollowingNotify1695288787870'
diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js
index 2c37297fc1..fdc256caf8 100644
--- a/packages/backend/migration/1695440131671-short-name.js
+++ b/packages/backend/migration/1695440131671-short-name.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class ShortName1695440131671 {
name = 'ShortName1695440131671'
diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
index 8c0e52a2f0..67d4243142 100644
--- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js
+++ b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class MutingNotificationTypes1695605508898 {
name = 'MutingNotificationTypes1695605508898'
diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js
index 7534040c4c..dc1d438dd7 100644
--- a/packages/backend/migration/1696323464251-user-list-membership.js
+++ b/packages/backend/migration/1696323464251-user-list-membership.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class UserListMembership1696323464251 {
name = 'UserListMembership1696323464251'
diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js
index 119d35913f..1487ece77c 100644
--- a/packages/backend/migration/1696331570827-hibernation.js
+++ b/packages/backend/migration/1696331570827-hibernation.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Hibernation1696331570827 {
name = 'Hibernation1696331570827'
diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js
index 2eecf70cff..3900b96328 100644
--- a/packages/backend/migration/1696332072038-clean.js
+++ b/packages/backend/migration/1696332072038-clean.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Clean1696332072038 {
name = 'Clean1696332072038'
diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js
index afd3247f5c..92c3ada4a1 100644
--- a/packages/backend/migration/1700383825690-hard-mute.js
+++ b/packages/backend/migration/1700383825690-hard-mute.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class HardMute1700383825690 {
name = 'HardMute1700383825690'
diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts
index 75843b9773..12251595e2 100644
--- a/packages/backend/src/core/ChannelFollowingService.ts
+++ b/packages/backend/src/core/ChannelFollowingService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 6d9dc86c16..2658d61cc3 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -134,7 +134,7 @@ export class ApNoteService {
value,
object,
});
- throw new Error('invalid note');
+ throw err;
}
const note = object as IPost;
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 8f5d986fac..37bf856212 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -14,9 +15,31 @@ import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, listenbrainzSchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
-import { MiNotification } from '@/models/Notification.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
+import {
+ birthdaySchema,
+ descriptionSchema,
+ listenbrainzSchema,
+ localUsernameSchema,
+ locationSchema,
+ nameSchema,
+ passwordSchema,
+} from '@/models/User.js';
+import type {
+ BlockingsRepository,
+ FollowingsRepository,
+ FollowRequestsRepository,
+ MiFollowing,
+ MiUserNotePining,
+ MiUserProfile,
+ MutingsRepository,
+ NoteUnreadsRepository,
+ RenoteMutingsRepository,
+ UserMemoRepository,
+ UserNotePiningsRepository,
+ UserProfilesRepository,
+ UserSecurityKeysRepository,
+ UsersRepository,
+} from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@@ -46,11 +69,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user);
}
+export type UserRelation = {
+ id: MiUser['id']
+ following: MiFollowing | null,
+ isFollowing: boolean
+ isFollowed: boolean
+ hasPendingFollowRequestFromYou: boolean
+ hasPendingFollowRequestToYou: boolean
+ isBlocking: boolean
+ isBlocked: boolean
+ isMuted: boolean
+ isRenoteMuted: boolean
+}
+
@Injectable()
export class UserEntityService implements OnModuleInit {
private apPersonService: ApPersonService;
private noteEntityService: NoteEntityService;
- private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private announcementService: AnnouncementService;
@@ -89,9 +124,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
- @Inject(DI.driveFilesRepository)
- private driveFilesRepository: DriveFilesRepository,
-
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@@ -101,12 +133,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.announcementReadsRepository)
- private announcementReadsRepository: AnnouncementReadsRepository,
-
- @Inject(DI.announcementsRepository)
- private announcementsRepository: AnnouncementsRepository,
-
@Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository,
) {
@@ -115,7 +141,6 @@ export class UserEntityService implements OnModuleInit {
onModuleInit() {
this.apPersonService = this.moduleRef.get('ApPersonService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
- this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.announcementService = this.moduleRef.get('AnnouncementService');
@@ -139,7 +164,7 @@ export class UserEntityService implements OnModuleInit {
public isRemoteUser = isRemoteUser;
@bindThis
- public async getRelation(me: MiUser['id'], target: MiUser['id']) {
+ public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
const [
following,
isFollowed,
@@ -213,6 +238,59 @@ export class UserEntityService implements OnModuleInit {
}
@bindThis
+ public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
+ const [
+ followers,
+ followees,
+ followersRequests,
+ followeesRequests,
+ blockers,
+ blockees,
+ muters,
+ renoteMuters,
+ ] = await Promise.all([
+ this.followingsRepository.findBy({ followerId: me })
+ .then(f => new Map(f.map(it => [it.followeeId, it]))),
+ this.followingsRepository.findBy({ followeeId: me })
+ .then(it => it.map(it => it.followerId)),
+ this.followRequestsRepository.findBy({ followerId: me })
+ .then(it => it.map(it => it.followeeId)),
+ this.followRequestsRepository.findBy({ followeeId: me })
+ .then(it => it.map(it => it.followerId)),
+ this.blockingsRepository.findBy({ blockerId: me })
+ .then(it => it.map(it => it.blockeeId)),
+ this.blockingsRepository.findBy({ blockeeId: me })
+ .then(it => it.map(it => it.blockerId)),
+ this.mutingsRepository.findBy({ muterId: me })
+ .then(it => it.map(it => it.muteeId)),
+ this.renoteMutingsRepository.findBy({ muterId: me })
+ .then(it => it.map(it => it.muteeId)),
+ ]);
+
+ return new Map(
+ targets.map(target => {
+ const following = followers.get(target) ?? null;
+
+ return [
+ target,
+ {
+ id: target,
+ following: following,
+ isFollowing: following != null,
+ isFollowed: followees.includes(target),
+ hasPendingFollowRequestFromYou: followersRequests.includes(target),
+ hasPendingFollowRequestToYou: followeesRequests.includes(target),
+ isBlocking: blockers.includes(target),
+ isBlocked: blockees.includes(target),
+ isMuted: muters.includes(target),
+ isRenoteMuted: renoteMuters.includes(target),
+ },
+ ];
+ }),
+ );
+ }
+
+ @bindThis
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
@@ -304,6 +382,9 @@ export class UserEntityService implements OnModuleInit {
schema?: S,
includeSecrets?: boolean,
userProfile?: MiUserProfile,
+ userRelations?: Map<MiUser['id'], UserRelation>,
+ userMemos?: Map<MiUser['id'], string | null>,
+ pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
},
): Promise<Packed<S>> {
const opts = Object.assign({
@@ -344,13 +425,41 @@ export class UserEntityService implements OnModuleInit {
const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
- const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
- const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
- .where('pin.userId = :userId', { userId: user.id })
- .innerJoinAndSelect('pin.note', 'note')
- .orderBy('pin.id', 'DESC')
- .getMany() : [];
- const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
+ const profile = isDetailed
+ ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
+ : null;
+
+ let relation: UserRelation | null = null;
+ if (meId && !isMe && isDetailed) {
+ if (opts.userRelations) {
+ relation = opts.userRelations.get(user.id) ?? null;
+ } else {
+ relation = await this.getRelation(meId, user.id);
+ }
+ }
+
+ let memo: string | null = null;
+ if (isDetailed && meId) {
+ if (opts.userMemos) {
+ memo = opts.userMemos.get(user.id) ?? null;
+ } else {
+ memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
+ .then(row => row?.memo ?? null);
+ }
+ }
+
+ let pins: MiUserNotePining[] = [];
+ if (isDetailed) {
+ if (opts.pinNotes) {
+ pins = opts.pinNotes.get(user.id) ?? [];
+ } else {
+ pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
+ .where('pin.userId = :userId', { userId: user.id })
+ .innerJoinAndSelect('pin.note', 'note')
+ .orderBy('pin.id', 'DESC')
+ .getMany();
+ }
+ }
const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
@@ -452,9 +561,7 @@ export class UserEntityService implements OnModuleInit {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
- ? this.userSecurityKeysRepository.countBy({
- userId: user.id,
- }).then(result => result >= 1)
+ ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
@@ -466,10 +573,7 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
- memo: meId == null ? null : await this.userMemosRepository.findOneBy({
- userId: meId,
- targetUserId: user.id,
- }).then(row => row?.memo ?? null),
+ memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
@@ -552,7 +656,7 @@ export class UserEntityService implements OnModuleInit {
return await awaitAll(packed);
}
- public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
+ public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
@@ -560,6 +664,70 @@ export class UserEntityService implements OnModuleInit {
includeSecrets?: boolean,
},
): Promise<Packed<S>[]> {
- return Promise.all(users.map(u => this.pack(u, me, options)));
+ // -- IDのみの要素を補完して完全なエンティティ一覧を作る
+
+ const _users = users.filter((user): user is MiUser => typeof user !== 'string');
+ if (_users.length !== users.length) {
+ _users.push(
+ ...await this.usersRepository.findBy({
+ id: In(users.filter((user): user is string => typeof user === 'string')),
+ }),
+ );
+ }
+ const _userIds = _users.map(u => u.id);
+
+ // -- 特に前提条件のない値群を取得
+
+ const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
+ .then(profiles => new Map(profiles.map(p => [p.userId, p])));
+
+ // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
+
+ let userRelations: Map<MiUser['id'], UserRelation> = new Map();
+ let userMemos: Map<MiUser['id'], string | null> = new Map();
+ let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
+
+ if (options?.schema !== 'UserLite') {
+ const meId = me ? me.id : null;
+ if (meId) {
+ userMemos = await this.userMemosRepository.findBy({ userId: meId })
+ .then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
+
+ if (_userIds.length > 0) {
+ userRelations = await this.getRelations(meId, _userIds);
+ pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
+ .where('pin.userId IN (:...userIds)', { userIds: _userIds })
+ .innerJoinAndSelect('pin.note', 'note')
+ .getMany()
+ .then(pinsNotes => {
+ const map = new Map<MiUser['id'], MiUserNotePining[]>();
+ for (const note of pinsNotes) {
+ const notes = map.get(note.userId) ?? [];
+ notes.push(note);
+ map.set(note.userId, notes);
+ }
+ for (const [, notes] of map.entries()) {
+ // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
+ notes.sort((a, b) => b.id.localeCompare(a.id));
+ }
+ return map;
+ });
+ }
+ }
+ }
+
+ return Promise.all(
+ _users.map(u => this.pack(
+ u,
+ me,
+ {
+ ...options,
+ userProfile: profilesMap.get(u.id),
+ userRelations: userRelations,
+ userMemos: userMemos,
+ pinNotes: pinNotes,
+ },
+ )),
+ );
}
}
diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts
index 49a48f6a6b..3e1c099e00 100644
--- a/packages/backend/src/misc/fastify-hook-handlers.ts
+++ b/packages/backend/src/misc/fastify-hook-handlers.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts
index 994d981522..f9c2243a06 100644
--- a/packages/backend/src/misc/is-pure-renote.ts
+++ b/packages/backend/src/misc/is-pure-renote.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import type { MiNote } from '@/models/Note.js';
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts
index 25f7b54d31..7f29b9db10 100644
--- a/packages/backend/src/misc/loader.ts
+++ b/packages/backend/src/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export type FetchFunction<K, V> = (key: K) => Promise<V>;
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index c03335dd63..6b29a0ce8c 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts
index d27d2490c5..45732a742b 100644
--- a/packages/backend/src/models/json-schema/signin.ts
+++ b/packages/backend/src/models/json-schema/signin.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export const packedSigninSchema = {
type: 'object',
properties: {
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 459729f61f..76a34924a0 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -67,7 +67,7 @@ export const paramDef = {
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
- required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['antennaId'],
} as const;
@Injectable()
@@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ if (ps.keywords && ps.excludeKeywords) {
+ if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
+ throw new Error('either keywords or excludeKeywords is required.');
+ }
}
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
@@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let userList;
- if (ps.src === 'list' && ps.userListId) {
+ if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
@@ -112,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
- userListId: userList ? userList.id : null,
+ userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 6a5b2262fa..1d75437b81 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
-
- const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
-
- return Array.isArray(ps.userId) ? relations : relations[0];
+ return Array.isArray(ps.userId)
+ ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
+ : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
});
}
}
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index cf5b9bf24d..861bc6db66 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { initTestDb, sendEnvResetRequest } from './utils.js';
beforeAll(async () => {
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 2b79041c86..79cb81f5c9 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import * as assert from 'assert';
import { Test } from '@nestjs/testing';
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
new file mode 100644
index 0000000000..ee16d421c4
--- /dev/null
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -0,0 +1,528 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import type { MiUser } from '@/models/User.js';
+import { secureRndstr } from '@/misc/secure-rndstr.js';
+import { genAidx } from '@/misc/id/aidx.js';
+import {
+ BlockingsRepository,
+ FollowingsRepository, FollowRequestsRepository,
+ MiUserProfile, MutingsRepository, RenoteMutingsRepository,
+ UserMemoRepository,
+ UserProfilesRepository,
+ UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { PageEntityService } from '@/core/entities/PageEntityService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
+import { IdService } from '@/core/IdService.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
+import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
+import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
+import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
+import { MfmService } from '@/core/MfmService.js';
+import { HashtagService } from '@/core/HashtagService.js';
+import UsersChart from '@/core/chart/charts/users.js';
+import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js';
+import InstanceChart from '@/core/chart/charts/instance.js';
+import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
+import { ReactionService } from '@/core/ReactionService.js';
+import { NotificationService } from '@/core/NotificationService.js';
+
+process.env.NODE_ENV = 'test';
+
+describe('UserEntityService', () => {
+ describe('pack/packMany', () => {
+ let app: TestingModule;
+ let service: UserEntityService;
+ let usersRepository: UsersRepository;
+ let userProfileRepository: UserProfilesRepository;
+ let userMemosRepository: UserMemoRepository;
+ let followingRepository: FollowingsRepository;
+ let followingRequestRepository: FollowRequestsRepository;
+ let blockingRepository: BlockingsRepository;
+ let mutingRepository: MutingsRepository;
+ let renoteMutingsRepository: RenoteMutingsRepository;
+
+ async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
+ const un = secureRndstr(16);
+ const user = await usersRepository
+ .insert({
+ ...userData,
+ id: genAidx(Date.now()),
+ username: un,
+ usernameLower: un,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfileRepository.insert({
+ ...profileData,
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ async function memo(writer: MiUser, target: MiUser, memo: string) {
+ await userMemosRepository.insert({
+ id: genAidx(Date.now()),
+ userId: writer.id,
+ targetUserId: target.id,
+ memo,
+ });
+ }
+
+ async function follow(follower: MiUser, followee: MiUser) {
+ await followingRepository.insert({
+ id: genAidx(Date.now()),
+ followerId: follower.id,
+ followeeId: followee.id,
+ });
+ }
+
+ async function requestFollow(requester: MiUser, requestee: MiUser) {
+ await followingRequestRepository.insert({
+ id: genAidx(Date.now()),
+ followerId: requester.id,
+ followeeId: requestee.id,
+ });
+ }
+
+ async function block(blocker: MiUser, blockee: MiUser) {
+ await blockingRepository.insert({
+ id: genAidx(Date.now()),
+ blockerId: blocker.id,
+ blockeeId: blockee.id,
+ });
+ }
+
+ async function mute(mutant: MiUser, mutee: MiUser) {
+ await mutingRepository.insert({
+ id: genAidx(Date.now()),
+ muterId: mutant.id,
+ muteeId: mutee.id,
+ });
+ }
+
+ async function muteRenote(mutant: MiUser, mutee: MiUser) {
+ await renoteMutingsRepository.insert({
+ id: genAidx(Date.now()),
+ muterId: mutant.id,
+ muteeId: mutee.id,
+ });
+ }
+
+ function randomIntRange(weight = 10) {
+ return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx);
+ }
+
+ beforeAll(async () => {
+ const services = [
+ UserEntityService,
+ ApPersonService,
+ NoteEntityService,
+ PageEntityService,
+ CustomEmojiService,
+ AnnouncementService,
+ RoleService,
+ FederatedInstanceService,
+ IdService,
+ AvatarDecorationService,
+ UtilityService,
+ EmojiEntityService,
+ ModerationLogService,
+ GlobalEventService,
+ DriveFileEntityService,
+ MetaService,
+ FetchInstanceMetadataService,
+ CacheService,
+ ApResolverService,
+ ApNoteService,
+ ApImageService,
+ ApMfmService,
+ MfmService,
+ HashtagService,
+ UsersChart,
+ ChartLoggerService,
+ InstanceChart,
+ ApLoggerService,
+ AccountMoveService,
+ ReactionService,
+ NotificationService,
+ ];
+
+ app = await Test.createTestingModule({
+ imports: [GlobalModule, CoreModule],
+ providers: [
+ ...services,
+ ...services.map(x => ({ provide: x.name, useExisting: x })),
+ ],
+ }).compile();
+ await app.init();
+ app.enableShutdownHooks();
+
+ service = app.get<UserEntityService>(UserEntityService);
+ usersRepository = app.get<UsersRepository>(DI.usersRepository);
+ userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+ userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository);
+ followingRepository = app.get<FollowingsRepository>(DI.followingsRepository);
+ followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
+ blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
+ mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
+ renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ test('UserLite', async() => {
+ const me = await createUser();
+ const who = await createUser();
+
+ await memo(me, who, 'memo');
+
+ const actual = await service.pack(who, me, { schema: 'UserLite' }) as any;
+ // no detail
+ expect(actual.memo).toBeUndefined();
+ // no detail and me
+ expect(actual.birthday).toBeUndefined();
+ // no detail and me
+ expect(actual.achievements).toBeUndefined();
+ });
+
+ test('UserDetailedNotMe', async() => {
+ const me = await createUser();
+ const who = await createUser({}, { birthday: '2000-01-01' });
+
+ await memo(me, who, 'memo');
+
+ const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any;
+ // is detail
+ expect(actual.memo).toBe('memo');
+ // is detail
+ expect(actual.birthday).toBe('2000-01-01');
+ // no detail and me
+ expect(actual.achievements).toBeUndefined();
+ });
+
+ test('MeDetailed', async() => {
+ const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+ const me = await createUser({}, {
+ birthday: '2000-01-01',
+ achievements: achievements,
+ });
+ await memo(me, me, 'memo');
+
+ const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any;
+ // is detail
+ expect(actual.memo).toBe('memo');
+ // is detail
+ expect(actual.birthday).toBe('2000-01-01');
+ // is detail and me
+ expect(actual.achievements).toEqual(achievements);
+ });
+
+ describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => {
+ test('no-preload', async() => {
+ const me = await createUser();
+ // meがフォローしてる人たち
+ const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followeeMe) {
+ await follow(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(true);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meをフォローしてる人たち
+ const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followerMe) {
+ await follow(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(true);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがフォローリクエストを送った人たち
+ const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsFromYou) {
+ await requestFollow(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meにフォローリクエストを送った人たち
+ const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsToYou) {
+ await requestFollow(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(true);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがブロックしてる人たち
+ const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingYou) {
+ await block(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(true);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meをブロックしてる人たち
+ const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingMe) {
+ await block(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(true);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがミュートしてる人たち
+ const muters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of muters) {
+ await mute(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(true);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがリノートミュートしてる人たち
+ const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of renoteMuters) {
+ await muteRenote(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(true);
+ }
+ });
+
+ test('preload', async() => {
+ const me = await createUser();
+
+ {
+ // meがフォローしてる人たち
+ const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followeeMe) {
+ await follow(me, who);
+ }
+ const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(true);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meをフォローしてる人たち
+ const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followerMe) {
+ await follow(who, me);
+ }
+ const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(true);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがフォローリクエストを送った人たち
+ const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsFromYou) {
+ await requestFollow(me, who);
+ }
+ const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meにフォローリクエストを送った人たち
+ const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsToYou) {
+ await requestFollow(who, me);
+ }
+ const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(true);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがブロックしてる人たち
+ const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingYou) {
+ await block(me, who);
+ }
+ const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(true);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meをブロックしてる人たち
+ const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingMe) {
+ await block(who, me);
+ }
+ const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(true);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがミュートしてる人たち
+ const muters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of muters) {
+ await mute(me, who);
+ }
+ const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(true);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがリノートミュートしてる人たち
+ const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of renoteMuters) {
+ await muteRenote(me, who);
+ }
+ const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(true);
+ }
+ }
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts
index fa37950951..2cf54e1555 100644
--- a/packages/backend/test/unit/misc/loader.ts
+++ b/packages/backend/test/unit/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { DebounceLoader } from '@/misc/loader.js';
class Mock {
diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html
index 30f3ebfb64..e50c488243 100644
--- a/packages/frontend/.storybook/preview-head.html
+++ b/packages/frontend/.storybook/preview-head.html
@@ -1,3 +1,8 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 95de0d0247..3194afa649 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -30,13 +30,13 @@ const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | { $el: HTMLElement }>();
useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
- source: el.value,
+ source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed');
});
</script>
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue
index 162aa2bcf8..6b7723e6ac 100644
--- a/packages/frontend/src/components/global/I18n.vue
+++ b/packages/frontend/src/components/global/I18n.vue
@@ -1,3 +1,8 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
<template>
<render/>
</template>
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index b3c58cf235..1f8c7fb399 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
+<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
<slot></slot>
</a>
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, shallowRef } from 'vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
@@ -26,6 +26,10 @@ const props = withDefaults(defineProps<{
behavior: null,
});
+const el = shallowRef<HTMLElement>();
+
+defineExpose({ $el: el });
+
const router = useRouter();
const active = computed(() => {
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index b810840b69..665cfcf1ab 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -50,7 +50,7 @@ if (props.showUrlPreview) {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
- source: el.value,
+ source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed');
});
}
diff --git a/packages/frontend/src/filters/kmg.ts b/packages/frontend/src/filters/kmg.ts
index 4dcb5c5800..9608e420f6 100644
--- a/packages/frontend/src/filters/kmg.ts
+++ b/packages/frontend/src/filters/kmg.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export default (v, fractionDigits = 0) => {
if (v == null) return 'N/A';
if (v === 0) return '0';
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index f1699f726e..4f1b3657bc 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -42,12 +42,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkFolder>
+ <template #icon><i class="ph-terminal-window ph-bold ph-lg"></i></template>
+ <template #label>{{ i18n.ts._plugin.viewLog }}</template>
+
+ <div class="_gaps_s">
+ <div class="_buttons">
+ <MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+ </div>
+
+ <MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<div class="_gaps_s">
<div class="_buttons">
- <MkButton inline @click="copy(plugin)"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton>
+ <MkButton inline @click="copy(plugin.src)"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton>
</div>
<MkCode :code="plugin.src ?? ''" lang="is"/>
@@ -74,6 +87,7 @@ import { ColdDeviceStorage } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { pluginLogs } from '@/plugin.js';
const plugins = ref(ColdDeviceStorage.get('plugins'));
@@ -87,8 +101,8 @@ async function uninstall(plugin) {
});
}
-function copy(plugin) {
- copyToClipboard(plugin.src ?? '');
+function copy(text) {
+ copyToClipboard(text ?? '');
os.success();
}
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 255e07c3fa..14960f6b9b 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -9,7 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<XTimeline class="tl"/>
<div class="shape1"></div>
<div class="shape2"></div>
- <img :src="misskeysvg" class="misskey"/>
+ <div class="logo-wrapper">
+ <div class="powered-by">Powered by</div>
+ <img :src="misskeysvg" class="misskey"/>
+ </div>
<div class="emojis">
<MkEmoji :normal="true" :noStyle="true" emoji="👍"/>
<MkEmoji :normal="true" :noStyle="true" emoji="❤"/>
@@ -113,14 +116,24 @@ misskeyApiGet('federation/instances', {
opacity: 0.5;
}
- > .misskey {
+ > .logo-wrapper {
position: fixed;
- top: 42px;
- left: 42px;
- width: 140px;
+ top: 36px;
+ left: 36px;
+ flex: auto;
+ color: #fff;
+ user-select: none;
+ pointer-events: none;
+
+ > .powered-by {
+ margin-bottom: 2px;
+ }
- @media (max-width: 450px) {
- width: 130px;
+ > .misskey {
+ width: 140px;
+ @media (max-width: 450px) {
+ width: 130px;
+ }
}
}
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index 743cadc36a..81233a5a5e 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { ref } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { inputText } from '@/os.js';
@@ -10,6 +11,7 @@ import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFo
const parser = new Parser();
const pluginContexts = new Map<string, Interpreter>();
+export const pluginLogs = ref(new Map<string, string[]>());
export async function install(plugin: Plugin): Promise<void> {
// 後方互換性のため
@@ -22,21 +24,27 @@ export async function install(plugin: Plugin): Promise<void> {
in: aiScriptReadline,
out: (value): void => {
console.log(value);
+ pluginLogs.value.get(plugin.id).push(utils.reprValue(value));
},
log: (): void => {
},
+ err: (err): void => {
+ pluginLogs.value.get(plugin.id).push(`${err}`);
+ throw err; // install時のtry-catchに反応させる
+ },
});
initPlugin({ plugin, aiscript });
- try {
- await aiscript.exec(parser.parse(plugin.src));
- } catch (err) {
- console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
- return;
- }
-
- console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+ aiscript.exec(parser.parse(plugin.src)).then(
+ () => {
+ console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+ },
+ (err) => {
+ console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
+ throw err;
+ },
+ );
}
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
@@ -92,6 +100,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
function initPlugin({ plugin, aiscript }): void {
pluginContexts.set(plugin.id, aiscript);
+ pluginLogs.value.set(plugin.id, []);
}
function registerPostFormAction({ pluginId, title, handler }): void {
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
index e7b473dd75..8fc857f84f 100644
--- a/packages/frontend/src/scripts/check-reaction-permissions.ts
+++ b/packages/frontend/src/scripts/check-reaction-permissions.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import * as Misskey from 'misskey-js';
import { UnicodeEmojiDef } from './emojilist.js';
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts
index b20109ec72..71d1232710 100644
--- a/packages/frontend/src/scripts/clear-cache.ts
+++ b/packages/frontend/src/scripts/clear-cache.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { unisonReload } from '@/scripts/unison-reload.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 2733897bab..5dd0a3be78 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { bundledThemesInfo } from 'shiki';
import { getHighlighterCore, loadWasm } from 'shiki/core';
import darkPlus from 'shiki/themes/dark-plus.mjs';
diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts
index 3421a38a76..4bf3ee5d97 100644
--- a/packages/frontend/src/scripts/media-has-audio.ts
+++ b/packages/frontend/src/scripts/media-has-audio.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export default async function hasAudio(media: HTMLMediaElement) {
const cloned = media.cloneNode() as HTMLMediaElement;
cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts
index 9c0fc2a11e..5ff27158d2 100644
--- a/packages/frontend/src/type.ts
+++ b/packages/frontend/src/type.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index db14823d37..2cc9aef1df 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -10350,19 +10350,19 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
antennaId: string;
- name: string;
+ name?: string;
/** @enum {string} */
- src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
+ src?: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
/** Format: misskey:id */
userListId?: string | null;
- keywords: string[][];
- excludeKeywords: string[][];
- users: string[];
- caseSensitive: boolean;
+ keywords?: string[][];
+ excludeKeywords?: string[][];
+ users?: string[];
+ caseSensitive?: boolean;
localOnly?: boolean;
- withReplies: boolean;
- withFile: boolean;
- notify: boolean;
+ withReplies?: boolean;
+ withFile?: boolean;
+ notify?: boolean;
};
};
};
diff --git a/scripts/tarball.mjs b/scripts/tarball.mjs
index fbb833d94e..707cd3e7e5 100644
--- a/scripts/tarball.mjs
+++ b/scripts/tarball.mjs
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { createWriteStream } from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';